Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.gateway.discovery

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.

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.
raw docstring

acquire-spawn-lock!clj

(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.
sourceraw docstring

await-registry!clj

(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.
sourceraw docstring

base-argvclj

(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 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
self-restart trick).
sourceraw docstring

current-pidclj

(current-pid)
source

db-targetclj

(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.
sourceraw docstring

default-token-fileclj

(default-token-file)

Stable bearer-token file shared by managed gateway daemons and local clients. A client can use it to authenticate a loopback /healthz probe and repair a missing registry without spawning a second daemon onto an occupied port.

Stable bearer-token file shared by managed gateway daemons and local clients.
A client can use it to authenticate a loopback `/healthz` probe and repair a
missing registry without spawning a second daemon onto an occupied port.
sourceraw docstring

delete-registry!clj

(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.
sourceraw docstring

delete-registry-if!clj

(delete-registry-if! db pred)

Atomically remove db's registry only when (pred current-entry) is truthy. The read + delete share the registry mutation lock, preventing an old daemon or stale discoverer from deleting a successor's freshly-written ownership.

Atomically remove `db`'s registry only when `(pred current-entry)` is truthy.
The read + delete share the registry mutation lock, preventing an old daemon
or stale discoverer from deleting a successor's freshly-written ownership.
sourceraw docstring

deregister-self!clj

(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).
sourceraw docstring

discover-or-start!clj

(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 :recovering} a live registered owner missed a probe — preserve it and wait for recovery instead of deleting it; {: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 registry whose PID is DEAD is conditionally deleted before spawning; a LIVE PID that merely missed a health probe is preserved and re-probed. This distinction is critical: deleting a live owner's only registry makes every new client spawn into its occupied port and time out forever. 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 :recovering}`         a live registered owner missed a probe — preserve
                               it and wait for recovery instead of deleting it;
`{: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 registry whose PID is DEAD is
conditionally deleted before spawning; a LIVE PID that merely missed a health
probe is preserved and re-probed. This distinction is critical: deleting a
live owner's only registry makes every new client spawn into its occupied port
and time out forever. 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.
sourceraw docstring

lock-fileclj

(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).
sourceraw docstring

memory-db?clj

(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.
sourceraw docstring

native-image?clj

(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.
sourceraw docstring

pid-alive-cached?clj

(pid-alive-cached? pid)

Like pid-alive? but memoized per pid for up to ~5s (PID_LIVENESS_TTL_MS). Hot refcount/status polling probes the same handful of pids many times a second, and each pid-alive? is a macOS proc-info syscall — that raw call rate, not the work, is what pins the gateway CPU. The TTL trades up to ~5s of liveness staleness (harmless for refcount shutdown, which also has a 30s startup grace) for removing the ProcessHandle/of syscall storm. A nil pid is NOT alive.

Like `pid-alive?` but memoized per pid for up to ~5s (`PID_LIVENESS_TTL_MS`).
Hot refcount/status polling probes the same handful of pids many times a
second, and each `pid-alive?` is a macOS proc-info syscall — that raw call
rate, not the work, is what pins the gateway CPU. The TTL trades up to ~5s of
liveness staleness (harmless for refcount shutdown, which also has a 30s
startup grace) for removing the `ProcessHandle/of` syscall storm. A nil pid
is NOT alive.
sourceraw docstring

pid-alive?clj

(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).
sourceraw docstring

read-registryclj

(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.
sourceraw docstring

register-self!clj

(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).
sourceraw docstring

registry-dirclj

(registry-dir)

Directory holding the per-DB registry files. Not created here.

Directory holding the per-DB registry files. Not created here.
sourceraw docstring

registry-fileclj

(registry-file db)

The registry EDN file for DB target db.

The registry EDN file for DB target `db`.
sourceraw docstring

registry-fresh?clj

(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).
sourceraw docstring

registry-keyclj

(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).
sourceraw docstring

release-spawn-lock!clj

(release-spawn-lock! {:keys [lock channel]})

Release a holder from acquire-spawn-lock!. Never throws.

Release a holder from [[acquire-spawn-lock!]]. Never throws.
sourceraw docstring

spawn-argvclj

(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).
sourceraw docstring

spawn-detached!clj

(spawn-detached! {:keys [db] :as 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 inside a throwaway shell that backgrounds and disowns it (the daemon is reparented to init) and renames it to vis gateway start …; 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. Its stdout+stderr are captured to a per-DB boot log under the registry dir so a daemon that dies on startup is diagnosable instead of vanishing. 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 inside a throwaway shell that backgrounds and disowns it (the daemon is
reparented to init) and renames it to `vis gateway start …`; 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. Its
stdout+stderr are captured to a per-DB boot log under the registry dir so a
daemon that dies on startup is diagnosable instead of vanishing. Returns nil.
sourceraw docstring

unix-launch-cmdclj

(unix-launch-cmd argv log-path)

Full [shell "-c" script] command that detaches argv with its stdio in log-path. Preferred form runs under bash so the daemon can exec -a itself into a readable vis <subcommand> … process line; trap '' HUP before the exec survives execve, giving the same SIGHUP immunity nohup provides. Falls back to plain nohup under sh when no bash exists (the process line then keeps the absolute executable path, which is cosmetic, not functional).

Full `[shell "-c" script]` command that detaches `argv` with its stdio in
`log-path`. Preferred form runs under bash so the daemon can `exec -a` itself
into a readable `vis <subcommand> …` process line; `trap '' HUP` before the
exec survives execve, giving the same SIGHUP immunity `nohup` provides. Falls
back to plain `nohup` under `sh` when no bash exists (the process line then
keeps the absolute executable path, which is cosmetic, not functional).
sourceraw docstring

write-registry!clj

(write-registry! db entry)

Atomically write entry (a map, :db/:created-at filled in if absent) as the registry for db. Registry readers therefore never observe a partial EDN file and mistake a live daemon for a missing one. Returns the written entry.

Atomically write `entry` (a map, `:db`/`:created-at` filled in if absent) as
the registry for `db`. Registry readers therefore never observe a partial EDN
file and mistake a live daemon for a missing one. Returns the written entry.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close