Service Lifecycle
This tutorial walks through every stage of a service lifecycle so you can build intuition for what happens under the hood.
Prerequisites
- xs installed and on your PATH (see Installation)
- Two terminal windows, both running
Nushellwithuse xs.nu *
Serve
Start a store in terminal 1:
xs serve ./store
Monitor the stream
In terminal 2, start a live monitor so we can watch lifecycle frames as they appear:
.cat -f | each { if $in.hash != null { insert content { .cas $in.hash } } else { } | print ($in | table -e)}Keep this running, every frame we discuss will show up here.
Create a service
Create a temporary file for the service to watch:
touch /tmp/log.txtNow create a service that tails the file:
r#'{ run: {|| ^tail -F /tmp/log.txt | lines }}'# | .append xs.service.log.createYour monitor shows two frames:
-#-+-----------------topic----------------+---id---+-hash-+-----meta--- 0 | xs.service.log.create | 03ab.. | ... | 1 | xs.service.log.active | 03ab.. | | source_id: 03ab..xs.service.log.create carries the script in CAS. xs.service.log.active
signals the pipeline is live; meta.source_id points back at the create.
See output
Write some lines to the file:
"hello\nworld\n" | save -a /tmp/log.txtTwo log.recv frames appear, one per line. (Note: recv/send are
app-namespace data topics, not under xs..) Read the content back:
.last log.recv | .cas $in.hashworld
Auto-restart
Remove the file so tail exits:
rm /tmp/log.txtNo lifecycle frame is emitted, the service’s run-loop treats a natural
completion as an internal hiccup and quietly restarts after 1 second. A new
xs.service.log.active frame appears.
Recreate the file and write to it:
touch /tmp/log.txt"back in business\n" | save -a /tmp/log.txtA fresh log.recv frame appears. The service recovered automatically.
Hot reload
Append a new xs.service.log.create while the service is running:
r#'{ run: {|| ^tail -F /tmp/log.txt | lines | each {|line| $"[LOG] ($line)" } }}'# | .append xs.service.log.createThe running service picks up the new create, stops the old pipeline, emits
xs.service.log.replaced (with meta.update_id pointing at the new
create), and a new xs.service.log.active follows for the reloaded task.
Verify the new behaviour:
"reloaded\n" | save -a /tmp/log.txt.last log.recv | .cas $in.hash[LOG] reloaded
The output now includes the [LOG] prefix.
What if the new script is broken?
If the replacement create has a parse error, the runtime emits
xs.service.log.invalid and the previous service keeps running. On the next
restart of xs, compaction also keeps the previous (good) create as the
fallback. The system never lands in an empty state because of a typo. See
the compaction algorithm for the rules.
Terminate
Stop the service explicitly:
.append xs.service.log.termThe monitor shows two frames:
xs.service.log.term, your requestxs.service.log.fin.term, the runtime ack confirming the stop
After fin.term, the service stays down. On the next restart of xs, it
will not be started: compaction sees the fin.* and clears its slots.
Graceful shutdown
When xs itself stops (e.g. Ctrl+C), it emits an xs.stopping frame. Every
running service sees this frame, interrupts its pipeline, and emits
xs.service.<name>.stopped as its ack.
Restart the service so we can see this in action:
r#'{ run: {|| ^tail -F /tmp/log.txt | lines }}'# | .append xs.service.log.createWait for xs.service.log.active to appear, then press Ctrl+C in
terminal 1 (where xs serve is running).
The monitor shows:
xs.stopping, emitted by xs before exitxs.service.log.stopped, the service acknowledging the shutdown
xs waits up to a few seconds for all services to finish before exiting. This gives services a chance to flush output and clean up resources.
Crucially, stopped is distinct from fin.*: compaction does not treat
it as terminal. On the next start of xs, the service resumes (its create is
still the latest known-good entry).
Recap
User inputs and runtime acks for a service:
| Event | Direction | When |
|---|---|---|
xs.service.<name>.create | user -> stream | You start (or reload) the service |
xs.service.<name>.term | user -> stream | You ask the service to stop |
xs.service.<name>.active | runtime -> stream | Pipeline is live |
xs.service.<name>.invalid | runtime -> stream | The create’s script failed to parse |
xs.service.<name>.fin.error | runtime -> stream | Runtime crash; service stays down |
xs.service.<name>.fin.term | runtime -> stream | Stop ack for .term |
xs.service.<name>.replaced | runtime -> stream | Hot-reloaded by a newer .create |
xs.service.<name>.stopped | runtime -> stream | Stopped because of xs.stopping |
Plus the app-namespace data channels: <name>.recv (output, one frame per
line), <name>.send (input, duplex services only).
For the full event vocabulary, the compaction algorithm, and the invariants the dispatcher honors, see the lifecycle reference.