SERVICE
Capy
function SERVICE:NAME() function SERVICE:NAME(props : any) function SERVICE:NAME(request : any) function SERVICE:NAME(props : any, request : any)
A service is a unit that keeps running between requests, in a process of its own, with one workspace for its whole life. SERVICE handlers are its events. Bearer runs each one as a short, bounded call, one at a time in event order, and the unit's top-level variables carry the state from one call to the next.
A service handler is always named. START, STOP, DATA, EXIT, and ACCEPT are raised by the runtime. Every other name is a signal that service_send() or service_every() delivers. The name follows the TASK:NAME rule: a letter or underscore, then letters, digits, and underscores.
Every event body has props and request of type any, even without parameter declarations. If both are declared, props comes first. Their names and types are fixed. The request is the service event context, not the HTTP request that started the service.
Events
| Handler | props | When |
|---|---|---|
SERVICE:START | the props given to service_start(); {} when a service_send() started the service | once, after the workspace is up and before any message |
SERVICE:STOP | {} | after service_stop() or a STOP message; the worker exits when it returns |
SERVICE:DATA | port, stream, data | a port had bytes to read |
SERVICE:EXIT | port, status, and on failure error and detail | a child exited or a connection closed, after its last DATA |
SERVICE:ACCEPT | port, connection | a listening port accepted a connection |
SERVICE:NAME | the posted props | a NAME message arrived |
port is the u64 handle a port_* call returned. stream is stdout or stderr for a spawned child and socket for a connection. data is a string holding whatever one read returned, up to 16 KiB, with no line framing; a service that wants lines keeps the partial tail in a unit variable, as the example below does. status is the child's exit code, 0 for a connection the peer closed, and -1 with error and detail when a connection or a read failed: connect_failed, read_failed, command_exit_127 for a program that could not be executed, child_status_lost. A spawned child that cannot enter options.cwd reports status 127 with error: "cwd_failed". port_spawn(argv, options) accepts only cwd and env options. Relative cwd paths start at the calling unit's directory. No membrane check applies. The child inherits the worker's filesystem view and operating-system restrictions.
connection is a port handle of its own; bytes on it arrive as DATA with that handle and its close as EXIT.
A complete service
This unit runs a pool of child processes, hands each job to an idle child, writes every answer line to a file, replaces a child that dies, and reports its size every thirty seconds. The jobs here are lines of text and the children run tr, which upper-cases them.
var command := none
var idle := {}
var busy := {}
var buffers := {}
function SERVICE:START(props : any)
{
command = props.command
for i := 0..s64(props.count) { spawn() }
service_every("SWEEP", 30000)
}
function spawn()
{
var worker := port_spawn(command)
if worker != 0 { idle[string(worker)] = true }
}
function SERVICE:ASSIGN(props : any)
{
for unused, key := idle
{
delete(idle, key)
busy[key] = props.job
port_write(u64(key), string(props.job) + "\n")
return
}
service_send(string(request.unit), "ASSIGN", props, 500)
}
function SERVICE:DATA(props : any)
{
var key := string(props.port)
var text := string(buffers[key]) + string(props.data)
while strpos(text, "\n") >= 0
{
var cut := strpos(text, "\n")
file_append(dirname(string(request.unit)) + "/results.log", substr(text, 0, cut) + "\n")
text = substr(text, cut + 1)
delete(busy, key)
idle[key] = true
}
buffers[key] = text
}
function SERVICE:EXIT(props : any)
{
var key := string(props.port)
delete(idle, key)
delete(busy, key)
delete(buffers, key)
service_send(string(request.unit), "RESPAWN", {}, 2000)
}
function SERVICE:RESPAWN(props : any)
{
spawn()
}
function SERVICE:SWEEP(props : any)
{
print("pool idle=", length(idle), " busy=", length(busy), "\n")
}
function SERVICE:STOP(props : any)
{
for unused, key := idle { port_close(u64(key)) }
for unused, key := busy { port_close(u64(key)) }
}
function CLI:START()
{
print(service_start(string(request.unit), command: ["stdbuf", "-oL", "tr", "a-z", "A-Z"], count: 2))
}
function CLI:ASSIGN()
{
print(service_send(string(request.unit), "ASSIGN", job: string(request.query.job)))
}
function CLI:STATUS()
{
var status := service_status(string(request.unit))
print(status.state, " restarts=", status.restarts, " mailbox=", status.mailbox)
}
function CLI:STOP()
{
print(service_stop(string(request.unit)))
}A page or CLI call gets a separate workspace. It cannot read the running service's variables, even when it uses the same unit. Props cross a serialization boundary.
The four top-level variables are the whole state: the command to run, the idle and busy children keyed by port handle, and the unread tail of each child's output. They are initialized when the workspace comes up and keep their values until the service stops.
START receives the props of the service_start() call. It spawns the children and arms the interval timer. Everything the service owns is created here, because START is also what runs after a crash restart, when the ports and timers of the old worker are gone.
ASSIGN is a signal the unit defines for itself. When no child is idle it posts the same message back to its own unit with a delay, which is how a service waits without blocking: the handler returns, the loop goes on with other events, and the message comes back half a second later.
DATA gets the bytes one read returned, which may be part of a line or several lines. The handler appends them to that child's tail, cuts off every complete line, and keeps the rest. EXIT forgets the child and schedules a replacement two seconds later, so a child that dies at once does not spin. SWEEP runs every thirty seconds from the timer START armed; what it prints goes to the operator log. STOP closes every port, which sends the children SIGTERM.
Save the complete unit as /agents/pool.capy under the site root. The CLI names route to CLI:START, CLI:ASSIGN, CLI:STATUS, and CLI:STOP. They do not directly route to SERVICE handlers. The CLI handlers are the side that talks to the service. A request handler does the same with the same calls. tr is run through stdbuf -oL because a child that buffers its output delivers it in large chunks or at exit, and the service would see nothing per line.
bearer-cli /agents/pool.capy:START
bearer-cli --get /agents/pool.capy:ASSIGN job=hello
bearer-cli /agents/pool.capy:STATUS
bearer-cli /agents/pool.capy:STOPStart and send report acceptance, not completed work. Status can still report starting immediately after start. Read results.log beside the saved unit after a worker processes the job.
This example has no durable job queue or completion acknowledgment. A child exit discards its busy job. Do not use it for work that must survive failure. Its line handler treats both stdout and stderr as answers, so use children whose stderr cannot contain job records.
Addressing a service
A service is identified by its unit path, and there is one service per unit. service_start(), service_send(), service_status(), and service_stop() take a path relative to the calling unit, or a site-root path with a leading /. string(request.unit) is the unit's own source path, so a service posts to itself with it, as ASSIGN and EXIT do above.
The request
Like every handler, a service handler binds request implicitly. A service has no HTTP request, so this one is synthetic: no session, no body, no form. It carries the start props in request.props, the unit path in request.unit, and the server values, and it lets request builtins such as ws_send() and unit_call() work unchanged. It is bound once for the life of the service. Keep state in unit variables, not in request keys.
Lifetime
Each handler call runs under the invocation deadline. A handler that hangs is interrupted, that event is dropped with an operator log line, and the service continues with the next event. A signal the unit has no handler for is dropped and logged. A service has no caller response buffer. print() writes to the service process's stdout. Use log() for structured operator records.
STOP jumps ahead of queued messages. Once it returns, the worker exits and whatever was still queued is dropped. service_stop() keeps a service down until the next service_start() or service_send(); a send alone brings it back, with {} as start props.
A worker that exits without a stop has crashed. The supervisor starts it again after a delay that doubles from two seconds to a cap of one minute, START runs again with the original start props, and restarts in service_status() counts it. Ports, timers, and queued messages of the crashed worker are gone. Write START so that it rebuilds everything the service needs.
A service does not restart when its source changes. stale in service_status() says that the running workspace predates the source. Stop and start it, which recompiles the unit.
var buffers := {}
function SERVICE:START(props : any)
{
port_spawn(["printf", "hello\nworld\n"])
}
function SERVICE:DATA(props : any)
{
var key := string(props.port)
var text := string(buffers[key]) + string(props.data)
while strpos(text, "\n") >= 0
{
var cut := strpos(text, "\n")
print(substr(text, 0, cut), "\n")
text = substr(text, cut + 1)
}
buffers[key] = text
}
var children := {}
var replaced := false
function SERVICE:START(props : any)
{
var child := port_spawn(["true"])
children[string(child)] = true
}
function SERVICE:EXIT(props : any)
{
delete(children, string(props.port))
if !replaced
{
service_send(string(request.unit), "RESPAWN", {}, 2000)
}
}
function SERVICE:RESPAWN(props : any)
{
replaced = true
var child := port_spawn(["true"])
children[string(child)] = true
print("replacement started\n")
}
Errors
A handler that traps or exceeds the invocation deadline drops that one event and is logged. A START that traps is logged and the service keeps running with whatever START left behind. A worker that cannot compile its unit or build its workspace exits, which the supervisor records as a crash and retries. port_* and service_every() calls outside a service worker trap.