Skip to content

Commit 705015c

Browse files
feat(connect): detect existing instance before install (disconnect + reinstall or cancel) (#76)
* feat(connect): detect existing instance before install; disconnect + reinstall or cancel Re-running the one-liner over a live edge-connect container would collide with the existing tunnel, container name, and host ports. The installers now detect a container named $DZ_NAME before any host prep or docker run, warn that an instance already exists, and prompt to reinstall or cancel. On reinstall they print "Uninstalling existing instance...", run `doublezero disconnect` inside the old container to drop the tunnel cleanly, then remove it. On cancel they abort and leave the running instance untouched. Non-interactive runs (DZ_ASSUME_YES=1) reinstall. Applied identically to all three installers. * fix(connect): graceful, bounded teardown of an existing instance; robust headless detection Address review of the existing-instance guard: - Graceful, bounded teardown: replace the raw unbounded `docker exec … doublezero disconnect` with `docker stop`, so the container entrypoint's SIGTERM trap runs its own bounded disconnect (releasing the GRE tunnel/routes/on-chain session) before doublezerod is killed. The stop is timeout-guarded so a wedged or restarting container can't hang the installer forever. - Warn loudly on orphaned state: after removal, verify the doublezero1 tunnel interface is gone from the host netns (the container is --network host) and warn if it lingers, and warn if the stop timed out/errored before the forced removal. - Preserve headless automation: a no-TTY run with DZ_ASSUME_YES unset now reinstalls (with a notice) instead of aborting, matching pre-guard behaviour; an interactive decline still aborts, with distinct messaging. - Robust TTY detection: probe an actual /dev/tty open rather than trusting `-r` (a readable inode still fails to open with no controlling terminal), which also sidesteps confirm()'s tty read in that case. - Network-aware prompt: name the existing instance's env/image (all three installers share DZ_NAME, so the testnet installer can find a live mainnet one). - Add tests/scripts/reinstall_existing.bats: running (stop-before-rm ordering), stopped (no stop), no-instance (no-op), and headless (reinstall, not abort). * fix(connect): guard the env-label command sub against set -o pipefail abort The existing-instance env label built its value with a bare `docker inspect … | sed | head -1` command substitution, missing the `|| true` its DZ_IMAGE sibling has. Under `set -euo pipefail` a failing docker inspect (container removed between the `ps -a` detection and the inspect, or a daemon blip) — or `head` closing the pipe early on multi-line env — makes the pipeline non-zero and aborts the whole installer before the "already exists" warning even prints. Add `|| true` so the label stays best-effort. Also harden the bats stub: `docker inspect` now emits realistic multi-line env + an image (exercising the DZ_ENV extraction and the head-closes-pipe path) and can be told to fail via STUB_INSPECT_FAIL. New test asserts a mid-teardown inspect failure still detects, warns, and reinstalls; the running-instance test now asserts the warning names the victim's env/image.
1 parent 3ccf056 commit 705015c

5 files changed

Lines changed: 382 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
290290
of the canonical BBO identity, so a count-only change at an unchanged price/size is a distinct quote.
291291

292292
### Changed
293+
- Installer (`scripts/connect*.sh`) now detects an **existing edge-connect instance** before
294+
installing: if a container named `$DZ_NAME` (default `doublezero-edge-connect`) already exists on
295+
the host, the installer warns (naming the instance's env/image, since all three installers share
296+
`$DZ_NAME` — so e.g. the testnet installer flags a live *mainnet* container) and prompts to
297+
**reinstall or cancel** instead of silently colliding with the live tunnel/ports. On reinstall it
298+
prints "Uninstalling existing instance..." and, for a running instance, tears it down **gracefully
299+
via `docker stop`** — the container entrypoint's SIGTERM trap runs a bounded `doublezero
300+
disconnect` (releasing the GRE tunnel/routes/on-chain session) before doublezerod is killed —
301+
rather than a raw, unbounded `docker exec … disconnect`; the stop is `timeout`-guarded so a wedged
302+
or restarting container can't hang the installer. After removal it verifies the `doublezero1`
303+
tunnel interface is actually gone from the host netns and warns loudly if it lingers (orphaned
304+
session). Interactively, declining aborts and leaves the instance untouched; non-interactively
305+
(`DZ_ASSUME_YES=1`, or no usable TTY) it reinstalls, preserving the previous silent-reinstall
306+
behaviour for automation. TTY detection probes an actual `/dev/tty` open rather than trusting
307+
`-r`, so a headless run with no controlling terminal is classified correctly. The env/image
308+
labelling is best-effort — a `docker inspect` that fails mid-teardown (container removed between
309+
detection and inspect, or a daemon blip) no longer aborts the installer under `set -o pipefail`.
310+
Applied identically to all three installers; covered by `tests/scripts/reinstall_existing.bats`.
293311
- `dz_depth_dropped_total` now carries a `publisher` label (the dropped copy's source class),
294312
symmetric with `dz_depth_admitted_total`, so a lagging publisher losing the book race is
295313
directly visible (#66). This changes the label set of an existing series — exact-label matchers

scripts/connect-devnet.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,72 @@ if ! command -v docker >/dev/null 2>&1; then
362362
fi
363363
$SUDO docker info >/dev/null 2>&1 || die "Docker is installed but the daemon isn't reachable. Start it (e.g. 'sudo systemctl start docker') and re-run."
364364

365+
# ----------------------------------------------------------------------------
366+
# 3b. existing instance? reinstall (graceful teardown) or cancel
367+
# ----------------------------------------------------------------------------
368+
# A prior run leaves a long-lived container (--restart unless-stopped) that holds
369+
# the DoubleZero tunnel and the WS sink. Re-running the installer over a live
370+
# instance would collide (same container name, same tunnel, same host ports), so
371+
# detect one up front and tear it down cleanly first.
372+
reinstall_existing_instance() {
373+
$SUDO docker ps -a -q --filter "name=^${DZ_NAME}$" 2>/dev/null | grep -q . || return 0
374+
375+
local running="" existing_env="" existing_img=""
376+
$SUDO docker ps -q --filter "name=^${DZ_NAME}$" 2>/dev/null | grep -q . && running=1
377+
# Label the victim: all three installers share DZ_NAME, so e.g. the testnet
378+
# installer can find a live *mainnet* container -- name the network/image so the
379+
# operator isn't asked to destroy an unidentified instance.
380+
existing_env="$($SUDO docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$DZ_NAME" 2>/dev/null | sed -n 's/^DZ_ENV=//p' | head -1 || true)"
381+
existing_img="$($SUDO docker inspect -f '{{.Config.Image}}' "$DZ_NAME" 2>/dev/null || true)"
382+
warn "An edge-connect instance ('$DZ_NAME'${existing_env:+, env=$existing_env}${existing_img:+, image=$existing_img}) already exists on this host${running:+ and is running}."
383+
384+
# Decide whether to reinstall, keeping the three cases distinct so we neither
385+
# break headless automation nor mislabel a genuine decline:
386+
# DZ_ASSUME_YES=1 -> reinstall (skip the prompt)
387+
# interactive decline -> abort (the operator said no)
388+
# no usable TTY, !yes -> reinstall, but say so (pre-3b behaviour was a silent
389+
# reinstall; keep automation working rather than abort)
390+
# A readable /dev/tty inode (-r) can still fail to OPEN with no controlling
391+
# terminal (cron/systemd/`curl|bash` without a tty), so probe an actual open
392+
# rather than trusting -r (and skip confirm()'s own tty read in that case).
393+
if [ "$DZ_ASSUME_YES" = 1 ]; then
394+
:
395+
elif { : <"$TTY"; } 2>/dev/null; then
396+
confirm "Reinstall? This disconnects and removes the existing instance" \
397+
|| die "Cancelled: leaving the existing instance in place (manage it with 'sudo docker logs $DZ_NAME')."
398+
else
399+
warn "No terminal to prompt on and DZ_ASSUME_YES is unset; reinstalling to preserve non-interactive behaviour (set DZ_ASSUME_YES=1 to silence this, or run interactively to be asked first)."
400+
fi
401+
402+
info "Uninstalling existing instance..."
403+
if [ -n "$running" ]; then
404+
# Graceful: `docker stop` sends SIGTERM, which the container entrypoint traps to
405+
# run a bounded `doublezero disconnect` (only if a tunnel is up) before tearing
406+
# doublezerod down -- releasing the GRE tunnel/routes/on-chain session cleanly.
407+
# The container's --stop-timeout (60s) bounds the stop; the outer `timeout`
408+
# guards a wedged docker CLI / restarting container so we can never hang forever.
409+
info "Stopping it gracefully (disconnecting the DoubleZero tunnel)..."
410+
local stop_ok=1
411+
if command -v timeout >/dev/null 2>&1; then
412+
$SUDO timeout 90 docker stop "$DZ_NAME" >/dev/null 2>&1 || stop_ok=0
413+
else
414+
$SUDO docker stop "$DZ_NAME" >/dev/null 2>&1 || stop_ok=0
415+
fi
416+
[ "$stop_ok" = 1 ] || warn "Could not stop the existing container cleanly (timed out or errored); forcing removal. Its GRE tunnel/routes may be orphaned in the host network namespace -- check 'doublezero status' / 'ip link' and disconnect manually if connectivity is off."
417+
fi
418+
$SUDO docker rm -f "$DZ_NAME" >/dev/null 2>&1 || true
419+
420+
# Version-independent confirmation that the tunnel actually came down: the
421+
# container ran with --network host, so a failed disconnect leaves the doublezero1
422+
# interface (and its routes/on-chain session) orphaned in the host netns. Warn
423+
# loudly if it lingers -- the fresh connect below usually recreates it, but a
424+
# leftover old tunnel means the previous session wasn't released.
425+
if command -v ip >/dev/null 2>&1 && $SUDO ip link show doublezero1 >/dev/null 2>&1; then
426+
warn "The DoubleZero tunnel interface (doublezero1) is still present after teardown; the previous instance may not have disconnected cleanly. Its on-chain session/routes could be orphaned -- verify with 'doublezero status'."
427+
fi
428+
}
429+
reinstall_existing_instance
430+
365431
# ----------------------------------------------------------------------------
366432
# 4. host kernel / network prep (host-side; safe to attempt)
367433
# ----------------------------------------------------------------------------

scripts/connect-testnet.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,72 @@ if ! command -v docker >/dev/null 2>&1; then
357357
fi
358358
$SUDO docker info >/dev/null 2>&1 || die "Docker is installed but the daemon isn't reachable. Start it (e.g. 'sudo systemctl start docker') and re-run."
359359

360+
# ----------------------------------------------------------------------------
361+
# 3b. existing instance? reinstall (graceful teardown) or cancel
362+
# ----------------------------------------------------------------------------
363+
# A prior run leaves a long-lived container (--restart unless-stopped) that holds
364+
# the DoubleZero tunnel and the WS sink. Re-running the installer over a live
365+
# instance would collide (same container name, same tunnel, same host ports), so
366+
# detect one up front and tear it down cleanly first.
367+
reinstall_existing_instance() {
368+
$SUDO docker ps -a -q --filter "name=^${DZ_NAME}$" 2>/dev/null | grep -q . || return 0
369+
370+
local running="" existing_env="" existing_img=""
371+
$SUDO docker ps -q --filter "name=^${DZ_NAME}$" 2>/dev/null | grep -q . && running=1
372+
# Label the victim: all three installers share DZ_NAME, so e.g. the testnet
373+
# installer can find a live *mainnet* container -- name the network/image so the
374+
# operator isn't asked to destroy an unidentified instance.
375+
existing_env="$($SUDO docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$DZ_NAME" 2>/dev/null | sed -n 's/^DZ_ENV=//p' | head -1 || true)"
376+
existing_img="$($SUDO docker inspect -f '{{.Config.Image}}' "$DZ_NAME" 2>/dev/null || true)"
377+
warn "An edge-connect instance ('$DZ_NAME'${existing_env:+, env=$existing_env}${existing_img:+, image=$existing_img}) already exists on this host${running:+ and is running}."
378+
379+
# Decide whether to reinstall, keeping the three cases distinct so we neither
380+
# break headless automation nor mislabel a genuine decline:
381+
# DZ_ASSUME_YES=1 -> reinstall (skip the prompt)
382+
# interactive decline -> abort (the operator said no)
383+
# no usable TTY, !yes -> reinstall, but say so (pre-3b behaviour was a silent
384+
# reinstall; keep automation working rather than abort)
385+
# A readable /dev/tty inode (-r) can still fail to OPEN with no controlling
386+
# terminal (cron/systemd/`curl|bash` without a tty), so probe an actual open
387+
# rather than trusting -r (and skip confirm()'s own tty read in that case).
388+
if [ "$DZ_ASSUME_YES" = 1 ]; then
389+
:
390+
elif { : <"$TTY"; } 2>/dev/null; then
391+
confirm "Reinstall? This disconnects and removes the existing instance" \
392+
|| die "Cancelled: leaving the existing instance in place (manage it with 'sudo docker logs $DZ_NAME')."
393+
else
394+
warn "No terminal to prompt on and DZ_ASSUME_YES is unset; reinstalling to preserve non-interactive behaviour (set DZ_ASSUME_YES=1 to silence this, or run interactively to be asked first)."
395+
fi
396+
397+
info "Uninstalling existing instance..."
398+
if [ -n "$running" ]; then
399+
# Graceful: `docker stop` sends SIGTERM, which the container entrypoint traps to
400+
# run a bounded `doublezero disconnect` (only if a tunnel is up) before tearing
401+
# doublezerod down -- releasing the GRE tunnel/routes/on-chain session cleanly.
402+
# The container's --stop-timeout (60s) bounds the stop; the outer `timeout`
403+
# guards a wedged docker CLI / restarting container so we can never hang forever.
404+
info "Stopping it gracefully (disconnecting the DoubleZero tunnel)..."
405+
local stop_ok=1
406+
if command -v timeout >/dev/null 2>&1; then
407+
$SUDO timeout 90 docker stop "$DZ_NAME" >/dev/null 2>&1 || stop_ok=0
408+
else
409+
$SUDO docker stop "$DZ_NAME" >/dev/null 2>&1 || stop_ok=0
410+
fi
411+
[ "$stop_ok" = 1 ] || warn "Could not stop the existing container cleanly (timed out or errored); forcing removal. Its GRE tunnel/routes may be orphaned in the host network namespace -- check 'doublezero status' / 'ip link' and disconnect manually if connectivity is off."
412+
fi
413+
$SUDO docker rm -f "$DZ_NAME" >/dev/null 2>&1 || true
414+
415+
# Version-independent confirmation that the tunnel actually came down: the
416+
# container ran with --network host, so a failed disconnect leaves the doublezero1
417+
# interface (and its routes/on-chain session) orphaned in the host netns. Warn
418+
# loudly if it lingers -- the fresh connect below usually recreates it, but a
419+
# leftover old tunnel means the previous session wasn't released.
420+
if command -v ip >/dev/null 2>&1 && $SUDO ip link show doublezero1 >/dev/null 2>&1; then
421+
warn "The DoubleZero tunnel interface (doublezero1) is still present after teardown; the previous instance may not have disconnected cleanly. Its on-chain session/routes could be orphaned -- verify with 'doublezero status'."
422+
fi
423+
}
424+
reinstall_existing_instance
425+
360426
# ----------------------------------------------------------------------------
361427
# 4. host kernel / network prep (host-side; safe to attempt)
362428
# ----------------------------------------------------------------------------

scripts/connect.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,72 @@ if ! command -v docker >/dev/null 2>&1; then
357357
fi
358358
$SUDO docker info >/dev/null 2>&1 || die "Docker is installed but the daemon isn't reachable. Start it (e.g. 'sudo systemctl start docker') and re-run."
359359

360+
# ----------------------------------------------------------------------------
361+
# 3b. existing instance? reinstall (graceful teardown) or cancel
362+
# ----------------------------------------------------------------------------
363+
# A prior run leaves a long-lived container (--restart unless-stopped) that holds
364+
# the DoubleZero tunnel and the WS sink. Re-running the installer over a live
365+
# instance would collide (same container name, same tunnel, same host ports), so
366+
# detect one up front and tear it down cleanly first.
367+
reinstall_existing_instance() {
368+
$SUDO docker ps -a -q --filter "name=^${DZ_NAME}$" 2>/dev/null | grep -q . || return 0
369+
370+
local running="" existing_env="" existing_img=""
371+
$SUDO docker ps -q --filter "name=^${DZ_NAME}$" 2>/dev/null | grep -q . && running=1
372+
# Label the victim: all three installers share DZ_NAME, so e.g. the testnet
373+
# installer can find a live *mainnet* container -- name the network/image so the
374+
# operator isn't asked to destroy an unidentified instance.
375+
existing_env="$($SUDO docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$DZ_NAME" 2>/dev/null | sed -n 's/^DZ_ENV=//p' | head -1 || true)"
376+
existing_img="$($SUDO docker inspect -f '{{.Config.Image}}' "$DZ_NAME" 2>/dev/null || true)"
377+
warn "An edge-connect instance ('$DZ_NAME'${existing_env:+, env=$existing_env}${existing_img:+, image=$existing_img}) already exists on this host${running:+ and is running}."
378+
379+
# Decide whether to reinstall, keeping the three cases distinct so we neither
380+
# break headless automation nor mislabel a genuine decline:
381+
# DZ_ASSUME_YES=1 -> reinstall (skip the prompt)
382+
# interactive decline -> abort (the operator said no)
383+
# no usable TTY, !yes -> reinstall, but say so (pre-3b behaviour was a silent
384+
# reinstall; keep automation working rather than abort)
385+
# A readable /dev/tty inode (-r) can still fail to OPEN with no controlling
386+
# terminal (cron/systemd/`curl|bash` without a tty), so probe an actual open
387+
# rather than trusting -r (and skip confirm()'s own tty read in that case).
388+
if [ "$DZ_ASSUME_YES" = 1 ]; then
389+
:
390+
elif { : <"$TTY"; } 2>/dev/null; then
391+
confirm "Reinstall? This disconnects and removes the existing instance" \
392+
|| die "Cancelled: leaving the existing instance in place (manage it with 'sudo docker logs $DZ_NAME')."
393+
else
394+
warn "No terminal to prompt on and DZ_ASSUME_YES is unset; reinstalling to preserve non-interactive behaviour (set DZ_ASSUME_YES=1 to silence this, or run interactively to be asked first)."
395+
fi
396+
397+
info "Uninstalling existing instance..."
398+
if [ -n "$running" ]; then
399+
# Graceful: `docker stop` sends SIGTERM, which the container entrypoint traps to
400+
# run a bounded `doublezero disconnect` (only if a tunnel is up) before tearing
401+
# doublezerod down -- releasing the GRE tunnel/routes/on-chain session cleanly.
402+
# The container's --stop-timeout (60s) bounds the stop; the outer `timeout`
403+
# guards a wedged docker CLI / restarting container so we can never hang forever.
404+
info "Stopping it gracefully (disconnecting the DoubleZero tunnel)..."
405+
local stop_ok=1
406+
if command -v timeout >/dev/null 2>&1; then
407+
$SUDO timeout 90 docker stop "$DZ_NAME" >/dev/null 2>&1 || stop_ok=0
408+
else
409+
$SUDO docker stop "$DZ_NAME" >/dev/null 2>&1 || stop_ok=0
410+
fi
411+
[ "$stop_ok" = 1 ] || warn "Could not stop the existing container cleanly (timed out or errored); forcing removal. Its GRE tunnel/routes may be orphaned in the host network namespace -- check 'doublezero status' / 'ip link' and disconnect manually if connectivity is off."
412+
fi
413+
$SUDO docker rm -f "$DZ_NAME" >/dev/null 2>&1 || true
414+
415+
# Version-independent confirmation that the tunnel actually came down: the
416+
# container ran with --network host, so a failed disconnect leaves the doublezero1
417+
# interface (and its routes/on-chain session) orphaned in the host netns. Warn
418+
# loudly if it lingers -- the fresh connect below usually recreates it, but a
419+
# leftover old tunnel means the previous session wasn't released.
420+
if command -v ip >/dev/null 2>&1 && $SUDO ip link show doublezero1 >/dev/null 2>&1; then
421+
warn "The DoubleZero tunnel interface (doublezero1) is still present after teardown; the previous instance may not have disconnected cleanly. Its on-chain session/routes could be orphaned -- verify with 'doublezero status'."
422+
fi
423+
}
424+
reinstall_existing_instance
425+
360426
# ----------------------------------------------------------------------------
361427
# 4. host kernel / network prep (host-side; safe to attempt)
362428
# ----------------------------------------------------------------------------

0 commit comments

Comments
 (0)