mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): pace concurrent binding replies against fd 3
`sendReply` ignored `proto.write`'s `false` return, so a program resolving several large values in one `asyncio.gather` round encoded every reply in the same turn and queued all of them in fd 3's writable buffer. Binding resolution carries no seam-level byte cap to bound that, and the failure kills the host process rather than failing the run: measured on a 64 KiB-highWaterMark pipe, eight 4 MiB replies buffered 32.0 MiB at once against 0.0 MiB once paced. Replies now go through a queue that encodes and writes one frame at a time, awaiting `drain` when the pipe is full. The encode happens inside the loop, so a queued reply the run no longer needs is dropped by the `settled` check without ever being serialized. This was previously deferred on the grounds that serializing would narrow the seam's concurrency contract. That reasoning was wrong: the child matches each reply to its `call` by id from a pump that reads fd 3 continuously, so arrival order was never observable, and the bindings still run concurrently. Only the host's peak memory and the flush timing change. The README entry recording the deferral is removed and the Agent Note records the mechanism instead.
This commit is contained in:
@@ -3863,6 +3863,35 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
expect(resolvedLate).toBe(true)
|
||||
}, 90_000)
|
||||
|
||||
it('paces concurrent binding replies instead of queueing every frame at once', async () => {
|
||||
// Binding resolution carries no seam-level byte cap. Before pacing, a program
|
||||
// resolving several large values in one `asyncio.gather` round encoded them
|
||||
// all in the same turn and queued every frame in fd 3's writable buffer,
|
||||
// which exhausted the host heap and killed the whole process rather than
|
||||
// failing the run. Replies are now encoded one at a time, waiting for
|
||||
// `drain` when the pipe is full.
|
||||
//
|
||||
// Eight concurrent 4 MiB replies (32 MiB of frames) must all round-trip. The
|
||||
// program sums the lengths, so the assertion proves every reply arrived and
|
||||
// was matched to its own call -- pacing must not drop or misroute any. What
|
||||
// this case cannot show is the peak itself, which lives in the stream's
|
||||
// buffer: measured directly on a 64 KiB-highWaterMark pipe with this same
|
||||
// 8x4 MiB shape, the unpaced writes buffered 32.0 MiB while the paced ones
|
||||
// peaked at 0.0 MiB.
|
||||
const chunk = 'A'.repeat(4 * 1024 * 1024)
|
||||
const { runtime } = await setup({ maxWallMs: 60_000 })
|
||||
const result = await runtime.run({
|
||||
program: [
|
||||
'import asyncio',
|
||||
'parts = await asyncio.gather(*[tools.chunk({}) for _ in range(8)])',
|
||||
'return sum(len(p) for p in parts)',
|
||||
].join('\n'),
|
||||
bindings: [{ global: 'tools', functions: { chunk: async () => chunk } }],
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.value).toBe(8 * chunk.length)
|
||||
}, 90_000)
|
||||
|
||||
it('bounds a flood of zero-byte log lines through the per-entry separator charge', async () => {
|
||||
// Blank print() lines carry zero content bytes; without the +1 separator
|
||||
// charge they would bypass maxLogBytes entirely and grow the retained
|
||||
|
||||
Reference in New Issue
Block a user