Skip to content

test(amber): add specs for the checkpoint and ECM promise handlers - #7118

Open
aglinxinyuan wants to merge 1 commit into
apache:mainfrom
aglinxinyuan:test-checkpoint-handlers
Open

test(amber): add specs for the checkpoint and ECM promise handlers#7118
aglinxinyuan wants to merge 1 commit into
apache:mainfrom
aglinxinyuan:test-checkpoint-handlers

Conversation

@aglinxinyuan

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

The checkpoint / ECM promise handlers were the engine's lowest-coverage cluster:

Handler Before New spec
TakeGlobalCheckpointHandler 4.5% TakeGlobalCheckpointHandlerSpec (6 tests)
FinalizeCheckpointHandler 4.5% FinalizeCheckpointHandlerSpec (5)
PrepareCheckpointHandler 4.8% PrepareCheckpointHandlerSpec (4)
EmbeddedControlMessageHandler 8.3% EmbeddedControlMessageHandlerSpec (6)
EvaluatePythonExpressionHandler 8.3% EvaluatePythonExpressionHandlerSpec (6)

All five follow the existing precedent in this layer (RetrieveWorkflowStateHandlerSpec): a real CoordinatorProcessor plus a second handler initializer, capturing what the output handler emits — so the tests drive the real handler methods, not a re-implementation. Storage uses ram:// in-memory VFS per the LoggingSpec / DPThreadSpec precedent, with a distinct folder per case since VFS.getManager is a JVM-wide singleton.

Covered: the single-physical-op guard; the already-taken short-circuit that downgrades a checkpoint to an estimate; the per-checkpoint subfolder that WorkflowActor.setupReplay later resolves; channel scoping and the "latest execution" worker selection; the estimation branches on both worker handlers; the CheckpointSupport hand-off and the observable consequence of its skip branch; and the fan-in that must not answer before every targeted worker has replied.

Assertion strength was measured, not asserted. Four rounds of production mutation, 21 of 22 non-trivial cases shown to go red, every mutation then reverted (git diff -- amber/src/main is empty):

Mutation Result
physicalOps.size != 1> 2 2 tests fail
msg.targetOps.flatMapmsg.scope.flatMap 1
if (!msg.estimationOnly) → inverted 3
findLastfind (latest execution) 1
channel-scope filter && → `
markAsReplayDestination hoisted before collect 1
drop the already-taken short-circuit 1
checkpoints.contains!contains 5
fan-in → fire-and-forget 1
…and 11 more

Also removes the two commented-out blocks in CheckpointSpec (−73 lines), verified dead rather than assumed: the first matches OpExecInitInfoWithCode / OpExecInitInfoWithFunc, names that a repo-wide grep finds only inside those comment lines (OpExecInitInfo is now a ScalaPB sealed oneof), and casts to CheckpointSupport, which no production operator implements — plus it has zero assertions and one branch is literally ???. Its intent is already covered by SerializationManagerSpec and CheckpointSubsystemSpec. The second calls AmberClient with six constructor arguments where the signature takes five, and starts a real two-operator workflow with 30s awaits.

Two production bugs surfaced while writing these; neither is fixed here, and neither is pinned as current behaviour.

  1. TakeGlobalCheckpointHandler under-reports totalSize. It accumulates into a captured var via a per-future .onSuccess { resp => totalSize += resp.size } registered before Future.collect wraps those futures. A com.twitter.util.Promise runs continuations in reverse registration order, so for the last-satisfied future the collect continuation — and the .map that reads the counter — runs before that future's onSuccess has contributed. Observed directly: two workers reporting 4000 and 56 produced TakeGlobalCheckpointResponse(4000); a single worker reporting 7 produced 0. Fix direction is to sum from the collected Seq inside the .map. The spec case is named for the fan-in it does verify and carries a comment explaining why the total is deliberately not asserted.

  2. A CheckpointState can never be written to storage with the shipped config. SequentialRecordWriter.writeRecord does AmberRuntime.serde.serialize(obj).get, but CheckpointState is a plain class that does not extend java.io.Serializable, while cluster.conf sets allow-java-serialization = off with "java.io.Serializable" = kryo as the only binding — so the write throws NotSerializableException. That kills both persist paths, which means global checkpointing cannot produce a checkpoint on disk and the checkpoint-restore branch of WorkflowActor.setupReplay has nothing to read. Existing specs missed it because they only exercise chkpt.save / chkpt.load (SerializedState is a case class). Consequence for this PR: the two write-branch cases wrap the call in Try and assert only the state the handler mutates beforehand — assertions that hold both before and after a fix, so they are not inverted change-detectors. The class scaladoc says so explicitly.

A third, smaller hazard is recorded in the notes but not tested: EvaluatePythonExpressionHandler statically types worker results as EvaluatedValue, which is not a member of the ControlReturn sealed oneof. Erasure hides it on the JVM side and the Python side sets a stray attribute, so an empty ControlReturn comes back. That is why the spec asserts dispatch and fan-in shape but never fulfils a worker promise with a fake EvaluatedValue — doing so would cement a type confusion.

Any related issues, documentation, discussions?

Closes #7117

How was this PR tested?

Five new specs, 27 tests. Run together with the three adjacent checkpoint specs they must not duplicate or break — 49 tests, Java 17:

sbt "WorkflowExecutionService/testOnly *EvaluatePythonExpressionHandlerSpec *EmbeddedControlMessageHandlerSpec *TakeGlobalCheckpointHandlerSpec *PrepareCheckpointHandlerSpec *FinalizeCheckpointHandlerSpec *CheckpointSpec *SerializationManagerSpec *CheckpointSubsystemSpec"
[info] Suites: completed 8, aborted 0
[info] Tests: succeeded 49, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Isolation was checked too: the new specs were re-run interleaved with LoggingSpec, WorkflowWorkerSpec, DPThreadSpec and the sibling promise-handler specs (16 suites, concurrent in one JVM), and then a second time in the same JVM to confirm the ram:// folders leave no residue. No new spec mutates AmberRuntime's private _serde / _actorSystem. Test/scalafmtCheck and Test/scalafix --check both [success].

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 5)

These five handlers were the engine's lowest-coverage cluster (4.5-8.3%).
All five specs follow the existing precedent in this layer
(RetrieveWorkflowStateHandlerSpec): a real CoordinatorProcessor plus a
second handler initializer, capturing what the output handler emits, so
the tests drive the real handler methods rather than a re-implementation.

- EmbeddedControlMessageHandlerSpec / EvaluatePythonExpressionHandlerSpec:
  dispatch shape, per-worker fan-out, channel scoping, the "latest
  execution" worker selection, and the fan-in that must not answer early.
- TakeGlobalCheckpointHandlerSpec: the single-physical-op guard, the
  already-taken short-circuit that downgrades to an estimate, the
  per-checkpoint subfolder the restore path later resolves, and the
  propagation request's shape. Uses `ram://` in-memory VFS storage, per the
  LoggingSpec / DPThreadSpec precedent, with a distinct folder per case.
- PrepareCheckpointHandlerSpec / FinalizeCheckpointHandlerSpec: the
  estimation branches, DP-state serialization under the requested id, the
  CheckpointSupport hand-off (and the skip branch's observable
  consequence), and the recorded-message fold.

Assertion strength was checked by mutation across four rounds: 21 of 22
non-trivial cases were shown to fail on a realistic production change, then
every mutation was reverted. No production file is touched.

Also removes the two commented-out test blocks in CheckpointSpec, which are
dead beyond repair. The first matches on OpExecInitInfoWithCode /
OpExecInitInfoWithFunc -- names that now appear nowhere else in the repo,
since OpExecInitInfo became a ScalaPB sealed oneof -- and casts to
CheckpointSupport, which no production operator implements; it also has zero
assertions and one branch is literally `???`. Its intent is already covered
by SerializationManagerSpec and CheckpointSubsystemSpec. The second calls
AmberClient with six constructor arguments where the current signature takes
five, and its body starts a real two-operator workflow with 30s awaits, so
it belongs in integration scope if anywhere.
@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @Yicong-Huang
    You can notify them by mentioning @Yicong-Huang in a comment.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

🟢 0 better · 🔴 6 worse · ⚪ 9 noise (<±5%) · 0 without baseline

Compared against main 301e3a8 benchmarked on this same runner, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.

Dashboard · Run

config throughput MB/s latency max Δ latest / 7d
bs=10 sw=10 sl=64 395 0.241 23,729/36,084/36,084 us ⚪ within ±5% / 🔴 +128.8%
🔴 bs=100 sw=10 sl=64 887 0.541 107,245/157,049/157,049 us 🔴 +20.8% / 🔴 +46.3%
🔴 bs=1000 sw=10 sl=64 1,096 0.669 908,807/986,619/986,619 us 🔴 +6.6% / 🟢 -7.3%
Baseline details

Latest main 301e3a8 from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 395 tuples/sec 408 tuples/sec 786.12 tuples/sec -3.2% -49.8%
bs=10 sw=10 sl=64 MB/s 0.241 MB/s 0.249 MB/s 0.48 MB/s -3.2% -49.8%
bs=10 sw=10 sl=64 p50 23,729 us 24,086 us 12,305 us -1.5% +92.8%
bs=10 sw=10 sl=64 p95 36,084 us 37,058 us 15,774 us -2.6% +128.8%
bs=10 sw=10 sl=64 p99 36,084 us 37,058 us 18,978 us -2.6% +90.1%
bs=100 sw=10 sl=64 throughput 887 tuples/sec 960 tuples/sec 999.71 tuples/sec -7.6% -11.3%
bs=100 sw=10 sl=64 MB/s 0.541 MB/s 0.586 MB/s 0.61 MB/s -7.7% -11.3%
bs=100 sw=10 sl=64 p50 107,245 us 102,487 us 100,616 us +4.6% +6.6%
bs=100 sw=10 sl=64 p95 157,049 us 130,009 us 107,356 us +20.8% +46.3%
bs=100 sw=10 sl=64 p99 157,049 us 130,009 us 113,255 us +20.8% +38.7%
bs=1000 sw=10 sl=64 throughput 1,096 tuples/sec 1,122 tuples/sec 1,031 tuples/sec -2.3% +6.3%
bs=1000 sw=10 sl=64 MB/s 0.669 MB/s 0.685 MB/s 0.63 MB/s -2.3% +6.3%
bs=1000 sw=10 sl=64 p50 908,807 us 896,211 us 980,328 us +1.4% -7.3%
bs=1000 sw=10 sl=64 p95 986,619 us 925,811 us 1,027,528 us +6.6% -4.0%
bs=1000 sw=10 sl=64 p99 986,619 us 925,811 us 1,054,298 us +6.6% -6.4%
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,506.57,200,128000,395,0.241,23729.07,36084.21,36084.21
1,100,10,64,20,2254.66,2000,1280000,887,0.541,107244.78,157049.18,157049.18
2,1000,10,64,20,18253.57,20000,12800000,1096,0.669,908807.14,986618.66,986618.66

@aglinxinyuan
aglinxinyuan requested a review from mengw15 July 30, 2026 05:30
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.71%. Comparing base (301e3a8) to head (0cbf0dc).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #7118      +/-   ##
============================================
+ Coverage     79.57%   79.71%   +0.14%     
- Complexity     3837     3849      +12     
============================================
  Files          1159     1159              
  Lines         46145    46145              
  Branches       5127     5127              
============================================
+ Hits          36718    36784      +66     
+ Misses         7799     7721      -78     
- Partials       1628     1640      +12     
Flag Coverage Δ *Carryforward flag
access-control-service 70.00% <ø> (ø) Carriedforward from 301e3a8
agent-service 77.42% <ø> (ø) Carriedforward from 301e3a8
amber 73.46% <ø> (+0.38%) ⬆️
computing-unit-managing-service 20.49% <ø> (ø) Carriedforward from 301e3a8
config-service 66.66% <ø> (ø) Carriedforward from 301e3a8
file-service 67.21% <ø> (ø) Carriedforward from 301e3a8
frontend 83.05% <ø> (ø) Carriedforward from 301e3a8
notebook-migration-service 78.94% <ø> (ø) Carriedforward from 301e3a8
pyamber 97.36% <ø> (ø) Carriedforward from 301e3a8
workflow-compiling-service 26.31% <ø> (ø) Carriedforward from 301e3a8

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add unit tests for the checkpoint and ECM promise handlers

2 participants