-
Notifications
You must be signed in to change notification settings - Fork 5
fix(sandbox/k8s): accumulate canonical stdout from polled deltas to avoid mid-stream window after log rotation #1917
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
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 |
|---|---|---|
|
|
@@ -427,6 +427,12 @@ export class KubernetesBackend implements ExecutionBackend { | |
| let lastLogLen = 0; | ||
| let logShrunk = false; | ||
| let loggedPollError = false; | ||
| // Spawner-side canonical stdout buffer: accumulated from polled deltas | ||
| // so that a kubelet log rotation never gives us a mid-stream window. | ||
| // After rotation logs.length resets to 0; a final readPodLog would then | ||
| // return only the new file's content — the head of the original stream | ||
| // is gone. By accumulating here we always retain the deterministic head. | ||
| let logBuf = ''; | ||
| const pollRunnerStdout = async (): Promise<void> => { | ||
| let logs: string; | ||
| try { | ||
|
|
@@ -446,12 +452,18 @@ export class KubernetesBackend implements ExecutionBackend { | |
| return; | ||
| } | ||
| if (logs.length > lastLogLen) { | ||
| scanner.onStdoutChunk?.(Buffer.from(logs.slice(lastLogLen), 'utf8')); | ||
| const delta = logs.slice(lastLogLen); | ||
| scanner.onStdoutChunk?.(Buffer.from(delta, 'utf8')); | ||
| lastLogLen = logs.length; | ||
| if (Buffer.byteLength(logBuf, 'utf8') < cfg.stdoutMaxBytes) { | ||
| ({ text: logBuf } = capText(logBuf + delta, cfg.stdoutMaxBytes)); | ||
| } | ||
| } else if (logs.length < lastLogLen) { | ||
| // The kubelet rotated the container log out from under us — the | ||
| // canonical head is gone, so the final read is a partial window. | ||
| // Kubelet rotated the container log out from under us — the | ||
| // canonical head is already captured in logBuf. Reset the length | ||
| // cursor so we continue accumulating from the new file's beginning. | ||
| logShrunk = true; | ||
| lastLogLen = 0; | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -540,19 +552,14 @@ export class KubernetesBackend implements ExecutionBackend { | |
| await sleep(POLL_INTERVAL_MS, opts.signal); | ||
| } | ||
|
|
||
| // Final stdout read (the runner may have emitted more between the last | ||
| // poll and exit) → feed the residual to the scanner, then drain it. The | ||
| // canonical buffer is the full (capped) runner log. | ||
| // Final stdout poll (the runner may have emitted more between the last | ||
| // loop iteration and exit) → feed the residual to the scanner, then | ||
| // drain it. Use the spawner-side accumulation (logBuf) as the canonical | ||
| // stdout — it always holds the deterministic head of the stream even | ||
| // across kubelet log rotations. | ||
| await pollRunnerStdout(); | ||
| scanner.finalize(); | ||
| let stdout = ''; | ||
| try { | ||
| stdout = await readPodLog(this.client, podName, 'runner', { | ||
| limitBytes: cfg.stdoutMaxBytes, | ||
| }); | ||
| } catch (err) { | ||
| console.warn('[sandbox.k8s] final runner log read failed:', err); | ||
| } | ||
| const stdout = logBuf; | ||
|
Comment on lines
+555
to
+562
Contributor
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. Handle final-iteration log shrink with one extra read before finalize. If the final poll only observes 💡 Suggested patch await pollRunnerStdout();
+ if (
+ logShrunk &&
+ Buffer.byteLength(logBuf, 'utf8') < cfg.stdoutMaxBytes
+ ) {
+ // If shrink is first detected in the final poll, do one extra read so
+ // we can capture the new file head before finalizing.
+ await pollRunnerStdout();
+ }
scanner.finalize();
const stdout = logBuf;🤖 Prompt for AI Agents |
||
| const stdoutStreamTruncated = | ||
| Buffer.byteLength(stdout, 'utf8') >= cfg.stdoutMaxBytes || logShrunk; | ||
|
|
||
|
|
||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: tale-project/tale
Length of output: 2253
🏁 Script executed:
Repository: tale-project/tale
Length of output: 149
🏁 Script executed:
Repository: tale-project/tale
Length of output: 884
🏁 Script executed:
Repository: tale-project/tale
Length of output: 1529
🏁 Script executed:
Repository: tale-project/tale
Length of output: 890
🏁 Script executed:
Repository: tale-project/tale
Length of output: 2159
🏁 Script executed:
Repository: tale-project/tale
Length of output: 1837
🏁 Script executed:
Repository: tale-project/tale
Length of output: 280
🏁 Script executed:
Repository: tale-project/tale
Length of output: 234
🏁 Script executed:
Repository: tale-project/tale
Length of output: 1962
🏁 Script executed:
Repository: tale-project/tale
Length of output: 722
Remove type assertions from the test client fixture.
The inlined client object at lines 372-403 uses
as V1Pod(twice) andas unknown as CoreV1Apiassertions, which violate the coding guideline:**/*.{ts,tsx}— "Neveras, neverany, neverunknown".Instead of inlining the client, use the existing
stubClient()helper with typed fixtures. This pattern is already used throughout the file and keeps test stubs assertion-free:Refactor to stubClient with typed fixtures
🤖 Prompt for AI Agents
Source: Coding guidelines