refactor(amber): read the loop input table from its materialization instead of shipping it in state - #6971
Open
aglinxinyuan wants to merge 1 commit into
Open
refactor(amber): read the loop input table from its materialization instead of shipping it in state#6971aglinxinyuan wants to merge 1 commit into
aglinxinyuan wants to merge 1 commit into
Conversation
…nstead of shipping it in state The loop's input table used to ride INSIDE the State content: LoopStart encoded its buffered input as Arrow IPC bytes, base64'd inside the JSON content, and that payload was re-written and re-read at every loop-body hop, every iteration (~33% base64 bloat, JSON-column size limits, and a second, divergent Arrow codec via pa.Table.from_pandas inference). That data already exists: in the fully-materialized mode loops require, the Loop Start's input-port materialization holds exactly the loop's input table for the whole loop (Loop Start re-reads it every iteration; the back-edge truncates only the state doc at the same base URI, never the result doc). So ship the port's BASE URI in the setup config and derive both addresses from it: - proto: InitializeExecutorRequest.loopStartStateUris -> loopStartPortUris (field 4 unchanged); the value is now the base URI. WorkflowExecutionManager ships storagePairs.head._1 directly. - back-edge write: main_loop derives state_uri(base) (unchanged behavior, one derivation later). - table read: on the matching consume, MainLoop reads result_uri(base) and injects the table into the LoopEnd via a new runtime-only attach_loop_table hook; a read failure is reported like a UDF error (report_exception) instead of silently dropping the consume. - LoopStartOperator.produce_state_on_finish emits only the user loop variables (the reserved-`table` collision raise stays: a user var named `table` would now be silently shadowed by the injected table); run_update uses the attached table; the "consumed" marker (_loop_table) is still set only by a successful update, so condition()'s short-circuit semantics are unchanged. - table_to_ipc_bytes / table_from_ipc_bytes and the now-unused TableOperator._buffered_table accessor are deleted. Nested loops keep working by construction: the inner Loop Start's map entry points at the outer Loop Start's output port, whose result doc is recreated per OUTER iteration but persists across inner iterations (the jump rewinds to the inner level only). Tests updated across test_loop_operators (attach-based flow, plus new pins: attach alone does not mark the loop consumed; run_update fails loud when no table was attached; produced state carries no table), test_main_loop (base-URI derivation for the back-edge write, consume injects the read table), test_initialize_executor_handler (renamed field), and the Scala ctor sites. The four LoopIntegrationSpec e2e cases exercise the full path in CI.
Contributor
Automated Reviewer SuggestionsBased on the
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6971 +/- ##
============================================
- Coverage 78.97% 78.97% -0.01%
- Complexity 3783 3787 +4
============================================
Files 1160 1160
Lines 46105 46110 +5
Branches 5115 5115
============================================
+ Hits 36413 36415 +2
- Misses 8068 8075 +7
+ Partials 1624 1620 -4
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 353 | 0.215 | 27,596/38,666/38,666 us | 🔴 +22.4% / 🔴 +138.2% |
| 🔴 | bs=100 sw=10 sl=64 | 758 | 0.463 | 124,915/192,394/192,394 us | 🔴 +34.1% / 🔴 +77.1% |
| ⚪ | bs=1000 sw=10 sl=64 | 899 | 0.548 | 1,115,167/1,140,207/1,140,207 us | ⚪ within ±5% / 🔴 +11.6% |
Baseline details
Latest main 5040cad from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 353 tuples/sec | 410 tuples/sec | 767.9 tuples/sec | -13.9% | -54.0% |
| bs=10 sw=10 sl=64 | MB/s | 0.215 MB/s | 0.25 MB/s | 0.469 MB/s | -14.0% | -54.1% |
| bs=10 sw=10 sl=64 | p50 | 27,596 us | 22,554 us | 12,502 us | +22.4% | +120.7% |
| bs=10 sw=10 sl=64 | p95 | 38,666 us | 35,496 us | 16,234 us | +8.9% | +138.2% |
| bs=10 sw=10 sl=64 | p99 | 38,666 us | 35,496 us | 18,919 us | +8.9% | +104.4% |
| bs=100 sw=10 sl=64 | throughput | 758 tuples/sec | 815 tuples/sec | 974.8 tuples/sec | -7.0% | -22.2% |
| bs=100 sw=10 sl=64 | MB/s | 0.463 MB/s | 0.497 MB/s | 0.595 MB/s | -6.8% | -22.2% |
| bs=100 sw=10 sl=64 | p50 | 124,915 us | 120,883 us | 102,449 us | +3.3% | +21.9% |
| bs=100 sw=10 sl=64 | p95 | 192,394 us | 143,460 us | 108,652 us | +34.1% | +77.1% |
| bs=100 sw=10 sl=64 | p99 | 192,394 us | 143,460 us | 116,310 us | +34.1% | +65.4% |
| bs=1000 sw=10 sl=64 | throughput | 899 tuples/sec | 895 tuples/sec | 1,004 tuples/sec | +0.4% | -10.5% |
| bs=1000 sw=10 sl=64 | MB/s | 0.548 MB/s | 0.546 MB/s | 0.613 MB/s | +0.4% | -10.6% |
| bs=1000 sw=10 sl=64 | p50 | 1,115,167 us | 1,116,468 us | 999,606 us | -0.1% | +11.6% |
| bs=1000 sw=10 sl=64 | p95 | 1,140,207 us | 1,155,929 us | 1,046,770 us | -1.4% | +8.9% |
| bs=1000 sw=10 sl=64 | p99 | 1,140,207 us | 1,155,929 us | 1,076,937 us | -1.4% | +5.9% |
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,566.80,200,128000,353,0.215,27596.18,38666.16,38666.16
1,100,10,64,20,2637.13,2000,1280000,758,0.463,124915.32,192393.68,192393.68
2,1000,10,64,20,22258.59,20000,12800000,899,0.548,1115167.28,1140207.37,1140207.37
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.
What changes were proposed in this PR?
The loop's input table used to ride inside the State content: LoopStart encoded its buffered input as Arrow IPC bytes, base64'd into the JSON
contentcolumn, and that payload was re-written and re-read at every loop-body hop, every iteration.That data already exists. In the fully-materialized mode loops require, the Loop Start's input-port materialization holds exactly the loop's input table for the whole loop — Loop Start re-reads it every iteration, and the back-edge truncates only the state doc at the same base URI, never the result doc. So this PR ships the port's base URI in the setup config and derives both addresses from it:
loopStartStateUris= state URIloopStartPortUris= base URI (renamed so the semantic change is loud)result_uri(base), injected via a new runtime-onlyattach_loop_tablehook; a read failure is reported like a UDF errortable_to_ipc_bytes/table_from_ipc_bytesfrom_pandasinference)Semantics deliberately preserved:
tablecollision raise stays (a user var namedtablewould now be silently shadowed by the injected table — worse than before);_loop_table) is still set only by a successfulrun_update, socondition()'s short-circuit for pass-through-only Loop Ends is unchanged;Wins: no ~33% base64 bloat, no JSON-column size ceiling on the table (large-table loops become viable), strictly less I/O for any non-empty loop body (one read per iteration replaces N state-doc writes+reads per hop), and one Arrow codec instead of two.
Note: this deepens the read-side use of
storagePairs.head._1— the same shared upstream URI as the known back-edge fan-out design discussion; if that ever moves to a per-loop private doc, this read moves with it.Any related issues, documentation, discussions?
Builds on #5900 (State columns) and #6661 (envelope through JVM hops). Related design context: #6660.
How was this PR tested?
test_loop_operators.pyrewritten for the attach-based flow plus new pins: the produced state carries notable; attaching alone does not mark the loop consumed;run_updatefails loud when no table was attached.test_main_loop.pypins the base-URI derivation for the back-edge write (state_uri(base)), the missing-config fail-loud, and that the matching consume injects the read table.test_initialize_executor_handler.pycovers the renamed proto field. 237 tests green locally (the only failures in a full sweep are pre-existing environment ones, identical on unmodified main).scalafmtCheckAll,scalafixAll --check, and the worker/descriptor spec suites (WorkerSpec,WorkflowWorkerSpec,SerializationManagerSpec,WorkflowExecutionManagerSpec,LoopStartOpDescSpec,LoopEndOpDescSpec) all pass on Java 17.LoopIntegrationSpeccases (single, nested 3×3, JVM chain, nested JVM chain) exercise the full read-at-consume path in theamber-integrationCI job; the nested cases specifically cover the inner-loop read against the outer Loop Start's per-outer-iteration output doc.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Fable 5)