Where the service directory lives
Where the service directory is stored, and how a host's resolver serves, relays and holds its routes.
The service_directory block of registry.json in the configured canonical
backend, served as versioned snapshots by the authority. Each host materializes
its own view as forward markers under $HOME/.stado/forwards/<service>.local,
and that host's resolver keeps them: it writes every marker the directory
declares for the host when it starts, before resolver status reports
serving, and again on every refresh, rewriting only a marker that is missing
or holds another address. The resolver logs each write, and, once that host's
markers follow a newer directory a refresh loaded, the generation it loaded:
stado resolver wrote forward marker <service> -> <url>
stado resolver loaded directory generation <N>
A service the directory gives the host no address for gets no
marker, and a marker the directory does not declare is left alone for
service directory publish --prune. Before the resolver kept them, markers
existed only where an operator had run service directory publish by hand, so
a consumer such as Weles could fail at start on a host the directory placed
its dependency on. service directory publish writes the same set on demand
and reports the fossils; stado host inventory reconciles marker against
socket table and marker against directory as deliberately independent axes.
On a host whose configured store is another host's object API, every
registry read — stado registry self, database resolve, service directory connect, the registry behind a host command, the storage layout — takes the
authority's versioned snapshot over that registry-owned SSH transport before
the store, in one place (the registry store itself), because the object API
authorizes each read against the vault on its host and a vault that stops
answering holds every such read with it. The authority is located through
this host's last-known-good copy; a host that keeps no copy, or is itself
the authority, reads its store. A snapshot that cannot be read is said on
stderr with the authority's own sentence (stado: the registry authority's snapshot could not be read (…); reading the registry store at … instead)
and the store is read, which the store read's own czekam line then names.
Writes go the same way: on a host whose authority is another target, the
compare-and-swap of every registry write (registry set, registry push,
registry host …) runs on the authority as stado registry push - --if-generation <the generation this host read> --force --allow-empty-fleet --json over the same SSH transport, with the document on standard input; the
guards ran on this host against that generation, which the authority's swap is
conditional on. A write therefore never waits on this host's own resolver, so
a resolver that refuses to start cannot hold back the registry change that
lets it start. A moved generation is still exit 75; a write no SSH path
carried says registry write through authority <target>: no SSH connection path answered (<path>: <cause>; …), and one the authority refused says
registry authority <target> refused the write with exit <code>: <its stderr>. Every write is then read back the way every reader reads it, here
through the authority's snapshot, and confirmed when the generation the swap
returned holds the document sent, compared as a document, not as bytes: the
snapshot carries the document re-serialized. A write that cannot be confirmed
says registry write of generation <generation> at <location> is unconfirmed:
and why: no object read back, another generation read back (another write
landed since, or the read came from a copy behind the store), something that
is not JSON, or a different document. registry push prints replaced=<the generation it replaced>, or created when it created the registry, whose
JSON receipt then carries no replaced.
A resolver serves every route it can and names each one it cannot; one route
it cannot serve does not stop it or any other route. On the
authority host, whose store is storage.stado.url, an adapter declared on that
same address can neither bind while the object API holds it nor read the
registry while it does not, so that one adapter is left unserved with this resolver is declared to serve <bind> for service … and the registry it must read first is configured at storage.stado.url = …; every other route is served. An adapter whose port something else holds is left unserved with its
bind error (could not bind <bind>: <cause>. Something else already holds this resolver port; find it with lsof …). Each is published in
~/.stado/resolver-state.json beside the served adapters with its reason
(unserved), the next directory change tries it again, and stado resolver status reports the resolver degraded with the resolver does not serve the <service> adapter for consumer <consumer> at <bind>: <reason>. Only a
resolution API that cannot bind still stops the resolver. A host whose
authority is another target is not refused the store adapter at all: its
resolver reads every snapshot from the authority over SSH, so a store address
it serves is the route it hands its own clients, and the workstation keeps
every adapter.
When the object API cannot serve the registry, the verbs that only read the
directory — connect, endpoint, bind, show and profiles — answer from
this host's last-known-good copy at ~/.stado/cache/registry-last-good.json.
Each such answer first prints one line on stderr with the copy's age, its
read_at, its generation and the authority's own refusal, for example
reading the last-known-good registry copy from 44135s ago (…) because the authority did not answer: Stado object API error HTTP 503: {"error":"object authorization unavailable"}. A host whose store is a local directory has no
such copy, and one that has never read the authority has none either; both
exit with the authority's error. publish, consumer add, consumer remove and
placement move never read the copy: a routing change committed against a
stale generation is what the authority exists to refuse.
A wait leaves ~/.stado/resolver-waiting.json (described below) as soon as its
client closes, so a service whose clients give up before it answers never shows an old wait there. Every request
that ends is therefore also recorded per service in
~/.stado/resolver-answers.json: last_answered_at, when the service last sent
the first byte of an answer, and, while it has answered none since,
first_unanswered_at and last_unanswered_at, when the first and the latest
request ended without one. A service with first_unanswered_at set has taken
every request since that instant and answered none, however young each wait
was when its client left. The first answer clears both.
A resolver adapter accepts its client at once; what it relays may never be
answered. The resolver publishes every such wait beside its state, in
~/.stado/resolver-waiting.json: a channel open the destination host has not
answered (phase: open), and a request that went up an open channel whose
service has not sent its first byte back (phase: answer), each with the
service, consumer, the adapter's bind, the destination host and endpoint,
since, when the client began waiting — the accept of its connection for a
request, since the client has held that connection from then on, through
the channel open before its request went up; the start of the open for a
channel open — client, the loopback address the client connected
from, and for a request, request, the request line the client sent first
(GET /api/object?uri=… HTTP/1.1) when its first bytes carried a whole
printable line: the one fact that names which operation the holding process
is waiting on, so many waits from one process each say what they asked for. A resolver publishes the list as it
starts, empty, so the waits a previous resolver left are not read as held by
this one, and a reader finds a list rather than none. stado resolver status
lists them under waiting_opens with waited_seconds, and one that has
waited longer than the refresh interval the target declares is a blocker:
service <name> for consumer <consumer> at <bind> has waited <N>s for the first byte of an answer on an open channel to to <endpoint> on <host>: the client holds an accepted connection that receives nothing; ask that host what answers on <endpoint> held by the client at <address>:lsof
-nP -i : names the process still holding it. The client address is
what ties a held connection to the process holding it: a hook that ended
hours ago is told from one still waiting by whether anything still holds that
port, and a list an older resolver published, without the address, names no
holder. Waits on one route — the same service, consumer, bind, phase,
endpoint and host — are one blocker, since they stand behind the same thing:
a route held for hours keeps every connection its clients open, and the
blocker reads <N> connections on one route, the oldest: <that wait's sentence>; a route holding one connection reads that sentence alone.
waiting_opens in the JSON still lists every wait.
A request that would go through such an adapter is refused before it is
sent, with that sentence, instead of standing behind the held connection for
as long as it stands: every object API request (Stado object API not asked: GET <url> not sent: this host's resolver holds <N> connection(s) on that adapter unanswered longer than the <N>s refresh interval it declares, so a request sent now would stand behind them: …), every Skarbiec request
(Skarbiec not asked: …), and the probe connect makes of the route it
hands out (<service> is placed on <host> and did not answer at <url>: this host's resolver holds …), each with the exit code of a store that does not
answer. The refusal ends when the service answers the held request or the
connection ends, and stado resolver status says which it is waiting for. A
waiting list that is there and cannot be read is itself the refusal, naming
the file.
The same list is read before a command sent to a host over its fleet channel; a host whose services it holds is not refused for that, but asked over every declared path, as a remote command describes.
A route through this host's own resolver adapter that nobody answers at all
(connection refused on the adapter's bind) is the resolver's doing, not
the service's. connect reads what the last serve --resolver on this host
published (~/.stado/resolver-state.json, the file stado resolver status
reads) and says why beside the refusal: when that resolver holds no
listener, <service> is placed on <host> and did not answer at <url>: <connection refused>; this host's resolver (pid <pid>, state failed at <time>) holds no listener: <the resolver's own reason>; when it serves
other routes but left this one unserved, …; this host's resolver (pid <pid>) does not serve <bind> for <service>/<consumer>: <its reason>. A resolver
that published no reason is said to have published none. The state counts
only while the process that published it still runs: the file outlives the
resolver that wrote it, so a dead resolver's listening: true is never read
as ports someone holds while its replacement is still starting. Whether it
runs is read from ~/.stado/resolver-state.lock, which the resolver locks
from its first publication until it exits and in which it records its pid,
never from the pid alone, which the system hands to another process once the
resolver is gone. A lock any reader can take means no resolver runs; a held
lock counts for the published state only when its holder is the pid that
published it. When that process is gone, connect, stado resolver status
and every program reading the published adapter say the resolver that published this host's state (pid <pid>, state <state> at <time>) is not running, and no resolver has published since; its listeners went with it.
The reason is the one to act on: a configuration contradiction or a held
port stays until the configuration, the holder or the installed build
changes, and no retry of the connect changes it.
Source: this website