Skip to content

fix: Kubernetes launcher - Read a container log within a budget, and give up quietly - #417

Merged
yuechao-qin merged 1 commit into
masterfrom
bound-log-upload-retries
Nov 5, 2026
Merged

yuechao-qin merged 1 commit into
masterfrom
bound-log-upload-retries

Conversation

@yuechao-qin

@yuechao-qin yuechao-qin commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

What

Both Kubernetes launchers now read a container log within their own budget, and give up quietly
when it runs out instead of letting the failure propagate into the caller's retry.

Per log that cannot be read Before After
Requests to the API server 5 1
Waits between them 5 × 1s 0
Worst case held by one lost log ~55s ~10s

10s is the launcher's existing request_timeout, which already bounds a single read.

Why

Reading a container log goes through the API server's proxy to the node's kubelet. A read that
fails has almost always lost the log for good: the node is gone or going, and the kubelet that
held the log went with it. OrchestratorService_Sql calls upload_log inside its own
five-attempt _retry, on a single-threaded loop — so every lost log costs that loop five
requests and five waits for a log that will never arrive.

Measured on one deployment over 72 hours: 323 failing log uploads produced 1723 attempt log
lines — 1723 / 323 = 5.33, the five attempts. Every failure in the window was the same
unreachable kubelet; the only other errors on that read path were 9 deleted Pods and one Pod
still initialising. Queries and raw figures are in
Shopify/oasis-backend#827, an internal
repository.

Key decisions

The budget wraps the log read only. The write to storage stays outside it and still raises,
so the caller's five retries continue to cover a failed upload — unlike a read from a dead node,
a failed write is worth retrying.

Giving up returns rather than raises. The caller catches Exception around upload_log, so
a launcher that raised after exhausting its own budget would simply have it applied five times.
Nothing is written on that path, so a log stored by an earlier attempt survives.

Nothing outside this file changes. The budget is the default arguments of one private
helper, so there is no class or base-class attribute for another launcher to inherit by
accident, and both call sites read _read_log_within_budget(self.get_log).

This bounds how long a failing read may hold the caller. What makes an unreachable kubelet stop
being an error at all is the companion change, #418;
the two are independent and can land in either order.

Verification

pytest --ignore=tests/instrumentation -q → 445 passed, black clean.

The tests build both launched containers through their real constructors and raise a real
ApiException carrying the body an unreachable kubelet actually returns, so the exception the
budget catches is one the Kubernetes client raised. One case drives the caller's real _retry
and asserts 1 request and 0 sleeps, against 5 and 5 for the same failure raised. Removing the
give-up return turns 7 of them red.

@yuechao-qin
yuechao-qin requested a review from a team October 30, 2026 04:14
@yuechao-qin
yuechao-qin requested a review from Ark-kun as a code owner October 30, 2026 04:14
@yuechao-qin
yuechao-qin force-pushed the bound-log-upload-retries branch 3 times, most recently from c1ff231 to 4fed669 Compare October 30, 2026 11:18
@yuechao-qin yuechao-qin changed the title fix: Orchestrator - Stop lost container logs from stalling the loop fix: Orchestrator - Let a launcher bound how long a log upload may block the loop Oct 30, 2026
@Ark-kun

Ark-kun commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Let's try a slightly different approach and move the configuration and the retries themselves to the launcher (Kubernetes launcher).

@yuechao-qin
yuechao-qin force-pushed the bound-log-upload-retries branch from 4fed669 to b52e714 Compare November 4, 2026 00:42
@yuechao-qin yuechao-qin changed the title fix: Orchestrator - Let a launcher bound how long a log upload may block the loop fix: Kubernetes launcher - Read a container log within a budget, and give up quietly Nov 4, 2026
@yuechao-qin
yuechao-qin force-pushed the bound-log-upload-retries branch from b52e714 to 24a1599 Compare November 4, 2026 02:59
…give up quietly

Reading a container log goes through the API server's proxy to the node's
kubelet, and a read that fails has almost always lost the log for good. The
caller retries `upload_log` five times, so each lost log cost its single
loop thread five requests and five waits for a log that would never arrive.

Both Kubernetes launchers now read within their own budget and return quietly
when it runs out. The budget wraps the read only: a failed write to storage
still raises and still earns the caller's retries.
@yuechao-qin
yuechao-qin force-pushed the bound-log-upload-retries branch from 24a1599 to 4e8d861 Compare November 4, 2026 04:23
Comment on lines +809 to +811
max_retries: int = _LOG_READ_MAX_RETRIES,
wait_seconds: float = _LOG_READ_WAIT_SECONDS,
max_total_seconds: float | None = _LOG_READ_MAX_TOTAL_SECONDS,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note the these are essentially unconfigurable (this is a private function that is always called without these optional parameters).

# Disabled stream="All" due to error in some new GKE clusters
# HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"PodLogOptions \"task-pod-xxxxx\" is invalid: stream: Forbidden: may not be specified","reason":"Invalid","details":{"name":"task-pod-xxxxx","kind":"PodLogOptions","causes":[{"reason":"FieldValueForbidden","message":"Forbidden: may not be specified","field":"stream"}]},"code":422}
# stream="All",
_request_timeout=launcher._request_timeout,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note that this log reading timeout is currently same as other request timeouts and is not affected by any of the new log timeout configurations.

@yuechao-qin
yuechao-qin merged commit b497595 into master Nov 5, 2026
8 checks passed
@yuechao-qin
yuechao-qin deleted the bound-log-upload-retries branch November 5, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants