Skip to content

fix(proxy): route every container-creating path through the stage-3c bridge - #169

Merged
mhenrixon merged 4 commits into
mainfrom
issue-168-reboot-bridge
Sep 12, 2026
Merged

mhenrixon merged 4 commits into
mainfrom
issue-168-reboot-bridge

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

docker run --volume dash-loadbalancer-config:... auto-creates the named volume, empty, when it does not exist. Three paths reached that docker run without ever running the stage-3c bridge first:

  • Dash::Cli::Proxy::Reboot#rundash proxy reboot, and the drift-detected reboot inside dash proxy boot
  • Dash::Cli::Proxy::LoadbalancerReboot#run — same, for the loadbalancer host
  • Dash::Cli::Proxy#loadbalancer("start")start_or_run falls through to docker run on a host with no container

So dash proxy reboot against a dedicated load-balancer host that has never been through dash proxy boot creates dash-loadbalancer-config before the bridge has had any chance to copy kamal-loadbalancer-config's routing table, dynamic domains and ACME cache into it. The next dash proxy boot finds the new volume already there and skips the copy for good via copy_legacy_config_volume's own guard — silently, every deploy. Since #167 that state also writes .legacy-renamed, so recovery needs the marker removed as well as the volume fixed.

Fixed at the source, as the issue asks, rather than by making the marker's heuristic smarter: every path that can create the new container, volume or network now runs prepare_boot (the bridge + the apps-config mkdir) first.

Closes #168

Round trips

On both reboot paths this is round-trip neutral: they already executed ensure_apps_config_directory as a round trip of its own, and prepare_boot is legacy_rename && ensure_apps_config_directory — the same fold #167 gave boot. A migrated or fresh-on-4.x host therefore runs one test -f more than before and nothing else.

dash proxy loadbalancer start does gain one round trip; it had no apps-config mkdir to fold into. It also stops leaving that bind-mount source directory for docker to create root-owned.

Placement

  • Reboot: after pull_image — the volume copy borrows the proxy image, and the pull is the cheapest guarantee it is local — and before stop_and_replace, so the bridge decides before the running proxy comes down.
  • LoadbalancerReboot: after ensure_network — the bridge's first step attaches the legacy network's containers to dash, which has to exist — and before the stop, matching the proxy bridge's own documented order (copy the volume before replacing the container).

Test plan

  • reboot routes a host through the stage-3c bridge before replacing the container — bridge lands before both the stop and the docker run
  • reboot carries the bridge in the apps-config round trip it already paid — asserts the fold, so a future split shows up as a failure
  • reboot routes the loadbalancer host through the stage-3c bridge before creating the container — network create < bridge < docker run --name load-balancer
  • reboot carries the loadbalancer bridge in the apps-config round trip it already paid
  • loadbalancer start routes through the stage-3c bridge before it can run
  • bundle exec rubocop --parallel — clean
  • Unit suite — 1926 runs, 0 failures
  • bin/test (integration) — running locally at the time of opening; CI's bin/test job is the gate

Deviations & judgment calls

  • The issue's round-trip claim was wrong, in our favour. It says routing reboot through the bridge "adds a round trip to reboot that it doesn't pay today". It does not — both reboot paths already spent one on ensure_apps_config_directory, which prepare_boot carries. Swapping one for the other is free on those paths.
  • Scope extended by one call site. The issue names only the two reboot classes. dash proxy loadbalancer start reaches start_or_run, which is the identical volume-creating path; leaving it would have left the marker foolable by exactly the mechanism this closes. That one does cost a round trip.
  • Rewrote the Dash::Commands::Loadbalancer#mark_legacy_renamed comment rather than leaving it — it documented this gap as open and named Loadbalancer reboot never routes through the stage-3c bridge, so it can create the new volume before it is copied #168 as the tracking issue. It now says which paths run the bridge, and keeps the manual recovery note for a host that got into that state some other way (an operator's own docker run, a volume created by hand).
  • Nothing about the marker's heuristic changed, per the issue's out-of-scope note. The verification is still "the new volume exists, or there was never a legacy one".

Summary by cubic

Fixes the stage-3c bridge being skipped on three container-creating paths so dash-loadbalancer-config is never created empty before the legacy config is copied.

  • dash proxy reboot, the loadbalancer reboot, and dash proxy loadbalancer start now run prepare_boot before anything can create the new container or volume.
  • Reboot paths stay round-trip neutral because they already paid for the apps-config mkdir; loadbalancer start gains one round trip but no longer leaves a root-owned mount source.
  • Documented at the shared copy why copying the legacy volume while it's still mounted is safe: every writer renames into place, so cp -a reads a complete file either way.
  • Fixed two seed-dependent test flakes: suite verbosity is pinned to :info so a --quiet test can't silence SSHKit output for later tests, and the proxy boot round-trip pin is now per host since hosts run in parallel.

Closes #168.

Written for commit d5945bc. Summary will update on new commits.

Review in cubic

…bridge

`Dash::Cli::Proxy::Reboot`, `Dash::Cli::Proxy::LoadbalancerReboot` and
`dash proxy loadbalancer start` all create the renamed container without ever
running the stage-3c bridge. `docker run --volume dash-loadbalancer-config:...`
auto-creates the named volume empty when it does not exist, so a `dash proxy
reboot` against a host that has never been through `dash proxy boot` brings the
new volume into existence before the bridge has had any chance to copy the
legacy routing table and ACME cache into it.

The next boot then finds the new volume already there and skips the copy for
good, via `copy_legacy_config_volume`'s own guard - silently. Since #167 that
state also writes the `.legacy-renamed` marker, so recovery needs the marker
deleted as well as the volume fixed.

Fix it at the source rather than making the marker's heuristic smarter: every
path that can create the container, the volume or the network now runs
`prepare_boot` first. On both reboot paths this is round-trip neutral - they
already spent a round trip on `ensure_apps_config_directory`, which
`prepare_boot` carries.

Refs #168

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread lib/dash/cli/proxy/loadbalancer_reboot.rb
Comment thread lib/dash/cli/proxy/reboot.rb
@mhenrixon mhenrixon self-assigned this Sep 12, 2026
@mhenrixon mhenrixon added enhancement New feature or request performance labels Sep 12, 2026
…is live

Both cubic and a human reader will ask whether `cp -a` over a volume the
legacy container still mounts can capture a half-written routing table or
certificate. It cannot: dash-proxy renames into place on every writer - the
routing table via writeFileAtomic, the dynamic domain and redirect state via
their own temp + rename, the response cache via CreateTemp + Rename, and the
ACME cache via autocert.DirCache.

Written at the shared copy rather than at one caller, since `boot` and both
reboots all reach it.

Refs #168

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

… output

`dash app stale_containers --quiet` in test/cli/app_test.rb leaves :error on both
the DASH singleton and SSHKit's global output_verbosity:
Cli::Base#initialize_commander sets the commander's verbosity and
Commander#configure_sshkit_with mirrors it into SSHKit. Nothing restores either
between tests — Commander#reset would, but only `dash alias` calls it — so from
that point on every SSHKit.config.output.info in the process is dropped.

Whether that mattered depended on the seed. CI run 34709935073 put the quiet
test ahead of test/cli/healthcheck/progress_reporter_test.rb on Ruby 3.2 (seed
36230) and three of its assertions saw "", while Ruby 3.3, 3.4 and 4.0 drew
seeds that passed the same commit. Reproduced locally with
`bin/test --seed 36230`, three failures, same three tests.

Pin both to :info in the suite's global setup, beside the Docker pins that
answer the same class of problem — a test that wants another verbosity still
sets it itself.

Refs #166

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

…t order

`on` runs the proxy hosts in parallel threads, and the recorder behind
"boot issues no round trip beyond the pinned per-host sequence" appended from
both. The pin then spelled out host 1's sequence followed by host 2's, which
held only while the two threads happened not to overlap. CI seed 59404
interleaved them (login, bridge, login, bridge, inspect, ...) - same commands,
same count per host, different scheduling - and the test failed on a run that
issued exactly what it pins. It reproduces standalone here too: 1 in 150.

Tag every recorded round trip with the host it went to (the Printer command
carries it; a capture reads it off SSHKit::Backend.current, the thread-local
the backend sets for its run) and assert each host's own sequence. That is
the claim the test was making - the count and the order the gem chooses -
minus the one it never meant to: which thread the scheduler ran first.

0 in 300 after.

Refs #167

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

@mhenrixon
mhenrixon merged commit 68f8843 into main Sep 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Loadbalancer reboot never routes through the stage-3c bridge, so it can create the new volume before it is copied

1 participant