valis / Reference / API reference

Active modules - API reference

Exported surface for the active subsystem. Part of the API reference.

Package valis/src/active/acme-listeners

Conditions

acme-responder-did-not-stop

Signalled when a challenge responder's accept thread is still running after the stop timeout. It replaces a wait with no end.

challenge-certificate-error

Signalled when the installed tls-alpn-01 challenge certificate cannot be made into a certificate chain to present. It is a fault in what was installed, never a transport event, so the accept loop reports it as a server-side failure rather than as a connection that merely ended.

Functions

call-with-http01-responder

(call-with-http01-responder listener-capability host body-fn &key inherited-socket)

Stand up a transient http-01 responder, call BODY-FN with (SOLVER SOCKET), a mercer :install/:remove solver plist and the live listening socket, and tear the responder down under unwind-protect on every exit path. The solver's :install registers a token's key-authorization; the responder serves it as plaintext at /.well-known/acme-challenge/<token>; :remove drops it.

The listening socket comes from one of two sources, and its provenance governs teardown:

  • INHERITED-SOCKET, when supplied, is an already-listening usocket socket derived from an inherited :80 descriptor fulcrum steers in. The responder drives accept on it WITHOUT its own bind-listen (LISTENER-CAPABILITY and HOST are unused on this path), and teardown does NOT close it: fulcrum owns the descriptor lifecycle and its listen backlog.
  • With no inherited socket, valis owns the bind through LISTENER-CAPABILITY (bound on HOST) and teardown closes the valis-bound socket.

call-with-tls-alpn01-responder

(call-with-tls-alpn01-responder listener-capability host body-fn)

Stand up a transient tls-alpn-01 responder on the valis-owned listener the LISTENER-CAPABILITY grants (bound on HOST), call BODY-FN with (SOLVER SOCKET), a mercer :install/:remove solver plist and the live listening socket, and tear the responder down under unwind-protect on every exit path. :install registers the domain's synthetic challenge cert + ephemeral key; each accepted connection presents it over acme-tls/1; :remove drops it. The ephemeral key is mercer's throwaway challenge key, never a leaf key.

Macros

with-http01-responder

(with-http01-responder (solver socket listener-capability &key (host "0.0.0.0") inherited-socket) &body body)

Stand up a transient http-01 responder, bind SOLVER to the mercer :install/:remove solver plist and SOCKET to the live listening socket, evaluate BODY (the order), and retract the responder on every exit path (unwind-protect). Pass (list :http-01 SOLVER) to obtain-certificate.

With no INHERITED-SOCKET valis owns the bind through LISTENER-CAPABILITY (bound on HOST) and closes it on teardown. INHERITED-SOCKET, when supplied, is an already-listening socket derived from an inherited :80 descriptor fulcrum steers in: the responder serves over it without its own bind-listen and leaves it open on teardown (fulcrum owns the descriptor).

with-tls-alpn01-responder

(with-tls-alpn01-responder (solver socket listener-capability &key (host "0.0.0.0")) &body body)

Stand up a transient tls-alpn-01 responder over the granted LISTENER-CAPABILITY, bind SOLVER to the mercer :install/:remove solver plist and SOCKET to the live valis-owned listening socket, evaluate BODY (the order), and retract the responder on every exit path (unwind-protect). Pass (list :tls-alpn-01 SOLVER) to obtain-certificate.

Variables

*acme-responder-stop-timeout-seconds*

How long a challenge responder's stop waits for its accept thread to return before it signals ACME-RESPONDER-DID-NOT-STOP. An idle responder returns within one poll interval, so reaching this means a connection it is serving has not finished.

+acme-challenge-path-prefix+

The RFC 8555 §8.3 http-01 request path prefix. The CA GETs this prefix followed by the challenge token; the responder answers with the token's key-authorization.

+acme-tls-alpn-protocol+

The RFC 8737 tls-alpn-01 ALPN protocol identifier the responder negotiates.

Package valis/src/active/acme-manager

Classes

acme-manager

The ACME manager's live handle: the alist of granted (designation . capability) it was handed at :start, and nothing else. Every seam it drives is looked up in this alist: the module holds no substrate global.

Conditions

certificate-without-identifiers

Signalled by a renewal driver when the held leaf claims no subjectAltName DNS entries. Ordering an empty identifier set would be a request for a certificate covering nothing, so the driver refuses instead of guessing a name.

missing-account-contact

Signalled by obtain-initial-certificate when the account-contact is NIL or empty, BEFORE any order is driven: the fail-closed refusal. The report names the missing contact by role only and never prints the contact value, so operator PII never reaches a log or a backtrace.

non-production-ca-refused

Signalled by a renewal driver before any identifier work and before any order, when the resident names a served public edge but the production opt-in is not set. It is the refusal that keeps a staging leaf from replacing a trusted one.

unpersistable-identifier-set

Signalled by obtain-initial-certificate when no identifier in an order names a host. Custody files a leaf under a name and a wildcard is a matching rule, so such an order could be placed and its certificate could not be kept. The refusal is raised BEFORE the order, which is what keeps the attempt from costing an issuance.

unsatisfiable-identifier

Signalled by select-challenge when NO available method can satisfy an identifier: the fail-closed refusal raised BEFORE any order is placed.

Generic functions

certificate-without-identifiers-domain

(certificate-without-identifiers-domain condition)

Undocumented: this exported symbol needs a docstring.

non-production-ca-refused-domain

(non-production-ca-refused-domain condition)

Undocumented: this exported symbol needs a docstring.

non-production-ca-refused-edge-domain

(non-production-ca-refused-edge-domain condition)

Undocumented: this exported symbol needs a docstring.

unpersistable-identifier-set-identifiers

(unpersistable-identifier-set-identifiers condition)

Undocumented: this exported symbol needs a docstring.

unsatisfiable-identifier-available

(unsatisfiable-identifier-available condition)

Undocumented: this exported symbol needs a docstring.

unsatisfiable-identifier-identifier

(unsatisfiable-identifier-identifier condition)

Undocumented: this exported symbol needs a docstring.

Functions

acme-manager-capabilities

(acme-manager-capabilities instance)

Undocumented: this exported symbol needs a docstring.

acme-manager-p

(acme-manager-p object)

Undocumented: this exported symbol needs a docstring.

adopt-configured-account-contact

(adopt-configured-account-contact)

Set CA-ACCOUNT-CONTACT from the site configuration and return what it now holds, NIL included.

This is the one place a boot establishes the contact, and it is the whole of what a renewal has: a renewal reads the special rather than being handed a contact, since there is nobody at the keyboard when a leaf falls due. I gave the step a name so that the boot and a proof of the renewal path reach the contact by the same route. A contact supplied any other way establishes only that the driver can order when something hands it one, which is not a question a deployed node ever asks.

A node that configured none holds NIL afterwards, and the order path refuses on NIL. That refusal must not soften here: a contact is the identity an ACME account is registered under, so one substituted at this point would register the node under an address the operator never chose.

attempt-renewal

(attempt-renewal cert-id renew-fn &key (now (get-universal-time)))

Run ONE renewal attempt for CERT-ID via RENEW-FN (the order thunk returning the next-renewal instant, and OPTIONALLY the freshly-issued material to hot-swap as a second value) and RETURN this task's own next-fire. On SUCCESS record a healthy outcome (consecutive-failures→0) FIRST, then best-effort hot-swap the live :443 cert in place when the renewal carried fresh material (the recorder runs before the swap, so a swap miss never demotes a genuinely successful renewal) and return the next ARI/heuristic fire. On FAILURE (never silently) write the durable renewal-health row (last-error, consecutive-failures++, next-retry), emit a log4cl ERROR carrying substrate facts only (the identifier and the error class, never key bytes or CA text as a format directive), and return an exponential-backoff next-fire so the scheduler reschedules. The task returning its own next-fire is what lets ARI and backoff both flow through the scheduler with no special-casing.

authoritative-for-identifier?

(authoritative-for-identifier? identifier authoritative-zones)

T when valis serves IDENTIFIER's zone: IDENTIFIER is at or under one of AUTHORITATIVE-ZONES (the canonical origins ndb holds). A leading wildcard label is ignored, since the wildcard and its apex share one zone.

call-with-dns01-published

(call-with-dns01-published zone-write-capability origin owner txt body-fn &key (await (lambda nil t)))

Publish the dns-01 challenge TXT member (OWNER, TYPE :txt, TTL, TXT) into the zone at ORIGIN through ZONE-WRITE-CAPABILITY, run BODY-FN, and RETRACT the member on EVERY exit path under the manager's OWN unwind-protect, so a forced error still leaves the zone byte-identical to its pre-publish state. AWAIT is polled after publish for self-NS visibility; if it reports the member missing, the member is published once more before BODY-FN runs. A publish is idempotent, so that costs nothing when the member was only slow to appear. Two things remove a live member: a retract of that same member, and deleting its zone, after which the publish is refused because the zone is no longer answerable. A full-zone re-import, whether by zone import or zone load, spares transient rows. Returns BODY-FN's value.

compute-backoff

(compute-backoff failure-count now)

The exponential-backoff next-fire: NOW plus base·2FAILURE-COUNT, capped. A growing FAILURE-COUNT yields a growing delay.

custody-leading-identifiers

(custody-leading-identifiers identifiers)

IDENTIFIERS ordered so a host name leads and a wildcard never does, and UNPERSISTABLE-IDENTIFIER-SET when not one of them names a host. Pass it any order you like and the set comes back the same set: only its order moves, so the certificate covers exactly the names you asked for.

Order carries weight here for one reason, and it is not the certificate. Custody files an issued leaf under the FIRST identifier its order carried, and a wildcard is a matching rule rather than a name, so an order led by one is persisted to a path the filesystem refuses. I put the guard on this side of the seam because a leaf covering an apex and its wildcard claims both in whatever order it was issued with, and a renewal re-orders the names the held leaf claims. Refusing before the order is what keeps the cost off you: an issuance the write into custody refuses afterwards is still spent against the same limit as one you keep.

custody-renewal-entries

(custody-renewal-entries &key store-path)

The certs held in the ACME custody store rooted at STORE-PATH, as the entry plists the renewal clock, the expiry belt and the STATUS line all read: :cert-id the domain, :certificate its held leaf chain PEM, :renew-fn its renewal driver. A NIL STORE-PATH takes mercer/boomer's own default store location.

The store is built the same way an initial order builds it, so the entries describe exactly the store the edge loads its certificate from.

This NEVER signals. It runs inside the module's :start thunk, where an escaping condition would take the boot down. But it does not fall back quietly either: a store that cannot be read emits an ERROR event naming the fault class before the empty list is returned, so an unreadable store is distinguishable at the log from a store that genuinely holds nothing. A quiet empty list here is how the renewal schedule stayed empty unnoticed.

Called on every health-loop iteration through the STATUS line, not only at boot, so it stays at one directory listing plus one file read per domain.

group-identifiers-by-topology

(group-identifiers-by-topology identifiers authoritative-zones available-methods)

Partition IDENTIFIERS by their selected challenge type into an alist (challenge-type . identifiers), so one order groups only identifiers that share a topology (mercer runs one challenge type per order). Fail-closed: an unsatisfiable identifier signals before any grouping is returned, so no order is ever attempted for it.

make-custody-renewal-driver

(make-custody-renewal-driver store domain)

Return the nullary thunk that renews DOMAIN's leaf inside STORE: the :renew-fn a custody entry carries to the scheduler, returning (values next-renewal-instant reload-plist) exactly as attempt-renewal expects.

The thunk re-orders the names the HELD certificate claims in its subjectAltName, never a name derived from the store's directory layout: a leaf covering both an apex and its wildcard must come back covering both, and a name guessed from the directory would silently narrow the certificate at its first renewal.

It obtains into the store it was HANDED, not into whatever store the edge configuration currently names. The two coincide on a deployed node and diverge under a scratch fixture, so enumerating one store while writing to another is a defect that would surface far from where it was introduced.

It refuses, before any identifier work and before any order, when this resident names a served public edge and the production opt-in is absent: boomer's store writes a renewed leaf over the same fullchain.pem the edge serves, so a renewal driven against a staging authority would replace a trusted production certificate with one no browser accepts. The refusal is loud by construction, since attempt-renewal records the durable failure row, emits an ERROR and backs off, so a misconfigured node is visible rather than quietly downgraded.

Every renewal asks for the in-process hot swap, under the name it renewed. The listener chooses a certificate per connection by the name the client asked for, so each renewed name has a credential of its own to land on. Asking for it only on behalf of one name would let a renewal persist a fresh certificate while the live listener went on presenting the expired one for that name until the next restart.

make-dns01-solver

(make-dns01-solver zone-write-capability authoritative-zones &key (await-fn (lambda (owner txt) (declare (ignore owner txt)) t)))

Build the mercer dns-01 challenge-solver plist (:publish/:retract) over ZONE-WRITE-CAPABILITY. mercer calls :publish with the full challenge name and the TXT value; the closure derives (origin owner) against AUTHORITATIVE-ZONES, publishes through the zone-write seam (carrying the live fence generation), and polls AWAIT-FN for self-NS visibility before returning so the CA sees the record. mercer wraps the matching :retract in its own order-loop unwind-protect.

normalize-ca-identifier

(normalize-ca-identifier identifier)

IDENTIFIER as the CA expects it: the DNS name with any trailing dot stripped. valis holds zone origins in canonical absolute (trailing-dot) form, but RFC 8555 order identifiers are relative dotless names. An ACME server (Pebble included) rejects an identifier that carries a trailing period. The dotless counterpart to %ensure-dot, to apply to every identifier threaded to an order so a zone origin never reaches the CA in absolute form.

obtain-initial-certificate

(obtain-initial-certificate identifiers &key account-contact profile store-path directory-url authoritative-zones await-fn)

Compose and DRIVE an INITIAL dns-01 order for IDENTIFIERS, the first-obtain call site the resident lacked (the manager could only reschedule renewals). Groups IDENTIFIERS by topology over valis's authoritative zones (dns-01 only, so a non-authoritative identifier is REFUSED fail-closed before any order), builds the zone-write and CA-transport capabilities under the manager's own designations, builds the dns-01 solver over the zone list with a bounded self-NS wire-visibility await, and drives the dns-01 group through first-obtain-driver.

ACCOUNT-CONTACT is mandatory and threaded to the order; a NIL or empty contact signals MISSING-ACCOUNT-CONTACT BEFORE the driver is called (mercer wraps the contact as "mailto:<contact>" unconditionally, so a NIL contact would register as "mailto:NIL" and be rejected). PROFILE defaults to ca-order-profile; see there for what an unset value actually sends. STORE-PATH defaults to resolve-edge-acme-store-path, so the issued cert lands in the SAME store load-edge-credential-cell reads at boot. A green order never boots :443 cert-gated-dark into a store it wrote elsewhere. DIRECTORY-URL is left to the config-gated default (staging unless the explicit production opt-in); the Pebble-only CA-TLS verification bypass is NEVER forwarded: the production path always verifies the CA's TLS certificate.

The group is ordered so a host name leads it and a wildcard never does, because custody files the issued leaf under the order's first identifier and a wildcard names no directory. An order carrying nothing but wildcards is refused outright with UNPERSISTABLE-IDENTIFIER-SET, before any issuance is spent on a certificate that could not be kept. Returns (values certificate-pem next-renewal-instant).

rederive-renewal-schedule

(rederive-renewal-schedule manager &key (now (get-universal-time)))

Re-derive the renewal schedule from mercer cert custody on boot: enumerate the certs in custody, compute each next-renewal, and post a renewal task at that instant. A next-fire in the past fires immediately (catch-up). Empty custody schedules nothing.

schedule-renewal

(schedule-renewal manager cert-id fire-at renew-fn)

Post CERT-ID's renewal task at FIRE-AT (absolute universal-time) on MANAGER's granted scheduler. A FIRE-AT in the past fires immediately (catch-up). A no-op when the scheduler was not granted.

select-challenge

(select-challenge identifier authoritative-zones available-methods)

Choose the challenge type for IDENTIFIER, fail-closed by topology: dns-01 when valis is authoritative for its zone AND the CA offers dns-01; otherwise a host-based method (tls-alpn-01, then http-01) the CA offers; otherwise SIGNAL unsatisfiable-identifier, the refusal raised before any order. A non-authoritative identifier never selects dns-01 (valis cannot publish into a zone it does not serve).

start-acme-manager

(start-acme-manager &key capabilities)

The :start thunk: capture the resolved CAPABILITIES alist into the module handle (no ambient globals), start the granted scheduler, re-derive the renewal schedule from mercer custody, and return the handle. Null-safe: with no scheduler granted (an unwired custody at dial-zero) it starts nothing and schedules nothing.

stop-acme-manager

(stop-acme-manager manager)

The :stop thunk: stop the granted scheduler behind MANAGER (idempotent). A no-op when MANAGER granted no scheduler.

wildcard-identifier-p

(wildcard-identifier-p identifier)

True when IDENTIFIER is an ACME wildcard identifier: the leading "*." label a certificate authority accepts in an order and a filesystem will not accept as a directory.

Variables

*authoritative-zones-fn*

The seam returning the authoritative zone origins. Default reads ndb; rebound in cold selection tests to an explicit list.

*ca-account-contact*

The operator ACME account contact threaded to an order, or NIL when unset. It MUST be a non-NIL string at order time: mercer wraps the contact as "mailto:<contact>" unconditionally, so a NIL contact becomes the literal "mailto:NIL" that an ACME server (Pebble included) rejects. A configurable seam: the operator sets it to their account email; the module never fabricates one.

*ca-order-profile*

The ACME profile an order asks for, or NIL to take mercer's own default. A configurable seam the operator sets per target authority.

⚠ NIL does NOT mean no profile is sent. I had it written down the other way here until I placed an order and watched it. NIL sends mercer's own default, "tlsserver", and that is a name only Let's Encrypt advertises, so every other authority refuses the order before it reaches a challenge. Set this to a name the target authority advertises: Pebble, the local test authority, advertises "default" and "shortlived". Asking for no profile at all is not something this seam can express today.

*cert-custody-enumerator*

The seam enumerating certs in mercer custody as a list of plists (:cert-id .. :certificate .. :renew-fn ..), read by the renewal clock, the expiry belt and the STATUS line alike. Rebound in tests.

The default reads the custody store the operator configured for the edge, and NOTHING when none is configured. It deliberately declines the library default store: reaching for it would make a production seam depend on whatever leftover certificate happens to sit in a developer's state directory, and a node would schedule real renewals for a name it has never served.

*first-obtain-driver*

The seam driving an initial order's CA transport. Default: forward to ca-transport-obtain (the granted CA-transport capability). Rebound in cold tests to a capturing stub so obtain-initial-certificate's compose (the topology-grouped identifiers, the solver plist, the threaded contact/store/profile) is proven without a live CA, mirroring the renewal path's injectable seams.

*next-renewal-fn*

The seam computing a cert's next-renewal instant. Default: mercer next-renewal-instant (ARI when offered, else the one-third-life heuristic).

*obtain-store-builder*

The seam building an initial order's ACME custody store from a store-path. Default: %obtain-store, which primes the ffi-free ACME client (ensure-ffi-free-acme) and constructs the boomer cert-store. Rebound in cold tests to a stub returning a dummy store, so the flag-to-driver compose is proven without booting the ffi-free ACME client: that boot does a register-immutable-system "cl+ssl" whose load order is fragile once the real cl+ssl is already present in the image.

*renewal-failure-counter*

The seam reading a cert's consecutive-failure count for the backoff. Default: the durable row. Rebound in cold tests.

*renewal-recorder*

The seam recording a renewal outcome. Default: the durable renewal-health CAS. Rebound in cold tests to a capturing stub.

*tls-credential-reloader*

The seam that hot-swaps the live :443 listener's certificate after a successful renewal. Called with the renewed DOMAIN and its freshly-issued CHAIN and KEY; the default resolves the running public-HTTPS edge and swaps the credential that name is served under, so the next handshake for it serves the new cert with the listening socket never dropped and every other name left alone. Late-resolved so the manager takes no load-time dependency on the edge module; rebound in cold tests to a capturing stub, so the success/failure branch is provable with no live listener and no CA.

+acme-manager-module-name+

The active-module registry name of the ACME manager.

Package valis/src/active/capability

Classes

active-capability

Base type for a seam-constructed capability object handed to an active module. Not directly instantiable (:constructor nil). Every subtype is a closed value: it carries only the authority it names and holds no ambient reference to a substrate global.

REVOCATION is the grant the capability was built under for one start of a module's active part, or NIL for one the node built for itself. Once that grant is revoked, every behaviour of the capability refuses with capability-refused and the reason :revoked, whichever thread still holds it, so a module that has been retired stops acting through what it was handed.

ca-transport-capability

The CA-transport egress capability. Carries a single OBTAIN closure wrapping mercer's obtain-certificate, defaulting the directory URL through the config production gate (resolve-acme-directory-url) so valis reaches the CA only via the grant and can never place a production order without the explicit opt-in. Holds no substrate global: the module drives it only through CA-TRANSPORT-OBTAIN.

dial-capability

The outbound-dial egress capability (the /net/dial and /net/dial/<scope> designations). Carries its DESIGNATION, the SCOPE read out of it by the same seam that decides on it (:EVERY-DESTINATION for the broad grant), and a single DIAL closure holding the grant that seam evaluates against. It holds only that closure, no substrate global and no socket and no address, so a module reaches the network through DIAL-THROUGH and nothing else.

A broad grant is by construction a general egress channel, correct for a person browsing and fully exfiltrating in the hands of a compromised process. Grant width should track what a module is doing now rather than everything it might one day need.

GRANT is the grant the seam evaluates, carrying who chose its destination and the client it was issued to.

echo-capability

The trivial dial-zero granted capability: its sole behavior is to echo its argument (see ECHO-THROUGH). It captures no substrate global (DESIGNATION is a trust-neutral string, not a seam handle), so a module holding it can reach nothing beyond the echo.

listener-bind-capability

A transient public-listener bind grant (the /edge/listen/<port> designation). It carries its DESIGNATION, parsed PORT, and a BINDER closure of (host backlog) that performs the valis-owned usocket:socket-listen on PORT and returns the live listening socket, the capability's real body (see BIND-LISTENER). valis owns the bind; the responder handed the socket drives accept and never binds ambiently at all. BOUND stays nil at construction: holding the capability is the authority to bind, not a live socket: a socket exists only for the span BIND-LISTENER returns one and the responder closes it under unwind-protect.

scheduler-capability

The scheduler egress capability. Carries a fresh, UNSTARTED scheduler instance; the module owns its lifecycle through SCHEDULER-CAP-START / SCHEDULER-CAP-STOP and posts renewal tasks through SCHEDULER-CAP-POST. Construction spins no thread (a closed value with no ambient reference), so the scheduler is reached only as a granted capability, never a process-global.

zone-write-capability

The zone-write egress capability. Carries a PUBLISH and a RETRACT closure that wrap publish-/retract-transient-rrset-member, each supplying the LIVE fence generation at call time and running inside a pooled operator-state connection. It holds only these closures (no substrate global), so a module reaches the zone only through the two behaviors ZONE-WRITE-PUBLISH / ZONE-WRITE-RETRACT.

Conditions

capability-refused

Signalled when a capability is requested for a designation the grant-set does not name (:not-granted) or that the seam has no constructor for (:no-constructor). The refusal is by construction: a non-granted designation's constructor is never invoked.

Also signalled, with the reason :revoked, by any behaviour of a capability whose grant has been revoked, as every grant of a module is when the module is retired.

Generic functions

capability-refused-designation

(capability-refused-designation condition)

Undocumented: this exported symbol needs a docstring.

capability-refused-reason

(capability-refused-reason condition)

Undocumented: this exported symbol needs a docstring.

Functions

active-capability-p

(active-capability-p object)

Undocumented: this exported symbol needs a docstring.

active-capability-revocation

(active-capability-revocation instance)

Undocumented: this exported symbol needs a docstring.

bind-listener

(bind-listener capability &key (host "0.0.0.0") (backlog 8))

Open the transient public listener CAPABILITY grants and return the live usocket listening socket. Performs the valis-owned usocket:socket-listen on the capability's granted port through its binder: valis owns the bind, so the responder that drives accept on the returned socket never calls socket-listen itself. HOST defaults to 0.0.0.0 so the CA can reach the responder during a real order (a test passes 127.0.0.1, or port 0 for an OS-assigned free port). The caller MUST close the returned socket on teardown, under unwind-protect. The transient bind lives only for the order's duration. A socket bound for a module is recorded against its grant, so retiring the module shuts down one it left open; once the grant is revoked, this refuses with capability-refused.

ca-transport-capability-designation

(ca-transport-capability-designation instance)

Undocumented: this exported symbol needs a docstring.

ca-transport-capability-p

(ca-transport-capability-p object)

Undocumented: this exported symbol needs a docstring.

ca-transport-obtain

(ca-transport-obtain capability identifiers challenge-solvers &rest keys)

Drive one certificate order for IDENTIFIERS with CHALLENGE-SOLVERS through CAPABILITY, the CA-transport authority. KEYS forwards obtain-certificate options (:account-contact :store :profile :skip-tls-verify, and :directory-url to override the config-gated default). Returns (values certificate-pem next-renewal-instant).

construct-capability

(construct-capability designation grant-set &key revocation)

Construct the capability object for DESIGNATION, but ONLY if DESIGNATION is a member of GRANT-SET. A designation GRANT-SET does not name is refused fail-closed: signals CAPABILITY-REFUSED with reason :not-granted and NEVER looks up or invokes a constructor for it: the refusal is by construction, not a post-hoc check. Signals CAPABILITY-REFUSED with reason :no-constructor when DESIGNATION is granted but the seam has no constructor for it. Otherwise returns the constructed object, built under REVOCATION when one is given, so that it refuses once that grant is revoked.

dial-capability-designation

(dial-capability-designation instance)

Undocumented: this exported symbol needs a docstring.

dial-capability-grant

(dial-capability-grant instance)

Undocumented: this exported symbol needs a docstring.

dial-capability-p

(dial-capability-p object)

Undocumented: this exported symbol needs a docstring.

dial-capability-scope

(dial-capability-scope instance)

Undocumented: this exported symbol needs a docstring.

dial-scope

(dial-scope designation)

What DESIGNATION reaches: :EVERY-DESTINATION, the one destination its trailing segment names, or NIL when it is not a designation this file recognises.

The trailing segment carries the reach in the shape the listener-bind designation already established for a parameter carried in a designation, so an operator reading a grant-set sees what a dial grant reaches in the designation itself rather than having to look it up somewhere else.

An unrecognised designation answers NIL rather than being treated as a harmless unknown, because a designation nobody wrote a meaning for is exactly the shape a mistake takes.

This is the one place the rule is written. A second reading of the same string somewhere else would be a second rule, and the two would drift apart quietly on a string that decides where a principal may go.

dial-through

(dial-through capability destination port &rest keys)

Open a connection to DESTINATION at PORT through CAPABILITY, the outbound authority. KEYS forwards the verb's options (:type :family :source :deadline). Returns (values file-descriptor peer family); the caller owns the descriptor and its close from the moment this returns. Once the grant CAPABILITY was built under has been revoked, refuses with capability-refused before anything is looked up.

echo-capability-designation

(echo-capability-designation instance)

Undocumented: this exported symbol needs a docstring.

echo-capability-p

(echo-capability-p object)

Undocumented: this exported symbol needs a docstring.

echo-through

(echo-through capability value)

The echo capability's sole behavior: return VALUE unchanged. CAPABILITY holds no substrate handle, so this reaches nothing beyond its argument. Refuses once the grant it was built under has been revoked.

granted-designations

(granted-designations vouch-token)

The list of designations VOUCH-TOKEN grants, read off the SAME verified token, no second trust check. Mirrors the module-admission discipline of reading grant directives from the token the verifier already validated; revocation and fence enforcement stay inside the verifier, never duplicated here.

listen-bind-designation

(listen-bind-designation port)

Build the reserved listener-bind designation edge/listen/<PORT>. The convention is collision-safe: build-base-view never materializes /edge/listen into a mountable subtree, so the seam interprets the string as a bind grant rather than a namespace right.

listener-bind-capability-bound

(listener-bind-capability-bound instance)

Undocumented: this exported symbol needs a docstring.

listener-bind-capability-designation

(listener-bind-capability-designation instance)

Undocumented: this exported symbol needs a docstring.

listener-bind-capability-p

(listener-bind-capability-p object)

Undocumented: this exported symbol needs a docstring.

listener-bind-capability-port

(listener-bind-capability-port instance)

Undocumented: this exported symbol needs a docstring.

make-ca-transport-capability

(make-ca-transport-capability designation &key dial-capability revocation)

Construct the CA-transport capability for DESIGNATION. Its obtain closure calls mercer's obtain-certificate (reached by symbol-call so no drakma is pulled until an order actually runs), defaulting DIRECTORY-URL to the config-gated resolve-acme-directory-url (production is unreachable without the acme.production opt-in), and forwarding only the caller-supplied keys so mercer's own defaults stand.

Every request the order makes goes out through the outbound seam, under DIAL-CAPABILITY when one was handed in and otherwise under the authority this node's own configuration names. The binding lasts exactly as long as the order: scoped, so the authority is provably on the path that was granted it, and so the tests and the proof script that legitimately stub the request seam are not captured by an order running somewhere else in the image.

Built under REVOCATION, an order refuses to begin once that grant is revoked, and the authority it travels under when it was handed none is built under the same grant, so an order already running makes no further request.

make-dial-capability

(make-dial-capability designation &key grant-hash revocation)

Construct the outbound-dial capability for DESIGNATION, as a grant whose destination somebody other than the owner chose.

This is what the capability-constructor table builds for a vouched module: the host in a narrowed designation is one the publisher picked, not the owner, so the grant never claims otherwise.

Built for a module the owner admitted, the grant is issued to that module, named by its ASDF system name as MODULE-DIAL-CLIENT gives it, so an allowance the owner writes for that name reaches its dials and no other module's. The allowance follows the system name, whichever vouched module carries it: a replacement, or another publisher's module admitted under the same name, inherits it. A module whose name cannot be written in an allowance, one named like a client of the node's own (acme, delegation-probe, dist, secondary-forward, zone-notify), and a capability built for anything other than an admitted module, name no client, and no allowance reaches them.

GRANT-HASH is what the owner names when withdrawing this grant. Supplying it is what lets a withdrawal reach decisions already taken for the grant; a capability built without one is evaluated afresh on every dial, which is slower and never less safe, and no withdrawal can reach it.

REVOCATION is the grant a module's part was handed when it started. Built under it, the capability refuses once that grant is revoked, as retiring the module does, and every descriptor it hands out is recorded against the grant, so retirement shuts down a connection the module left open. A part's dial is built with no GRANT-HASH: the owner has no listing that names it, so its decisions are never held and a change to the operator's narrowing reaches it on the next dial. Retiring the module is what takes its dial back.

make-echo-capability

(make-echo-capability designation &key revocation)

Construct the trivial echo capability for DESIGNATION, a closed object whose only behavior is ECHO-THROUGH and whose only state is the trust-neutral designation string and REVOCATION, the grant it is built under.

make-listener-bind-capability

(make-listener-bind-capability designation &key revocation)

Construct the listener-bind capability for DESIGNATION (a /edge/listen/<port> string) with its REAL body: a valis-owned socket-listen binder over the granted port. The returned capability carries its designation, parsed port, and a binder closure that performs usocket:socket-listen when the responder calls BIND-LISTENER: valis owns the bind, so the responder drives accept on the handed socket and never calls socket-listen itself, and no bind happens ambiently.

Built under REVOCATION, the binder refuses once that grant is revoked, and every socket it hands out is recorded against the grant, so retiring the module shuts down a listener it left open.

make-owner-dial-capability

(make-owner-dial-capability designation client &key ports grant-hash revocation)

Construct the outbound-dial capability for DESIGNATION, marked as reaching a destination the owner chose, issued to CLIENT. PORTS lists the ports it is issued for, NIL for any; a dial to any other port is refused before its destination is looked up.

For node code that reads the destination from the owner's own configuration. A destination that arrived from anywhere else is not the owner's choice, whoever holds the grant, and is built with MAKE-THIRD-PARTY-DIAL-CAPABILITY instead. Every caller under src/ is listed in a test, so a new one arrives as a change somebody reviews.

GRANT-HASH and REVOCATION mean what they mean for MAKE-DIAL-CAPABILITY.

make-scheduler-capability

(make-scheduler-capability designation &key revocation)

Construct the scheduler capability for DESIGNATION over a fresh, unstarted scheduler. No thread runs until the module calls SCHEDULER-CAP-START. Built under REVOCATION, starting it and posting to it refuse once that grant is revoked; stopping it never does, since a retired module's :stop must still be able to let its clock go.

make-third-party-dial-capability

(make-third-party-dial-capability designation client &key ports grant-hash revocation)

Construct the outbound-dial capability for DESIGNATION, marked as reaching a destination somebody other than the owner chose, issued to CLIENT.

For node code acting on a destination it was handed rather than configured: a name looked up in somebody else's zone, a peer's advertised address. CLIENT is the name of the holder the grant is issued to, so a decision can be made per client. PORTS lists the ports it is issued for, NIL for any; a dial to any other port is refused before its destination is looked up.

GRANT-HASH and REVOCATION mean what they mean for MAKE-DIAL-CAPABILITY.

make-zone-write-capability

(make-zone-write-capability designation &key revocation)

Construct the zone-write capability for DESIGNATION. Its publish/retract closures wrap publish-/retract-transient-rrset-member (reached by symbol-call), each folding in the live store generation (%live-store-generation, Pitfall 6) and running inside a pooled operator-state connection (%call-in-operator-state). The caller supplies (origin owner type ttl rdata), never the generation, which the capability always reads current. Built under REVOCATION, both refuse once that grant is revoked, before any connection is taken.

mint-active-grant

(mint-active-grant designations)

Mint an owner-rooted capability token carrying one mount-directive per string in DESIGNATIONS (the module's egress grant-set), signed by the live owner custody (factotum-custody). Mirrors the module-admission vouch mint but carries egress designations rather than a single :admit directive: the SEAM interprets each designation (a listener-bind, an internal seam), so the rights are a uniform :mount placeholder over the closed set. Draws a fresh OS-entropy nonce per mint, signs the draft's canonical bytes, and stamps the canonical hash as SHA-256 of (canonical-bytes || signature), the same minter contract verify-token-chain reconstructs. Returns nil fail-closed when no custody is wired: no grant is honored without a live owner.

register-capability-constructor

(register-capability-constructor designation constructor)

Install CONSTRUCTOR (a function of the designation string returning a capability object) under DESIGNATION. Idempotent: re-registering the same designation replaces the constructor. Returns CONSTRUCTOR.

When a module's part starts, CONSTRUCTOR is also passed the keyword :revocation, the grant it is building for, and must check that grant at the head of every behaviour of what it builds, so the capability refuses once the module is retired. That grant also names the module it was handed to, through MODULE-GRANT-SYSTEM, for a constructor whose capability is issued per module. Built for the node itself, it is passed the designation alone.

resolve-granted-capabilities

(resolve-granted-capabilities grant-set &key revocation)

Resolve GRANT-SET (a list of granted designation strings, e.g. from GRANTED-DESIGNATIONS on the minted vouch) into an alist of (designation . capability-object). Constructs ONLY designations that are BOTH in GRANT-SET AND have a seam constructor; a granted designation with no constructor is omitted. A designation NOT in GRANT-SET is never iterated and never constructed. The module receives exactly this alist and can reach no substrate seam absent from it. This is the fail-closed-by-construction guarantee: egress is bounded by which objects were built, not by a runtime allowlist a caller could bypass.

REVOCATION is the grant one start of a module's part is handed. Every capability is built under it and recorded against it, so revoking it makes all of them refuse.

scheduler-cap-ask-to-stop

(scheduler-cap-ask-to-stop capability)

Ask the scheduler behind CAPABILITY to stop without waiting for it. Never refused: asking a clock to stop is no authority, and a retired module's :stop may need it.

scheduler-cap-live-p

(scheduler-cap-live-p capability)

True while the scheduler behind CAPABILITY runs and has not been asked to stop. A clock whose stop gave up waiting for a task is no longer live, though its thread may still be running that task.

scheduler-cap-post

(scheduler-cap-post capability fire-at thunk)

Post THUNK to run at FIRE-AT (an absolute universal-time) on the scheduler behind CAPABILITY. THUNK returns its own next-fire time (NIL one-shot, or a universal-time to reschedule, an ARI instant or a backoff delay). Returns the scheduled task.

scheduler-cap-running-p

(scheduler-cap-running-p capability)

True while a thread the scheduler behind CAPABILITY started is still running, whether or not it has been asked to stop. Asking is no authority, so it is never refused.

scheduler-cap-start

(scheduler-cap-start capability)

Spin the owned scheduler thread behind CAPABILITY (idempotent). Returns the scheduler. Refuses once the grant it was built under has been revoked, including a revocation that lands while the clock is being started: the clock is then stopped again, waiting no longer than revoked-clock-stop-seconds, and the start refuses.

scheduler-cap-stop

(scheduler-cap-stop capability)

Stop and join the owned scheduler thread behind CAPABILITY (idempotent). Never refused: letting a clock go is no authority, and a retired module's :stop needs it.

scheduler-capability-designation

(scheduler-capability-designation instance)

Undocumented: this exported symbol needs a docstring.

scheduler-capability-p

(scheduler-capability-p object)

Undocumented: this exported symbol needs a docstring.

zone-write-capability-designation

(zone-write-capability-designation instance)

Undocumented: this exported symbol needs a docstring.

zone-write-capability-p

(zone-write-capability-p object)

Undocumented: this exported symbol needs a docstring.

zone-write-publish

(zone-write-publish capability origin owner type ttl rdata)

Publish the transient rrset member (OWNER TYPE TTL RDATA) into the zone at ORIGIN through CAPABILITY, the zone-write authority carrying the live fence generation. TYPE must be :txt; any other type signals transient-type-refused and leaves the zone untouched. OWNER's first label must be _acme-challenge; any other owner signals transient-owner-refused and leaves the zone untouched. Returns (values resolved-serial changed-p).

zone-write-retract

(zone-write-retract capability origin owner type rdata)

Retract the transient rrset member (OWNER TYPE RDATA) from the zone at ORIGIN through CAPABILITY, the zone-write authority carrying the live fence generation. Returns (values resolved-serial changed-p).

Variables

*capability-constructors*

Designation string -> constructor function of the designation (and, for a module's part, the keyword :revocation; see REGISTER-CAPABILITY-CONSTRUCTOR) returning a closed capability object. The seam alone owns this table: a designation absent from it (and not matching a reserved prefix) has no constructor and can never be built, so it can never be handed to a module.

*revoked-clock-stop-seconds*

How long stopping a clock waits for a task it is running when the clock was started just as its grant was revoked. Short, so a start that races a retirement never adds a long wait of its own; a task that outlasts it finishes on its own, and the clock then ends.

+ca-transport-designation+

The reserved designation of the CA-transport egress capability: the authority to drive one mercer obtain-certificate order, so valis reaches the CA only through the grant (and only the config-gated directory URL).

+dial-designation+

The designation of a grant that reaches every destination.

+dial-prefix+

The prefix of a grant whose reach is narrowed by its trailing segment, in the shape the listener-bind designation already established for a parameter carried in a designation.

+scheduler-designation+

The reserved designation of the scheduler egress capability: the authority to post/start/stop the time-based event loop that drives the renewal clock, so the scheduler is reached only as a granted capability, never a process-global.

+zone-write-designation+

The reserved designation of the zone-write egress capability: the authority to publish/retract a transient TXT member (the dns-01 challenge) in the live authoritative zone, carrying the live fence generation. It publishes TXT records only, and only at _acme-challenge names, so a module holding it cannot change the zone's NS, SOA or address records, nor its SPF, DMARC or DKIM records.

Package valis/src/active/cert-watch

Functions

check-cert-expiry

(check-cert-expiry &key (now (get-universal-time)))

The independent cert-expiry belt: enumerate the certs in mercer custody and, for each whose renewal instant has already passed (the ACME manager should have renewed it but the leaf is still in custody) AND whose last renewal attempt is older than one backoff-cap interval (so a cert the manager is actively retrying does not false-alarm), emit an ERROR cert.expiry-imminent event carrying substrate facts only: the cert-id and its whole days-to-expiry, never key bytes. Returns the list of flagged cert-ids.

Kept strictly OFF the watchdog path: this never pings the watchdog and never feeds serving-path-live-p. A silently un-renewed leaf is an operator-visible ERROR to surface, not a service-restart condition.

Variables

*cert-days-to-expiry-fn*

Seam returning a cert's whole days-to-expiry. Default: mercer's PEM validity parse. Rebound in tests.

*cert-last-attempt-fn*

Seam returning a cert's last renewal-attempt universal-time, or NIL when none is recorded. Default: the durable renewal-health row. Rebound in tests.

Package valis/src/active/controller

Conditions

active-grant-unminted

The owner's grant for an active module could not be minted, because the node's entropy source or the owner's custody failed. Every active module's grant is minted the same way, so this is a fault of the node and not of the module named: start-actives offers no restart that leaves the module out, and the active modules already started are stopped.

active-module-start-failed

One active module could not be started: the capabilities its grant names could not be resolved, or its :start signalled. Any active module the node did not mark as its own is a module's, including one whose owner nobody can name, so this is that module's fault alone, and start-actives signals it with the leave-active-module-out restart in effect. OWNER names the owner it was registered under, so a caller leaving it out can retire everything registered under that owner rather than leave the module's other parts serving; it is NIL when no owner can be named, and then only the active part itself can be retired.

node-active-start-failed

An active service the node registers for itself, marked with the owner :node, such as the certificate manager, could not be started. It is part of the node rather than a module the owner loaded, and a node running without it would report itself ready while doing less than it should, such as no longer renewing its certificates. So start-actives offers no restart that leaves it out, and the active modules already started are stopped.

Generic functions

active-grant-unminted-cause

(active-grant-unminted-cause condition)

Undocumented: this exported symbol needs a docstring.

active-grant-unminted-name

(active-grant-unminted-name condition)

Undocumented: this exported symbol needs a docstring.

active-module-start-failed-cause

(active-module-start-failed-cause condition)

Undocumented: this exported symbol needs a docstring.

active-module-start-failed-name

(active-module-start-failed-name condition)

Undocumented: this exported symbol needs a docstring.

active-module-start-failed-owner

(active-module-start-failed-owner condition)

Undocumented: this exported symbol needs a docstring.

active-module-start-failed-threads

(active-module-start-failed-threads condition)

Undocumented: this exported symbol needs a docstring.

node-active-start-failed-cause

(node-active-start-failed-cause condition)

Undocumented: this exported symbol needs a docstring.

node-active-start-failed-name

(node-active-start-failed-name condition)

Undocumented: this exported symbol needs a docstring.

Functions

call-with-boot-retirement-budget

(call-with-boot-retirement-budget thunk)

Call THUNK, which stands the node's modules up at boot, and return its values, with every wait on a module's threads or clocks inside it, each module's own budget included, ending no later than boot-retirement-budget-seconds from now, or sooner when a ceiling already in force says so.

call-with-clock-stop-budget

(call-with-clock-stop-budget thunk)

Call THUNK, which retires one module, with one clock-stop-budget-seconds budget, never running past clock-stop-ceiling, for every wait on the clocks of every part it retires, its parts' own :stop included, and return its values. Inside a retirement that already holds a budget, THUNK shares that one.

descriptors-left-open

(descriptors-left-open owner)

How many descriptors the parts registered under OWNER were handed that are still open. Once its module is retired, these are the ones it left shut down and open for a restart to reclaim.

leave-active-module-out

(leave-active-module-out condition)

Go on starting the rest without the one active module CONDITION, an active-module-start-failed, names. Use it from a handler for active-module-start-failed. The module stays registered, so a later start-actives tries it again; a caller that means it never to run retires its owning module.

retire-active-module

(retire-active-module name &key spec)

Stop and forget the live active entry whose spec name matches NAME (normalized via STRING), and remove NAME from active-modules so a later start-actives will not start a now-deleted module - the per-module drain the delete-hook calls, mirroring retire-edge-adapter (drain the live handle, then unregister).

When SPEC is given, only that spec is retired: the live entry stopped is the one started from SPEC, and NAME leaves the registry only while SPEC is what it holds. A part another owner has registered under the same name since is left running and registered, so retiring one module never takes another's part down.

Before its :stop is called, every grant the part's starts were handed is revoked, so each capability built for it refuses from then on, whichever thread holds it. Once it has been asked to stop, every descriptor handed out through those capabilities is shut down and left open for a restart to reclaim, and every scheduler behind them is stopped.

Returns T if a live entry was stopped, NIL if none was live. Signals RETIREMENT-INCOMPLETE when the module's :stop did not return: the handle is still running, so the entry STAYS in active-entries and a second retire attempts it again. The unregister is attempted either way, so a module that could not be stopped is at least not started again by a later start-actives.

retirements-left-running

(retirements-left-running)

Each module retired while the node runs that left threads running, as (subject . description), oldest first.

revoke-module-grants

(revoke-module-grants owner)

Begin retiring the module OWNER names by taking back what its active parts were handed: nothing more is granted to it, and every capability any start of its parts was handed refuses from now on, whichever thread still holds it. I made this the first step of retiring a module, ahead of asking it to stop, so a thread it leaves running is harmless before anything else is attempted. Returns no values.

settle-module-grants

(settle-module-grants owner &optional threads)

Finish retiring the module OWNER names once its parts have been asked to stop. Its threads are asked to end their work, so every cancel-aware wait in them signals module-cancelled; every descriptor its parts were handed is shut down, waking a thread blocked on one, and left open for a restart to reclaim; every scheduler behind them is stopped; and then every thread known to be the module's, with each of THREADS, is waited for, within the same budget as the rest of its retirement. Nothing is stopped from outside: a thread still running when the budget runs out is left running, and the caller names it. Returns three values: :retired, or :retired-with-survivors when something it ran is still running or it left descriptors open; how many descriptors it left shut down and open; and the threads still running. Its custodian stays retired, so it is granted nothing more until a restart.

start-actives

(start-actives &key (modules (active-modules)))

Start every registered active module (default (active-modules), deterministic STRING<-sorted order) AFTER the edge is up. For EACH spec: mint its grant-set, read the granted designations back off the verified token, resolve them to capability objects, and hand ONLY that capability alist to :start; record the (SPEC . HANDLE) in active-entries. Returns the started (SPEC . HANDLE) entries in start order, which are exactly those still live.

A spec in MODULES that is no longer the one registered under its name when its turn comes, because a module was taken out of service meanwhile, is not started.

An active module whose start fails, unless the node registered it as its own, signals active-module-start-failed naming it, with the leave-active-module-out restart in effect. A caller that invokes it runs without that module in this start, and the rest are still started; any active part with the same owner that already started is stopped and retired, so no part of a module that will not run is left live. The failed module stays registered unless the caller retires it, which is the caller's to decide. A module whose start failed returned no handle, so there is nothing of it here to stop, and anything its :start set going before it failed is not undone by this.

A grant that cannot be minted, or an active service the node registered for itself with the owner :node that does not start, is a fault of the node: active-grant-unminted or node-active-start-failed is signalled with no restart to leave it out. A caller that lets any of these conditions pass gets the fail-closed answer: the modules already started are stopped, in reverse start order, and the condition propagates, so no half-started fleet and no live entry is left behind.

stop-actives

(stop-actives)

Tear every started active module down in reverse start order (active-entries carries the most-recently-started module at the head, so a head-to-tail walk IS reverse start order), calling each spec's :stop thunk on its handle, each step error-isolated so one failure cannot strand the rest, then clear active-entries. Idempotent: a no-op on an empty registry. This is the FIRST teardown call on every boot unwind, stopping actives before the edge.

Each part is taken down in the same order as a retirement: what its starts were handed is revoked before its :stop is called, and the descriptors handed out through it are shut down and its schedulers stopped after, within one clock budget for each module, :stop included. No module's budget runs past actives-stop-budget-seconds from the start of this call, so the teardown that follows still has time before a supervisor gives up on the process. A part stopped this way is started again with a fresh grant.

Variables

*active-entries*

The supervisor's single-owner ordered record of started active modules, a list of (SPEC . HANDLE) with the most-recently-started module at the head, so a head-to-tail walk is reverse start order. start-actives pushes each module it successfully starts; stop-actives tears the whole list down in reverse and clears it; retire-active-module removes one entry by name. Empty when no active module is running.

*actives-stop-budget-seconds*

The longest, in wall-clock seconds from the moment the node begins taking its active parts down, whether it is going down or a start failed partway, that it waits for their clocks to stop, all modules together. I chose 45 because the supervisor's unit gives the whole process 90 seconds to stop before it is killed: half of that covers two modules each spending their full budget, and the other half is left for the edge, the fabric, the listener and the host's own cleanup, which run after the active parts and must not be cut off.

*boot-retirement-budget-seconds*

The longest, in wall-clock seconds from the moment the node begins standing its modules up at boot, that the boot waits on the threads and clocks of every module it leaves out, all of them together. The node reports itself ready only once its active parts have started, and its supervisor's unit gives a start 90 seconds before it kills the process and starts it again; I chose 30 so the waits for any number of broken modules take at most a third of that, leaving the rest for the store, the fabric, re-admitting and compiling the recorded modules, and the edge. A boot that ran past 90 seconds would be killed and restarted into the same failure forever, which would let one broken module keep the node down.

*clock-stop-budget-seconds*

The longest, in wall-clock seconds from the moment a module's retirement begins, that the retirement waits for the module's clocks to stop. It is a ceiling on the whole retirement's waiting for clocks, not a wait added on top: its parts' own :stop, stopping their clocks, waits within it, every part of the module shares it, and so does the settling that finishes the retirement. When the whole node goes down, each module has one of its own, and none may run past actives-stop-budget-seconds.

*retirements-left-running*

Each module retired while the node runs that left threads running, as (subject . description), oldest first: SUBJECT names the module as a reader would, and DESCRIPTION names its threads and how the owner gets the restart that clears them. Kept until the process ends, since the threads are.

Package valis/src/active/scheduler

Classes

scheduler

A single-owner time-based event loop. One owned thread sleeps until the nearest task deadline, runs every due task, and re-enqueues each by the next-fire time its thunk returns. LOCK guards QUEUE and the lifecycle flags; WAKEUP wakes the sleeping thread when a nearer task is posted or a stop is requested (no busy-wait). QUEUE is time-ordered ascending by (FIRE-AT, SEQ). Reached only as a granted capability, never a process-global.

Conditions

scheduler-did-not-stop

Signalled by STOP-SCHEDULER when the owned thread is still running after the stop timeout. It replaces a wait with no end.

scheduler-thread-still-running

Signalled by START-SCHEDULER while the thread a timed-out stop left behind is still running, so a second loop never runs beside it.

Functions

ask-scheduler-to-stop

(ask-scheduler-to-stop scheduler)

Ask SCHEDULER's owned thread to stop and return at once, without waiting for it. An idle thread ends straight away; one running a task ends once the task returns. A later stop-scheduler joins it. Returns no values.

clock-stop-wait-seconds

(clock-stop-wait-seconds)

How long a stop may wait now: scheduler-stop-timeout-seconds, cut to what is left before clock-stop-deadline when one is in force, or NIL when what is left is below clock-stop-floor-seconds.

make-scheduler

(make-scheduler)

Construct an inert scheduler. No thread exists and no task fires until START-SCHEDULER spins the owned thread. Reached by an active module only as a granted capability, never a process-global.

post-task

(post-task scheduler fire-at thunk)

Enqueue THUNK to run at FIRE-AT (an absolute universal-time) on SCHEDULER's owned thread and wake the thread so a nearer deadline is honored immediately. THUNK is called with no arguments and must return its own next-fire universal-time: NIL to run once, or a universal-time to reschedule (a value already in the past fires on the next loop turn). Returns the scheduled-task.

scheduler-alive-p

(scheduler-alive-p scheduler)

True while SCHEDULER's owned thread is running and has not been stopped.

scheduler-thread-running-p

(scheduler-thread-running-p scheduler)

True while a thread SCHEDULER started is still running, whether or not it has been asked to stop, such as one still running a task a stop gave up waiting for.

start-scheduler

(start-scheduler scheduler)

Spin SCHEDULER's single owned thread and return SCHEDULER. Idempotent: starting an already-running scheduler is a no-op. The thread runs until STOP-SCHEDULER.

While a thread a stop gave up on is still running a task, the start is refused with SCHEDULER-THREAD-STILL-RUNNING rather than running a second loop beside it. Once that thread has gone, a start proceeds as usual.

stop-scheduler

(stop-scheduler scheduler)

Stop SCHEDULER's owned thread and join it cleanly (no busy-wait). Idempotent: stopping an already-stopped scheduler is a benign no-op. Returns no values.

A thread still running scheduler-stop-timeout-seconds after the request, because a task it is running has not returned, signals SCHEDULER-DID-NOT-STOP instead of leaving you waiting with no word of why. The thread stays recorded on the scheduler until it returns, so a later stop joins it again and a start in the meantime is refused rather than running a second loop beside it.

While clock-stop-deadline is in force, as it is while a module is retired, the wait ends at that deadline instead, and with less than clock-stop-floor-seconds left the scheduler is only asked to stop and not waited on, so nothing is signalled about a thread there was no time to wait for.

Variables

*clock-stop-deadline*

The internal real time by which every wait for a scheduler to stop must be over, or NIL when nothing bounds it beyond scheduler-stop-timeout-seconds. Retiring a module binds it, so the module's own :stop, stopping its clocks, waits within the module's retirement budget rather than on top of it.

*clock-stop-floor-seconds*

Below this much time left before clock-stop-deadline, a stop does not wait at all: the scheduler is asked to stop and left to end. A wait that short would call an idle clock stuck merely because it had not yet woken.

*scheduler-stop-timeout-seconds*

How long STOP-SCHEDULER waits for the owned thread to return before it signals SCHEDULER-DID-NOT-STOP. An idle scheduler returns as soon as it is woken, so reaching this means a task it is running has not returned.