-
-
Notifications
You must be signed in to change notification settings - Fork 0
perf(boot): wait for readiness on the host and take the container id from docker run #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Turns the server-side readiness wait's progress lines back into the beacon the | ||
| # client-side poll used to print. It is an SSHKit interaction handler, so it sees the | ||
| # wait's stderr as it streams — the operator gets the same once-a-second feedback they | ||
| # got when the laptop was the thing doing the polling, for one round trip instead of one | ||
| # per attempt. The cadence is fixed at a second because the host loop's is. | ||
| # | ||
| # The stream is line-oriented but arrives in chunks (the SSH backend splits on packet | ||
| # boundaries, not newlines), so data is buffered and only whole lines are reported. The | ||
| # line format is the only filter: the wait's stdout carries the final status, and the | ||
| # host's own noise is none of this class's business. | ||
| class Dash::Cli::Healthcheck::ProgressReporter | ||
| LINE = /\A#{Regexp.escape(Dash::Commands::Base::READINESS_PROGRESS_PREFIX)} (?<elapsed>\d+) (?<left>\d+)(?: |\z)/ | ||
|
|
||
| def initialize | ||
| @buffer = +"" | ||
| @mutex = Mutex.new | ||
| end | ||
|
|
||
| # SSHKit's interaction-handler contract. | ||
| def on_data(_command, _stream_name, data, _channel = nil) | ||
| @mutex.synchronize do | ||
| @buffer << data.to_s | ||
| while (newline = @buffer.index("\n")) | ||
| report @buffer.slice!(0..newline).chomp | ||
| end | ||
| end | ||
| end | ||
|
|
||
| private | ||
| def report(line) | ||
| match = LINE.match(line) or return | ||
|
|
||
| SSHKit.config.output.info "Container not ready yet, retrying in 1s (#{match[:elapsed]}s elapsed, #{match[:left]}s left)" | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,27 @@ def health_probe(version:) | |
| docker :exec, container_name(version), *shell([ role.healthcheck.exec ]) | ||
| end | ||
|
|
||
| # Waits on the host for the container to reach a status the poller accepts, so a boot | ||
| # pays one round trip for the wait however long the container takes to come up - the | ||
| # client-side poll paid one per attempt. Exits 0 with that status on stdout the moment | ||
| # it sees one of READY_STATUSES; otherwise it reports progress on stderr once a second | ||
| # and, at the deadline, prints the last status it saw and exits non-zero. Waiting through | ||
| # every other status is deliberate: docker reports a container `unhealthy` after three | ||
| # failed probes, which for an app slower than that is a state it recovers from. | ||
| def wait_for_ready(version:, timeout:) | ||
| shell [ | ||
| "started=$(date +%s);", | ||
| "while true; do", | ||
| *readiness_probe(version: version), | ||
| "case \"$status\" in #{READY_STATUSES.join("|")}) echo \"$status\"; exit 0;; esac;", | ||
| "elapsed=$(( $(date +%s) - started ));", | ||
| "if [ \"$elapsed\" -ge #{timeout.to_i} ]; then echo \"$status\"; exit 1; fi;", | ||
| "echo \"#{READINESS_PROGRESS_PREFIX} $elapsed $(( #{timeout.to_i} - elapsed )) $status\" 1>&2;", | ||
| "sleep 1;", | ||
| "done" | ||
| ] | ||
| end | ||
|
|
||
| def stop(version: nil) | ||
| pipe \ | ||
| version ? container_id_for_version(version) : current_running_container_id, | ||
|
|
@@ -120,6 +141,17 @@ def ensure_env_directory | |
| end | ||
|
|
||
| private | ||
| # The same two readiness sources #status and #health_probe cover, read into `$status` | ||
| # so the loop around them is the same either way. Both swallow their own stderr: the | ||
| # wait's stderr is the progress channel, and nothing else may appear on it. | ||
| def readiness_probe(version:) | ||
| if role.healthcheck&.exec? | ||
| [ "if", *health_probe(version: version), ">/dev/null 2>&1;", "then status=healthy;", "else status=\"#{EXEC_PROBE_FAILED}\";", "fi;" ] | ||
| else | ||
| [ "status=$({", *status(version: version), ";} 2>/dev/null);" ] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When Docker or the inspected container is unavailable, this redirection turns a command failure into an empty readiness status and silently waits until Prompt for AI agents |
||
| end | ||
| end | ||
|
|
||
| def latest_image_id | ||
| docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'" | ||
| end | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: When stdout’s final status arrives between chunks of a stderr progress line, this shared buffer concatenates the separate streams and drops that progress beacon. Ignore non-
:stderrcallbacks before appending, or maintain one buffer per stream.Prompt for AI agents