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.
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.
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.
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.
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.
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)
Parameter
Type
svc
sig_service *
sig_service_stop
sig_status_t sig_service_stop(sig_service * svc)
Parameter
Type
svc
sig_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.
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.
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.
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.
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.
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.
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.
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.