Gateway daemon discovery + registry (build order step 1).
One long-lived gateway per DB owns execution; every TUI/web/whatever is a thin client of it. This namespace answers the boot-time question: "is a gateway already running for my DB — attach; else spawn one, DETACHED (nobody's child), then hand back where to connect."
Registry: one EDN file per DB at ~/.vis/gateway/registry/<sha256(db)>.edn
holding {:pid :port :host :secret :db :created-at}. Freshness = the recorded
:pid is still alive AND a caller-supplied probe confirms the port+secret
are really OUR daemon (guards OS pid reuse — see D4/Q3 in TODO-gateway-daemon).
Design decisions (locked, see TODO-gateway-daemon.md):
--db share one daemon).register-self! from serve-main!), so a
spawner never needs the child pid — it just polls for a fresh registry.:memory never registers/spawns (headless one-shot stays in-process).Effects (spawn, probe, pid-liveness) are injectable so the orchestration
in discover-or-start! is unit-testable without a real process.
Gateway daemon discovery + registry (build order step 1).
One long-lived gateway per DB owns execution; every TUI/web/whatever is a
thin client of it. This namespace answers the boot-time question: "is a
gateway already running for my DB — attach; else spawn one, DETACHED (nobody's
child), then hand back where to connect."
Registry: one EDN file per DB at `~/.vis/gateway/registry/<sha256(db)>.edn`
holding `{:pid :port :host :secret :db :created-at}`. Freshness = the recorded
`:pid` is still alive AND a caller-supplied `probe` confirms the port+secret
are really OUR daemon (guards OS pid reuse — see D4/Q3 in TODO-gateway-daemon).
Design decisions (locked, see TODO-gateway-daemon.md):
- Q2 registry key = the DB path (two dirs sharing `--db` share one daemon).
- Q3 race = port-bind winner is the daemon; the loser attaches. The daemon
SELF-REGISTERS on startup (via [[register-self!]] from `serve-main!`), so a
spawner never needs the child pid — it just polls for a fresh registry.
- Q5 `:memory` never registers/spawns (headless one-shot stays in-process).
Effects (`spawn`, `probe`, pid-liveness) are injectable so the orchestration
in [[discover-or-start!]] is unit-testable without a real process.(acquire-spawn-lock! db)Try to grab the EXCLUSIVE cross-process spawn lock for db WITHOUT blocking.
Returns a {:channel :lock} holder when THIS process won the right to spawn,
or nil when another process already holds it (that process is the designated
spawner — the caller should just await the registry instead of launching a
competing daemon). Never throws.
Try to grab the EXCLUSIVE cross-process spawn lock for `db` WITHOUT blocking.
Returns a `{:channel :lock}` holder when THIS process won the right to spawn,
or nil when another process already holds it (that process is the designated
spawner — the caller should just await the registry instead of launching a
competing daemon). Never throws.(await-registry! db probe)(await-registry! db
probe
{:keys [timeout-ms poll-ms on-tick]
:or {timeout-ms 8000 poll-ms 100}})Poll for a FRESH registry entry for db up to timeout-ms, checking every
poll-ms. :on-tick (optional) is called before each sleep with the elapsed
millis so a caller can render live 'still waiting…' feedback; it never breaks
the poll loop. Returns the entry or nil on timeout.
Poll for a FRESH registry entry for `db` up to `timeout-ms`, checking every `poll-ms`. `:on-tick` (optional) is called before each sleep with the elapsed millis so a caller can render live 'still waiting…' feedback; it never breaks the poll loop. Returns the entry or nil on timeout.
(base-argv)Prefix argv that re-launches vis in a fresh process: the native binary as-is,
or java -cp … clojure.main -m com.blockether.vis.core on the JVM (mirrors the
telegram self-restart trick).
Prefix argv that re-launches vis in a fresh process: the native binary as-is, or `java -cp … clojure.main -m com.blockether.vis.core` on the JVM (mirrors the telegram self-restart trick).
(db-target db)Normalize a DB target/spec to the value that identifies a daemon. SQLite specs
use their :path; :memory stays ephemeral; other values pass through.
Normalize a DB target/spec to the value that identifies a daemon. SQLite specs use their `:path`; `:memory` stays ephemeral; other values pass through.
(delete-registry! db)Remove the registry file for db. Never throws. Returns true when a file was
removed.
Remove the registry file for `db`. Never throws. Returns true when a file was removed.
(deregister-self! db)Called from the daemon's shutdown hook: delete OUR registry entry for db,
but only if it still points at us (never clobber a successor that took over).
Called from the daemon's shutdown hook: delete OUR registry entry for `db`, but only if it still points at us (never clobber a successor that took over).
(discover-or-start! {:keys [db] :as opts}
&
{:keys [probe spawn timeout-ms poll-ms on-event]
:or {probe (constantly true) spawn spawn-detached!}})Resolve a gateway for db. Returns:
{:mode :none} for a :memory DB (Q5, never spawns);{:mode :attach :entry {…}} a fresh daemon already runs — connect;{:mode :spawned :entry {…}} WE spawned one and it self-registered;{:mode :awaited :entry {…}} ANOTHER process was spawning — we waited
on its daemon instead of piling on;{:mode :timeout} nobody came up in time.probe is the port+secret liveness check (defaults to pid-liveness alone).
spawn (default spawn-detached!) and now are injectable for tests.
:on-event (optional) is a side-effecting callback the caller uses to surface
live progress so waiters are NEVER left staring at a frozen screen. It fires
only on the SLOW path (never on a plain :attach) with maps:
{:phase :spawning} WE won the lock and are launching the daemon;
{:phase :awaiting} another vis is starting it — we wait, not spawn;
{:phase :tick :elapsed-ms n} a poll heartbeat while awaiting either of those;
{:phase :ready :mode m :entry e} the daemon came up;
{:phase :timeout} nobody came up in time. It never throws upward.
THUNDERING-HERD GUARD (see acquire-spawn-lock!): when the registry is not
fresh, only the ONE process that wins the cross-process spawn lock actually
launches a daemon; every other concurrent starter finds the lock held, learns
'someone is already spawning', and just awaits the winner's self-registration.
That replaces the old blind-port-bind race (N daemons launched, N-1 crashing)
with a single spawn + N-1 cheap waiters. A stale registry is deleted before
spawning; the lock is re-checked against a double-read so a daemon that came up
between our read and the lock is attached, not re-spawned.
Resolve a gateway for `db`. Returns:
- `{:mode :none}` for a `:memory` DB (Q5, never spawns);
- `{:mode :attach :entry {…}}` a fresh daemon already runs — connect;
- `{:mode :spawned :entry {…}}` WE spawned one and it self-registered;
- `{:mode :awaited :entry {…}}` ANOTHER process was spawning — we waited
on its daemon instead of piling on;
- `{:mode :timeout}` nobody came up in time.
`probe` is the port+secret liveness check (defaults to pid-liveness alone).
`spawn` (default [[spawn-detached!]]) and `now` are injectable for tests.
`:on-event` (optional) is a side-effecting callback the caller uses to surface
live progress so waiters are NEVER left staring at a frozen screen. It fires
only on the SLOW path (never on a plain `:attach`) with maps:
`{:phase :spawning}` WE won the lock and are launching the daemon;
`{:phase :awaiting}` another vis is starting it — we wait, not spawn;
`{:phase :tick :elapsed-ms n}` a poll heartbeat while awaiting either of those;
`{:phase :ready :mode m :entry e}` the daemon came up;
`{:phase :timeout}` nobody came up in time. It never throws upward.
THUNDERING-HERD GUARD (see [[acquire-spawn-lock!]]): when the registry is not
fresh, only the ONE process that wins the cross-process spawn lock actually
launches a daemon; every other concurrent starter finds the lock held, learns
'someone is already spawning', and just awaits the winner's self-registration.
That replaces the old blind-port-bind race (N daemons launched, N-1 crashing)
with a single spawn + N-1 cheap waiters. A stale registry is deleted before
spawning; the lock is re-checked against a double-read so a daemon that came up
between our read and the lock is attached, not re-spawned.(lock-file db)OS advisory-lock file guarding daemon SPAWN for db. Sits beside the registry
file so it shares the per-DB key; a stale lockfile is harmless (the OS lock,
not the file's existence, is what's held).
OS advisory-lock file guarding daemon SPAWN for `db`. Sits beside the registry file so it shares the per-DB key; a stale lockfile is harmless (the OS lock, not the file's existence, is what's held).
(memory-db? db)True for the ephemeral in-memory DB target — it never spawns/attaches a
daemon (Q5). Accepts raw --db values and resolved DB specs.
True for the ephemeral in-memory DB target — it never spawns/attaches a daemon (Q5). Accepts raw `--db` values and resolved DB specs.
(native-image?)True when running as the compiled GraalVM binary rather than on the JVM.
True when running as the compiled GraalVM binary rather than on the JVM.
(pid-alive? pid)Best-effort: is OS process pid still running? A nil pid is NOT alive (an
entry with no pid can't own a live daemon).
Best-effort: is OS process `pid` still running? A nil pid is NOT alive (an entry with no pid can't own a live daemon).
(read-registry db)Read the registry entry for db, or nil when absent/unreadable/garbage.
Read the registry entry for `db`, or nil when absent/unreadable/garbage.
(register-self! db {:keys [port host secret]})Called by the daemon (serve-main!) once it is listening: write this process
as the owner of db. secret should be the gateway bearer token (or any
nonce a client can echo back on registry-fresh?'s probe). Returns the
entry, or nil for a :memory DB (which never registers).
Called by the daemon (`serve-main!`) once it is listening: write this process as the owner of `db`. `secret` should be the gateway bearer token (or any nonce a client can echo back on [[registry-fresh?]]'s probe). Returns the entry, or nil for a `:memory` DB (which never registers).
(registry-dir)Directory holding the per-DB registry files. Not created here.
Directory holding the per-DB registry files. Not created here.
(registry-file db)The registry EDN file for DB target db.
The registry EDN file for DB target `db`.
(registry-fresh? entry)(registry-fresh? {:keys [pid] :as entry} probe)True when entry describes a LIVE daemon: it has a pid, that pid is alive,
AND probe confirms it (defaults to trusting pid-liveness alone). The client
adapter injects a real probe that hits http://host:port with :secret to
defeat pid reuse (Q3).
True when `entry` describes a LIVE daemon: it has a pid, that pid is alive, AND `probe` confirms it (defaults to trusting pid-liveness alone). The client adapter injects a real `probe` that hits `http://host:port` with `:secret` to defeat pid reuse (Q3).
(registry-key db)Stable key for a DB target: sha256 of its canonical path (Q2).
Stable key for a DB target: `sha256` of its canonical path (Q2).
(release-spawn-lock! {:keys [lock channel]})Release a holder from acquire-spawn-lock!. Never throws.
Release a holder from [[acquire-spawn-lock!]]. Never throws.
(spawn-argv {:keys [db port host token-file require-token? base]})Full argv to launch a fresh gateway daemon for db on port. Serve flags
follow the gateway start subcommand. :base lets a caller override the
re-exec prefix (tests do).
Full argv to launch a fresh gateway daemon for `db` on `port`. Serve flags follow the `gateway start` subcommand. `:base` lets a caller override the re-exec prefix (tests do).
(spawn-detached! opts)Fire-and-forget launch of a gateway daemon for db, fully DETACHED so closing
the spawner's terminal (SIGHUP) does NOT kill it (D4). On unix this runs the
argv under nohup … & inside a throwaway sh (the daemon is reparented to
init); elsewhere it falls back to a plain detached ProcessBuilder. The daemon
SELF-REGISTERS its pid/port on startup, so we never need its pid here. Output
is discarded (the daemon logs through Telemere). Returns nil.
Fire-and-forget launch of a gateway daemon for `db`, fully DETACHED so closing the spawner's terminal (SIGHUP) does NOT kill it (D4). On unix this runs the argv under `nohup … &` inside a throwaway `sh` (the daemon is reparented to init); elsewhere it falls back to a plain detached ProcessBuilder. The daemon SELF-REGISTERS its pid/port on startup, so we never need its pid here. Output is discarded (the daemon logs through Telemere). Returns nil.
(write-registry! db entry)Write entry (a map, :db/:created-at filled in if absent) as the registry
for db. Creates the registry dir. Returns the written entry.
Write `entry` (a map, `:db`/`:created-at` filled in if absent) as the registry for `db`. Creates the registry dir. Returns the written entry.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |