Skip to content

fix: send an oversized first operation in a batch - #872

Merged
hln33 merged 5 commits into
mainfrom
fix/871-overflow-oversized-first-op
Oct 28, 2026
Merged

hln33 merged 5 commits into
mainfrom
fix/871-overflow-oversized-first-op

Conversation

@hln33

@hln33 hln33 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Issue #, if available

Fixes #871

Description of changes

A step result larger than max_batch_size_bytes (750 KB) was never sent to CheckpointDurableExecution. The handler thread blocked on the sync SUCCEED checkpoint forever, and the execution hung until the Lambda timeout. The next invocation then ran the step again.

Cause. _drain_overflow called batch.fits(size) on the first operation of an empty batch. An operation above the limit fails fits even in an empty batch. So _drain_overflow returned it to the overflow queue on every pass. _wait_for_first_operation had the opposite rule and accepted an oversized first operation.

Fix. _BatchAccumulator.fits now returns True for an empty batch. So every intake path sends an oversized operation alone, and the service decides whether it is within the per-operation limit. This matches the JS, Java and Rust SDKs, which all add the first queued item regardless of size.

Only the default AT_LEAST_ONCE_PER_RETRY semantics hit the bug: its START checkpoint is async, so the batcher still holds the START batch when the large SUCCEED arrives, and SUCCEED takes the overflow path. AT_MOST_ONCE_PER_RETRY sends START first, so SUCCEED becomes the first operation of a fresh batch and _wait_for_first_operation accepted it.

Tests.

  • Unit (tests/state_test.py): test_collect_checkpoint_batch_sends_oversized_operation_from_overflow and test_drain_overflow_takes_oversized_operation_when_batch_is_empty drive the batcher directly.
  • End to end (tests/e2e/large_step_result_int_test.py): runs the reproduction from [Bug]: Step result larger than 750 KB is never checkpointed; execution hangs #871 (an 800 KB step result) through DurableFunctionTestRunner. It passes in 0.3 s with the fix.

Design

  • Design note (rule 3): not needed. No public API change.
  • Simpler options considered (rule 2): adding not batch.is_empty() and to the condition in _drain_overflow also fixes the bug, but it leaves the "first operation is never refused" rule stated twice (in _drain_overflow and in _wait_for_first_operation). Moving the rule into fits states it once and removes the special case from _wait_for_first_operation's comment. No logic is added beyond the one is_empty() check.
  • Root cause (rule 5): see above. The fix changes fits, where the size decision is made.
  • Net change: +6/−2 source lines, 0 public API members.

Assumptions

  • The service accepts a single STEP output up to 1 MB, so operations between 750 KB and 1 MB are valid and must be sent. Evidence: service configuration (MaxCheckpointPayloadBytes = 1,048,576), and the reporter's reproduction in [Bug]: Step result larger than 750 KB is never checkpointed; execution hangs #871 (745 KB completes, 800 KB hangs).
  • A CheckpointDurableExecution request carrying one operation above 750 KB is accepted by the API as long as the operation is within the per-operation limit. Evidence: the JS, Java and Rust SDKs already send such requests. The new e2e test runs against the local emulator, and the emulator does not check STEP payload size (checkpoint/validators/checkpoint.py _validate_payload_sizes). So acceptance of a 750 KB–1 MB operation by the real service is unverified from this branch.

Decisions not covered by the issue or the design note

  • The regression test sets _checkpointing_stopped before the second _collect_checkpoint_batch() call, so a regression returns [] and fails instead of blocking the test run forever in _wait_for_first_operation.
  • The e2e test keeps the emulated Lambda timeout at its 900 s default. A shorter timeout would re-invoke the handler, and the re-run SUCCEED would be the first operation of a fresh batch, so the test would pass on the code before the fix.
  • On the code before the fix, the e2e test fails with TimeoutError after 30 s, but the pytest process then does not exit on its own (likely the blocked handler thread). A regression in CI would show the failure and then hold the job until the job timeout. I did not add thread cleanup to the test, because that would change the runner's behavior only to shorten a failure.
  • Operations above the service's per-operation limit (1 MB) are now sent and rejected by the service with its payload-size error, instead of hanging. No SDK-side size check was added; the SDK does not know the account's limit.

Checklist

  • Every new branch has a test that fails without it (rule 4): removing if self.is_empty(): return True fails both new tests; removing the size comparison fails test_collect_checkpoint_batch_uses_overflow_queue and test_collect_checkpoint_batch_respects_size_limit.
  • For a bug fix: the first commit adds a test that fails on the current code (rule 5). Both new unit tests fail on main with AssertionError: assert [] == [QueuedOperation(... 'big_step' ...)]. The e2e test (third commit) fails on main with TimeoutError: Execution did not complete within timeout after 30.19 s.
  • Every edited test expectation is listed in the description, with the reason (rule 5): none edited.

Verification: hatch run dev-core:test 2119 passed; hatch fmt --check clean; hatch run dev-core:typecheck no issues.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@hln33
hln33 deployed to ai-pr-review-runtime October 28, 2026 17:57 — with GitHub Actions Active
@hln33
hln33 deployed to ai-pr-review-runtime October 28, 2026 17:58 — with GitHub Actions Active
@github-actions

This comment has been minimized.

yaythomas
yaythomas previously approved these changes Oct 28, 2026

@yaythomas yaythomas left a comment

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.

:shipit:

Can this land as one squashed commit with a conventional commit message, for example the PR title?

Comment thread packages/aws-durable-execution-sdk-python/tests/state_test.py Outdated
hln33 added 5 commits October 28, 2026 11:53
A step result above max_batch_size_bytes (750 KB) that reaches the
overflow queue is never sent. Both tests fail on the current code with
[] == [large_op].

Refs #871
_drain_overflow refused an operation above max_batch_size_bytes even
when the batch was empty, so the operation returned to the overflow
queue on every pass and was never sent. Its sync caller then blocked
forever, and the execution hung until the Lambda timeout.

_wait_for_first_operation already accepted an oversized first
operation. The rule now lives in _BatchAccumulator.fits: an empty batch
accepts an operation of any size. The service enforces the
per-operation limit.

Fixes #871
Runs the #871 reproduction through DurableFunctionTestRunner. The
result is above max_batch_size_bytes (750 KB) and below the service's
1 MB limit. On the code before the fix, run() raises TimeoutError
after 30 s.

Refs #871
CONTRIBUTING asks for return annotations so that mypy checks the
function bodies.
@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

One test cleanup issue at packages/aws-durable-execution-sdk-python/tests/e2e/large_step_result_int_test.py:31. No SDK correctness or rules 1–9 approach violations found. Review used static inspection; tests were not executed, and real-service payload acceptance remains unverified.

Reviewed commit 028501308ec116cf9e4e4e08bd18ccfb3b49cdae. Workflow run

@yaythomas yaythomas left a comment

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.

:shipit:

@hln33
hln33 merged commit 73b9d68 into main Oct 28, 2026
43 checks passed
@hln33
hln33 deleted the fix/871-overflow-oversized-first-op branch October 28, 2026 19:51
@hln33 hln33 mentioned this pull request Oct 28, 2026
3 tasks done

This branch was successfully deployed

1 active deployment
ai-pr-review-runtime — 02850130 Deployed Oct 28, 2026 by hln33 via ai-pr-review / Codex review / Generate Codex review #2583
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.

[Bug]: Step result larger than 750 KB is never checkpointed; execution hangs

2 participants