dev: isolate dev-server state per port and verify the pid holds it (#54) #60
No reviewers
Labels
No labels
area:defold
area:docs
area:infra
area:protocol
area:testing
kind:bug
kind:chore
phase:breadth
phase:core
phase:hardening
phase:slice
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
yuris/spacetimedb-defold!60
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/54-dev-server-state"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #54. First item of the v1.1.0 milestone (#59), landed alone and before any
parallel work: every later phase spawns concurrent subagents in separate
worktrees, and this bug bit three of them during the v1.0.0 push.
Two independent holes, both closed
The issue proposes either fix; only the first addresses data collision, which is
what actually bit people. Both are kept because they close different cases.
1. The default state dir is now keyed on
$PORT.STATEwas a single fixed/tmp/spacetimedb-defold, andPIDFILE,DATA,KEYS,LOGFILEandHOMEallderive from it, so two ports shared all five. An explicit
STDB_STATE_DIRstilloverrides, unchanged.
2.
server_pid()now verifies the recorded pid is our server for this port,by reading its own argv:
It previously did only
kill -0, which passes for a recycled pid. The trailingspace is load-bearing: it stops port 1234 matching a server on 12345. Guarded on
/procbeing readable, so a host without it (macOS) degrades to the oldbehaviour rather than failing every check — verified by masking
/procin anunprivileged mount namespace.
Reading argv rather than asking who holds the port is deliberate, and it is what
round two of review confirmed:
set -o pipefailcannot reach it — structurally, not bycareful quoting. See round one.
ssis iproute2, is not supplied by the flake, and isabsent from some minimal CI images.
stopable to kill a server that is still starting. A port check answers "is someone
serving?"; this answers "is this pid mine?", and the latter is what both
callers actually ask.
Both failures reproduced on
mainfirst, not inferredCollision, with 6901 already up:
3809830is the 6901 server. Recycled pid, onmain, pidfile holding a live pidthat holds no port:
Both are gone on this branch: two servers run concurrently on two ports each
minting its own identity,
restart 6901thenstop 6902leaves 6901 at 200 and6902 refused, and the planted-pidfile case starts a real server instead of
falsely reporting one.
Round one: the
sscheck was broken, and the premise for it was falseThe first revision used
ss -ltnp | grep -q "127.0.0.1:$PORT[^0-9].*pid=$pid,".Review found it blocking:
grep -qexits on first match,ssthen dies ofSIGPIPE, and
pipefailreports 141 for a pipeline that matched — soserver_pid()calls a healthy server absent. It needs >64KiB ofssoutput(~600 listening sockets) to surface, so it would have appeared only on a busy
host. Reproduced end to end with a same-format
ssstand-in:startfalselyrefused with "port held by another process", and
stopprinted "no serverrunning" while leaking a live server. This script already documents the
identical trap at the publish call.
Review also disproved the justification I had given: I stated
ss -ltnpwas"already read for the stray-server message". It was not —
mainreads no socketsanywhere, so that line would have introduced the repo's first iproute2
dependency. The claim came from the milestone plan and I repeated it without
checking, which is exactly what the re-capture instruction exists to catch.
Rather than patch the pipeline, the mechanism was replaced, which dissolved three
of the five findings at once (the pipefail defect, a
shellcheckSC1087 on$PORT[^0-9], and the dependency).Verified at head
Round two attacked the new check specifically: healthy server identified with the
right pid across start/restart/stop; stale live pid and live-server-on-another-port
both rejected; the trailing-space anchor works in both directions and survives
--listen-addrbeing the final argv element (the kernel's terminating NUL istranslated too); the glob RHS is double-quoted so
$PORTmatches literally andevery metacharacter fails closed;
/procmasked → check inert, degrading tomain's behaviour; no pipeline anywhere inserver_pid;shellcheckexits 0where the previous revision was the repo's only shellcheck error.
Also here
README.md's dev-server section documented the old fixed/tmp/spacetimedb-defold, which this change invalidates. Corrected, and it nownames the default the reader actually gets:
STDB_PORTdefaults to 3000, so thedirectory is
/tmp/spacetimedb-defold-3000. The README ships in the consumerarchive, so it is cheapest to fix in the PR that caused it.
Out of scope, filed as #63
With two ports pointed at one explicit
STDB_STATE_DIR, the second startoverwrites the pidfile with its own dead child, after which
stopcannot find thefirst server and leaks it. Reproduced. It requires deliberately sharing one state
dir, which is the configuration this PR's default now stops anyone reaching by
accident. Review also showed my first rationale for deferring it was wrong — the
pid is still discoverable in
/proc, so a fallback scan is a cheaper fix than"the fix has to be earlier" implied; #63 has been corrected accordingly.
Review actioned. Verdict: NEEDS ANOTHER REVIEW — the mechanism changed, so
the new one deserves a fresh look rather than my own sign-off.
Thank you for finding the pipefail defect. It was correct, it was blocking, and
it would have surfaced only on a busy host.
Finding 1 (BLOCKING, pipefail SIGPIPE) — fixed, but not with the suggested patch
The diagnosis is right:
grep -qexits on first match,ssdies of SIGPIPE,and
pipefailreports 141 for a pipeline that matched, soserver_pid()calls a healthy server absent. Doubly embarrassing given this file already
documents the identical trap at the publish call.
The here-string patch would have worked. I dropped
ssentirely instead,reading
/proc/$pid/cmdline, because that dissolves findings 1, 4 and 5 at oncerather than fixing them one at a time:
by careful quoting. Verified: no executable line in
server_pidcontains apipe, only
||.concern about the
stoppath.shellcheckexits 0, clearing finding 4's SC1087.The trailing space in
--listen-addr 127.0.0.1:$PORTis load-bearing: it stopsport 6960 matching a server on 69601. Observed argv confirms the format:
spacetimedb-standalone start --listen-addr 127.0.0.1:6960 --data-dir .... The/procguard degrades on macOS exactly ascommand -v ssdid.Re-verified after the change: recycled-pid case still rejected (starts a real
server instead of falsely reporting one); two ports concurrently still isolated,
restart 6901thenstop 6902leaves 6901 at 200 and 6902 refused; unit suite6 passed 0 failed; stylua exit 0; native and HTML5 conformance both PASS.
Finding 5 (the false premise) — you were right, and it was mine
I asserted in the PR body that
ss -ltnpwas "already read at :80-82 for thestray-server message". It is not.
mainreads no sockets anywhere. That claimcame from the milestone plan and I repeated it without checking, which is exactly
what the re-capture instruction exists to catch. Moot now that
ssis gone, butworth recording that the premise was wrong rather than merely superseded.
Finding 2 (README path) — fixed
README.md:1505still documented the old fixed/tmp/spacetimedb-defold, whichthis change made false. Corrected, and it now states why the path is keyed on
the port. Good catch that the README ships in the consumer archive.
Finding 3 (stop leak) — partly fixed here, remainder filed as #63
The
/proccheck fixes the half you flagged as the design concern:stopcannow identify our own server during the window before it binds, which no port
check could.
The scenario you reproduced is not fixed, and I confirmed that honestly rather
than claiming it. With two ports pointed at one explicit
STDB_STATE_DIR:6913's pidfile held 3848953; 6919 failed the database lock and overwrote the
pidfile with its own dead child 3849062;
stopon 6913 then saidno server runningwhile 6913 still answered 200.server_pid()cannot fix it — by thatpoint the pid it needs is gone from the pidfile. The fix has to be earlier
(refuse to start when the pidfile names a live server for another port, or key
the pidfile by port within the state dir).
Filed as #63 rather than expanded into this PR: it requires deliberately sharing
one state dir across two ports, which is the configuration this PR's default now
stops anyone reaching by accident.
Finding 4 (SC1087) — cleared
Folded in;
shellcheckis now silent on this file, where the previous revisionwas the repo's only shellcheck error.