[e2e] Add a checkpoint-recovery test for restored Python state - #992
Open
weiqingy wants to merge 3 commits into
Open
[e2e] Add a checkpoint-recovery test for restored Python state#992weiqingy wants to merge 3 commits into
weiqingy wants to merge 3 commits into
Conversation
Adds the deterministic agent and job submitted by the standalone-cluster checkpoint-recovery test, plus unit tests for its two pure predicates. The agent drives the built-in chat-model tool-call flow with a mock model that always emits a tool call, and blocks inside the tool on a filesystem handshake. Blocking there holds the tool-call context in keyed state: sensory memory is flushed to state when each action task completes and is cleared only when the whole run finishes, so parking mid-run is what lets a checkpoint capture it. Whether a restored value came from the checkpoint or from a fresh write in the same run cannot be answered from checkpointed state, since any marker written to state is itself restored. The agent therefore counts writes in a module-level variable. Pemja runs with ExecType.MULTI_THREAD and a process singleton MainInterpreter, so that counter survives a task-level restart and is reset only by process death. A swallowed tool exception is invisible to the assertions, so the tool returns a sentinel on release that the transcript check requires. The sentinel shares no substring with the tool's own name, because the framework's failure text embeds that name. The modules are named to avoid two collection traps: *_example.py would be auto-submitted by the example-submission suite, and *_test.py would be collected by pytest. Generated-by: Claude Code 2.1.224
Drives the recovery test: configures checkpointing, starts a standalone cluster, submits the recovery job, waits until the agent run parks inside the tool, confirms a checkpoint captured the parked state, kills the TaskManager, restarts it, confirms the restore, releases the handshake and asserts the job's verdict. Checkpointing is not enabled anywhere in this repo's existing setup, so the script appends the configuration itself. The append is idempotent because a repeated flat key is a hard YAML parse failure that prevents the cluster from starting, and install.sh reuses an already-extracted Flink home. The interval is read back from the job's own checkpoint config rather than the cluster config, which echoes unrecognized keys and so cannot show that a key took effect. Waits are deadline-based and each one distinguishes three outcomes: the probe field never parsed, the probe parsed but the target did not, and the target parsed with the condition false. Only the third is evidence about the condition. The handshake budget measures elapsed time rather than summing nominal timeouts, and each step draws the smaller of its ceiling and the time actually left, so an unmodelled cost reduces later grants instead of overrunning the tool's own deadline. Verdicts come from the job's single verdict field, never re-derived here, and waits match marker filenames exactly, since every file the job writes has a transient .tmp twin that a crash can leave behind. The bats suite exercises the sourceable functions: JSON extraction against malformed and empty bodies, the three wait outcomes, config append and revert, the cleanup decision table, and the budget arithmetic against the configuration that failed it. The Flink distribution is installed by sourcing install.sh's download and extraction helpers rather than running install.sh as a whole program. The whole-program path also installs a released flink-agents JAR, and no such JAR is published for every Flink version this checkout can build against, so that path fails outright on the version the nightly job pins. The test has to run the artifacts built from this checkout regardless, so nothing is lost by stopping before that step and installing the built wheel instead. Generated-by: Claude Code 2.1.224
Adds a checkpoint-recovery job alongside the existing example-submission one. It installs Flink, builds the checkout, and runs the recovery script, which kills the TaskManager mid-run and asserts the agent's memory survived. The job is guarded to scheduled and manual runs. The workflow's pull_request trigger exists for the examples job, and extending it here would spend a second full download-and-build on every PR matching those paths, with nothing in this repository caching either. The accepted consequence is that this test produces no pre-merge signal, so a path filter would buy nothing and none is added. The timeout is 75 minutes rather than the neighbouring job's 60. The recovery sequence runs last, so a ceiling that truncates the run truncates the part under test. The budget comment records where the time goes, including that the Flink download alone can take about 60 of those minutes if every curl retry runs to its deadline. The job carries no model-server configuration: the payload answers from a deterministic mock chat model rather than a real one, so a flaky or slow model cannot turn a recovery failure into a green run. Generated-by: Claude Code 2.1.224
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue: #836
Purpose of change
No test covers Python agent memory surviving the loss of a TaskManager process. Existing e2e suites run on MiniCluster, which never recreates the embedded Python interpreter, so a value Pemja cannot materialize into a JVM type fails only on a real restore. This test hard-kills a real TaskManager and asserts the tool-call context and a raw
bytesvalue both survived.Runtime flow
tool-enteredand blocks onrelease.tool-enteredit waits for two further completed checkpoints, records the pid, resource id and restore count, SIGKILLs the TaskManager, starts a replacement, waits for a restore, then writesrelease.verdict.json, and raises on mismatch.Key decisions
The source must be unbounded. A bounded one raises end-of-input right after the record, and
ActionExecutionOperator.endInput()then holds the mailbox thread while the run is parked, so no checkpoint completes. Measured on the pinned stack: unbounded 16 completions in 15s, bounded 0.Provenance rests on module-level counters outside Flink state, since anything checkpointed is restored with the payload and cannot tell a restored read from the reader's own write. Pemja's singleton interpreter keeps globals across an in-place task restart, so zero counters prove a different OS process.
counts.restoredrather thannumRestarts, which counts a restart that restored nothing.Implementation Description
Behavioral contracts
verdict: "pass"; a run recording no assertion exits non-zero.counts.restoredrises above its pre-kill baseline.latest.restoredis non-null. If 4 and 5 disagree the run fails rather than picking one.bytesorbytearraywith the exact content; a list of the same ints fails.0.3.dev0,flink_agents.apiunder its path.Failure behavior
A missing
handshake_dirorverdict_dirraisesValueError. Checkpointing off or a wrong interval records a FAIL and exits non-zero, and an infeasible budget exits 1.An unreachable, slow, or wrongly shaped REST response never reads as a false condition. Requests are bounded by
CURL_MAX_TIME; an empty body, bad JSON, a missing path, or a wrongly typed leaf all fail the read.One path degrades deliberately. The framework absorbs a tool exception into a
... execute failed.response rather than failing the action, so the tool returns a sentinel the assertion requires. The agent publishes the verdict before raisingAssertionError, so a failed run leaves a readable file.Tests
assert_verdict, 3print_summaryassert_checkpointing_enabled_blob_matches, 9 casesassert_handshake_budget, 3charged_timeout, 1handshake_budget_leftconfigure_flink/delete_config_keyawait_releaseAbout 45 of the 78 bats tests pin harness plumbing the contracts above deliberately do not cover: REST reads, file waits, cluster and venv setup, and EXIT-trap cleanup.
The 78 bats and 12 Python tests run pre-merge; the cluster test does not, since its job is
scheduleandworkflow_dispatchonly. Fork runs are the only evidence, and the controls are what make the pass meaningful.Happy path: 9/9 assertions, verdict
pass, pid 4656 to 6034,counts.completed1 to 3 while parked, both counters 0, blob back asbytes.Negative control, release marker pre-created: failed at
checkpoint-containment, completed stuck at 1.Provenance control, kill skipped: failed at
restore,counts.restoredstayed 0, pid unchanged.It does not reproduce the SIGSEGV that motivated the issue: the memory value validator now raises
TypeErroratset()before Pemja is reached, so that control no longer exists. Unit tests pin which types the validator admits; only this one shows an admitted type survives a restore.Known limitations: timeouts are sized from observed runs, not proven bounds; and 11 pre-existing bats assertions are vacuous under bash 3.2 bodies, since
errexitdoes not apply to[[ ]].API
No public API change, and no product code touched. Six of the seven files are new; the seventh adds a job to the nightly workflow, leaving its existing job, triggers and paths filter untouched. Nothing changes for a caller who does nothing differently.
Documentation
doc-neededdoc-not-neededdoc-includedWas this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code 2.1.224