sig_service.h

include/sig/runtime/sig_service.h

Threading contract: - sig_service_start() runs every owned instance's module `start` hook SYNCHRONOUSLY on the CALLING thread, before spawning the service's own thread -- by the time start() returns, start hooks have already run. sig_service_stop() runs every owned instance's `stop` hook synchronously on the calling thread, AFTER joining the service's thread -- by the time stop() returns, the thread has fully exited and stop hooks have run. - Before each instance's start hook, sig_capsule_instance_resolve() fills in its property values. If resolving or a start hook fails, sig_service_start() stops starting, runs the stop hook of every instance that had started, cancels any jobs those hooks scheduled, spawns no thread, and returns that failure status (the service stays stopped and can be started again). - Once a service is started, terminal handlers (sig_terminal_fn) and lifecycle hooks run exclusively on that service's own thread. See sig_capsule.h for what this means for an instance's storage/state. - sig_service_stop() and sig_service_destroy() are both safe to call on a service that was never started, or was already stopped -- both are no-ops in that case, not undefined behavior. - sig_service_destroy() stops the service first if it is still running, so it is always safe to call directly without a prior stop(). sig_service_start() on an already-started service returns SIG_ERR_EXISTS rather than spawning a second thread. - sig_service_enqueue() is safe to call from any thread, including the service's own.

sig_service

typedef struct sig_service sig_service;

sig_service_create

sig_status_t sig_service_create(sig_service ** out, const char * name)
ParameterType
outsig_service **
nameconst char *

sig_service_destroy

void sig_service_destroy(sig_service * svc)
ParameterType
svcsig_service *

sig_service_add_instance

sig_status_t sig_service_add_instance(sig_service * svc, sig_capsule_instance * instance)
ParameterType
svcsig_service *
instancesig_capsule_instance *

Adds instance to svc's owned set (keyed by instance->name) and sets instance->service = svc. The service does not take ownership of instance -- the caller is still responsible for sig_capsule_instance_destroy() after the service (and any other holder) is done with it. See sig_capsule.h for the threading discipline this establishes: once added to a STARTED service, only that service's own thread may safely touch the instance's storage/ state directly; sig_capsule_instance_notify() is the only thread-safe way to affect it from any other thread.

sig_service_get_instance

sig_status_t sig_service_get_instance(const sig_service * svc, const char * name, sig_capsule_instance ** out)
ParameterType
svcconst sig_service *
nameconst char *
outsig_capsule_instance **

shared with sig_service_add_instance

Adds instance to svc's owned set (keyed by instance->name) and sets instance->service = svc. The service does not take ownership of instance -- the caller is still responsible for sig_capsule_instance_destroy() after the service (and any other holder) is done with it. See sig_capsule.h for the threading discipline this establishes: once added to a STARTED service, only that service's own thread may safely touch the instance's storage/ state directly; sig_capsule_instance_notify() is the only thread-safe way to affect it from any other thread.

sig_service_name

const char * sig_service_name(const sig_service * svc)
ParameterType
svcconst sig_service *

Read-only introspection. All four are safe to call from any thread once the service's instance set is final: instances are only added during project load (sig_service_add_instance), never while a started service is running.

sig_service_is_running

bool sig_service_is_running(const sig_service * svc)
ParameterType
svcconst sig_service *

True between a successful sig_service_start() and the matching sig_service_stop(). Backed by an atomic, so it may be read from any thread concurrently with start/stop. NULL svc is false.

sig_service_instance_count

uint32_t sig_service_instance_count(const sig_service * svc)
ParameterType
svcconst sig_service *

sig_service_instance_fn

typedef sig_status_t (*sig_service_instance_fn)(sig_capsule_instance *instance, void *ctx);

Visits every owned instance, in unspecified order. A non-SIG_OK return from fn stops the walk and becomes this function's result.

sig_service_walk_instances

sig_status_t sig_service_walk_instances(const sig_service * svc, sig_service_instance_fn fn, void * ctx)
ParameterType
svcconst sig_service *
fnsig_service_instance_fn
ctxvoid *

shared with sig_service_instance_fn

Visits every owned instance, in unspecified order. A non-SIG_OK return from fn stops the walk and becomes this function's result.

sig_service_start

sig_status_t sig_service_start(sig_service * svc)
ParameterType
svcsig_service *

sig_service_stop

sig_status_t sig_service_stop(sig_service * svc)
ParameterType
svcsig_service *

sig_service_wake_fn

typedef void (*sig_service_wake_fn)(void *ctx);

Single-threaded operation. After sig_service_set_driven(svc, true), sig_service_start() starts the service exactly as usual but spawns no worker thread: the owner's event loop calls sig_service_step() instead. The mode is kept across stop/start; changing it on a started service is SIG_ERR_STATE. sig_service_step() delivers the signals queued so far, runs every job that is due, and reports when it next needs to be called: *out_has_deadline is false if nothing is scheduled (call again when woken), otherwise *out_deadline is an absolute CLOCK_MONOTONIC time (see sig_time_now()). A deadline of "now" means signals emitted by handlers are still queued. Returns SIG_ERR_STATE unless the service is started and driven. sig_service_set_wake() registers a callback, called with no lock held whenever a signal is enqueued or a job scheduled or canceled, so the loop can recompute its sleep. Set it before starting; it may be called from any thread. sig_service_stop() on a driven service delivers the signals still queued before returning, as the threaded service does.

sig_service_set_driven

sig_status_t sig_service_set_driven(sig_service * svc, bool driven)
ParameterType
svcsig_service *
drivenbool

shared with sig_service_wake_fn

Single-threaded operation. After sig_service_set_driven(svc, true), sig_service_start() starts the service exactly as usual but spawns no worker thread: the owner's event loop calls sig_service_step() instead. The mode is kept across stop/start; changing it on a started service is SIG_ERR_STATE. sig_service_step() delivers the signals queued so far, runs every job that is due, and reports when it next needs to be called: *out_has_deadline is false if nothing is scheduled (call again when woken), otherwise *out_deadline is an absolute CLOCK_MONOTONIC time (see sig_time_now()). A deadline of "now" means signals emitted by handlers are still queued. Returns SIG_ERR_STATE unless the service is started and driven. sig_service_set_wake() registers a callback, called with no lock held whenever a signal is enqueued or a job scheduled or canceled, so the loop can recompute its sleep. Set it before starting; it may be called from any thread. sig_service_stop() on a driven service delivers the signals still queued before returning, as the threaded service does.

sig_service_is_driven

bool sig_service_is_driven(const sig_service * svc)
ParameterType
svcconst sig_service *

shared with sig_service_wake_fn

Single-threaded operation. After sig_service_set_driven(svc, true), sig_service_start() starts the service exactly as usual but spawns no worker thread: the owner's event loop calls sig_service_step() instead. The mode is kept across stop/start; changing it on a started service is SIG_ERR_STATE. sig_service_step() delivers the signals queued so far, runs every job that is due, and reports when it next needs to be called: *out_has_deadline is false if nothing is scheduled (call again when woken), otherwise *out_deadline is an absolute CLOCK_MONOTONIC time (see sig_time_now()). A deadline of "now" means signals emitted by handlers are still queued. Returns SIG_ERR_STATE unless the service is started and driven. sig_service_set_wake() registers a callback, called with no lock held whenever a signal is enqueued or a job scheduled or canceled, so the loop can recompute its sleep. Set it before starting; it may be called from any thread. sig_service_stop() on a driven service delivers the signals still queued before returning, as the threaded service does.

sig_service_step

sig_status_t sig_service_step(sig_service * svc, bool * out_has_deadline, struct timespec * out_deadline)
ParameterType
svcsig_service *
out_has_deadlinebool *
out_deadlinestruct timespec *

shared with sig_service_wake_fn

Single-threaded operation. After sig_service_set_driven(svc, true), sig_service_start() starts the service exactly as usual but spawns no worker thread: the owner's event loop calls sig_service_step() instead. The mode is kept across stop/start; changing it on a started service is SIG_ERR_STATE. sig_service_step() delivers the signals queued so far, runs every job that is due, and reports when it next needs to be called: *out_has_deadline is false if nothing is scheduled (call again when woken), otherwise *out_deadline is an absolute CLOCK_MONOTONIC time (see sig_time_now()). A deadline of "now" means signals emitted by handlers are still queued. Returns SIG_ERR_STATE unless the service is started and driven. sig_service_set_wake() registers a callback, called with no lock held whenever a signal is enqueued or a job scheduled or canceled, so the loop can recompute its sleep. Set it before starting; it may be called from any thread. sig_service_stop() on a driven service delivers the signals still queued before returning, as the threaded service does.

sig_service_set_wake

void sig_service_set_wake(sig_service * svc, sig_service_wake_fn fn, void * ctx)
ParameterType
svcsig_service *
fnsig_service_wake_fn
ctxvoid *

shared with sig_service_wake_fn

Single-threaded operation. After sig_service_set_driven(svc, true), sig_service_start() starts the service exactly as usual but spawns no worker thread: the owner's event loop calls sig_service_step() instead. The mode is kept across stop/start; changing it on a started service is SIG_ERR_STATE. sig_service_step() delivers the signals queued so far, runs every job that is due, and reports when it next needs to be called: *out_has_deadline is false if nothing is scheduled (call again when woken), otherwise *out_deadline is an absolute CLOCK_MONOTONIC time (see sig_time_now()). A deadline of "now" means signals emitted by handlers are still queued. Returns SIG_ERR_STATE unless the service is started and driven. sig_service_set_wake() registers a callback, called with no lock held whenever a signal is enqueued or a job scheduled or canceled, so the loop can recompute its sleep. Set it before starting; it may be called from any thread. sig_service_stop() on a driven service delivers the signals still queued before returning, as the threaded service does.

sig_service_enqueue

sig_status_t sig_service_enqueue(sig_service * svc, const sig_terminal * terminal, sig_capsule_instance * instance, const sig_value * signal)
ParameterType
svcsig_service *
terminalconst sig_terminal *
instancesig_capsule_instance *
signalconst sig_value *

Copies *signal (sig_value_dup) into a heap-owned queue entry and pushes it onto svc's queue under lock, then signals svc's condition variable. Safe to call from any thread, including svc's own. The signal is delivered by calling terminal->handler(instance, &copy, terminal->name) on svc's own thread, sometime after this call returns -- delivery is asynchronous. A service accepts signals from creation until sig_service_stop() begins; from then until the next sig_service_start() it returns SIG_ERR_STATE and drops the signal, so an emitter running on another thread cannot leave a stale signal queued for the next start. A service that was never started still queues signals, which are delivered once it starts. A start that fails (a start hook fails, or the worker thread cannot be created) leaves the service not accepting and discards what its start hooks queued. sig_service_stop() still delivers what was queued before it began, but a handler that runs during that final drain and emits again gets SIG_ERR_STATE, so a chain of capsules inside the service is cut at the first hop. That is deliberate: nothing new is produced once shutdown begins.

sig_service_schedule_job

sig_status_t sig_service_schedule_job(sig_service * svc, sig_capsule_instance * instance, struct timespec interval, bool repeat, sig_job_fn callback, void * job_data, sig_job ** out_job)
ParameterType
svcsig_service *
instancesig_capsule_instance *
intervalstruct timespec
repeatbool
callbacksig_job_fn
job_datavoid *
out_jobsig_job **

Schedules callback(instance, job_data) to run on svc's own worker thread, first firing after interval, repeating every interval if repeat is true (one-shot otherwise). instance must already have been added to svc via sig_service_add_instance(). Safe to call before svc's thread has been spawned (e.g. from a capsule's start() hook, which sig_service_start() runs before spawning the thread -- see the threading-contract comment at the top of this file), or from svc's own worker thread once started (e.g. from inside a running job callback or terminal handler), or from any other thread at any time. interval must be >= 1ms: SIG_ERR_INVALID_ARG otherwise, as for a NULL svc/instance/callback. SIG_ERR_ALLOC on allocation failure. On success, *out_job (if out_job is non-NULL) is a handle valid for sig_service_cancel_job() for as long as svc itself exists -- a job's underlying node is never freed until sig_service_destroy(), so the handle is always safe to pass to cancel(), even long after the job fired (one-shot), was already canceled, or svc stopped. If a repeating job's callback runs longer than interval, missed fires are coalesced (skipped ahead to the next future deadline) rather than queued -- a slow callback never triggers a back-to-back catch-up burst, but it also means a job that is chronically slower than its own interval will silently fire less often than requested, not more. Every job scheduled with a non-NULL out_job (a handle) permanently retains its (small, fixed-size) node after it fires, is canceled, or the service stops, until svc is destroyed -- this is the deliberate cost of never handing back a dangling handle (see above). Do not request handles for an unbounded number of jobs on a long-lived service, nor for jobs scheduled on every start of a service that is stopped and started repeatedly: each one retains memory for the rest of that service's lifetime. Pass out_job = NULL when you never need to cancel the job: its node is then freed as soon as the job finishes, is canceled by a service stop, or the service stops.

sig_service_cancel_job

sig_status_t sig_service_cancel_job(sig_service * svc, sig_job * job)
ParameterType
svcsig_service *
jobsig_job *

Cancels job so it will not fire again. Safe to call from job's own callback (self-cancel for effectively-one-shot repeating jobs), any other thread, or after svc has already stopped -- see the handle- lifetime note on sig_service_schedule_job() above. A no-op, not an error, if job already fired (one-shot) or was already canceled.

sig_service_set_tap

void sig_service_set_tap(sig_service * svc, sig_tap_fn fn, void * ctx)
ParameterType
svcsig_service *
fnsig_tap_fn
ctxvoid *

Installs a tap (see sig_tap.h) on svc; a NULL fn clears it. Safe from any thread at any time: fn and ctx change together atomically. With no tap set the cost on the signal path is one atomic pointer load. A tap that is replaced may still be running on the service's thread when this returns. ctx must stay valid until the tap is cleared and the service has stopped or been destroyed.

sig_service_tap_emit

void sig_service_tap_emit(const sig_service * svc, const sig_capsule_instance * instance, sig_tap_direction dir, const char * terminal, const sig_value * signal)
ParameterType
svcconst sig_service *
instanceconst sig_capsule_instance *
dirsig_tap_direction
terminalconst char *
signalconst sig_value *

Calls svc's tap, if any. For sig_capsule.c (output boundary) and the service's own delivery loop (input boundary); not for capsule code.