Repository navigation
feat(workflow-core): add operator output port cache key - #5966
Conversation
CacheKeyUtil computes a deterministic cache key (SHA-256 over the canonical upstream sub-DAG) for an output port, to match reusable results across executions. Pure function, no DB/storage dependency. Part of apache#5882.
Automated Reviewer SuggestionsBased on the
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5966 +/- ##
============================================
+ Coverage 56.28% 56.69% +0.40%
- Complexity 2992 3024 +32
============================================
Files 1120 1122 +2
Lines 43217 43342 +125
Branches 4662 4670 +8
============================================
+ Hits 24326 24574 +248
+ Misses 17472 17326 -146
- Partials 1419 1442 +23
*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:
|
|
@carloea2 could you help review this one when you get a chance? Thanks! |
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 370 | 0.226 | 24,541/38,381/38,381 us | 🔴 -9.6% / 🔴 +154.7% |
| ⚪ | bs=100 sw=10 sl=64 | 815 | 0.497 | 120,954/141,566/141,566 us | ⚪ within ±5% / 🔴 +31.6% |
| ⚪ | bs=1000 sw=10 sl=64 | 938 | 0.572 | 1,069,318/1,105,838/1,105,838 us | ⚪ within ±5% / 🔴 +8.3% |
Baseline details
Latest main a24d1d1 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 370 tuples/sec | 409 tuples/sec | 777.62 tuples/sec | -9.5% | -52.4% |
| bs=10 sw=10 sl=64 | MB/s | 0.226 MB/s | 0.25 MB/s | 0.475 MB/s | -9.6% | -52.4% |
| bs=10 sw=10 sl=64 | p50 | 24,541 us | 23,928 us | 12,612 us | +2.6% | +94.6% |
| bs=10 sw=10 sl=64 | p95 | 38,381 us | 37,635 us | 15,070 us | +2.0% | +154.7% |
| bs=10 sw=10 sl=64 | p99 | 38,381 us | 37,635 us | 18,360 us | +2.0% | +109.0% |
| bs=100 sw=10 sl=64 | throughput | 815 tuples/sec | 833 tuples/sec | 988.31 tuples/sec | -2.2% | -17.5% |
| bs=100 sw=10 sl=64 | MB/s | 0.497 MB/s | 0.508 MB/s | 0.603 MB/s | -2.2% | -17.6% |
| bs=100 sw=10 sl=64 | p50 | 120,954 us | 118,143 us | 101,066 us | +2.4% | +19.7% |
| bs=100 sw=10 sl=64 | p95 | 141,566 us | 145,038 us | 107,594 us | -2.4% | +31.6% |
| bs=100 sw=10 sl=64 | p99 | 141,566 us | 145,038 us | 115,830 us | -2.4% | +22.2% |
| bs=1000 sw=10 sl=64 | throughput | 938 tuples/sec | 938 tuples/sec | 1,019 tuples/sec | 0.0% | -8.0% |
| bs=1000 sw=10 sl=64 | MB/s | 0.572 MB/s | 0.573 MB/s | 0.622 MB/s | -0.2% | -8.1% |
| bs=1000 sw=10 sl=64 | p50 | 1,069,318 us | 1,061,623 us | 986,982 us | +0.7% | +8.3% |
| bs=1000 sw=10 sl=64 | p95 | 1,105,838 us | 1,113,786 us | 1,028,491 us | -0.7% | +7.5% |
| bs=1000 sw=10 sl=64 | p99 | 1,105,838 us | 1,113,786 us | 1,058,493 us | -0.7% | +4.5% |
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,540.55,200,128000,370,0.226,24541.19,38380.85,38380.85
1,100,10,64,20,2455.43,2000,1280000,815,0.497,120953.52,141565.63,141565.63
2,1000,10,64,20,21322.96,20000,12800000,938,0.572,1069317.58,1105838.07,1105838.07|
Two questions:
|
Yicong-Huang
left a comment
There was a problem hiding this comment.
Thanks! Please see my comments above. Also left inline comments
Move the cache-key unit test from the amber module into workflow-core, building the PhysicalPlan directly instead of via the engine test helpers, so JaCoCo attributes coverage to the workflow-core module (the patch was reported 0% because the test ran in a different module). 8 tests, all pass.
Move the upstream-collection into PhysicalPlan.getTransitiveUpstreamSubPlan and have computeCacheKey take the sub-DAG. Rename CacheKey->StorageCacheKey, sameComputation->isSameComputation, buildPayload->buildJSONPayload. Derive the JSON mapper from JSONUtils.objectMapper (plus ORDER_MAP_ENTRIES_BY_KEYS) so config stays in sync while keeping deterministic keys. Document why opExecInitInfo is hashed.
|
Thanks @Yicong-Huang — pushed changes addressing these (replies inline). Summary: renamed to |
|
@Yicong-Huang on your two questions: 1. How will the cache key be used, and does the test reflect that? 2. Why did coverage say 0%? |
Yicong-Huang
left a comment
There was a problem hiding this comment.
LGTM with two changes needed
…n and multi-source/sink Address review: add cases for converging inputs (join or union) and for a graph with multiple sources and sinks (the sub-DAG of one sink excludes the other).
carloea2
left a comment
There was a problem hiding this comment.
Comment 1
Could we add a CacheKeyUtil test with a non-empty OpExecInitInfo, such as OpExecWithClassName(..., descString) or OpExecWithCode(...)?
Comment 2
Should computeCacheKey validate that target is an output port and belongs to the provided sub-DAG, or is that expected to be enforced by callers?
Comment 3
Should fields like OutputPort.mode, blocking, or reuseStorage be included in the cache identity, or should we add a test documenting that they are intentionally ignored?
…ort attributes Address review (Carlos): add a case with a non-empty OpExecInitInfo (OpExecWithCode) showing exec info is part of the key; add a case documenting that output-port attributes that do not change the data (blocking, mode, reuseStorage) are intentionally excluded from the cache identity; document computeCacheKey's caller contract in scaladoc.
|
@carloea2 thanks, addressed all three:
|
|
Thanks @Xiao-zhen-Liu One more question. I don't know if it is possible, but if an operator can access those properties like mode, blocking, etc., and change its computation based on that, shouldn't they be considered? I know this is an extreme case anyways. |
Good question, but I think mode blocking those are execution configs, and they won't affect result. However, I wonder if change of these invalidates the cache/creates a new storage cache key. |
What changes were proposed in this PR?
Adds
CacheKeyUtil(in workflow-core), which computes a deterministic cache key foran output port from the upstream sub-DAG that produces it. The sub-DAG is the port's
operator plus its transitive upstream, obtained from a new
PhysicalPlan.getTransitiveUpstreamSubPlan(opId);computeCacheKeytakes that sub-DAG.For a target output port, the key is built from:
its execution init info (the concrete execution definition the engine runs), and the
schema of each of its output ports;
These are written to a JSON document with sorted keys and a sorted operator and edge
order, so the same upstream computation always serializes to the same bytes and hashes
(SHA-256) to the same key. Any upstream change (a parameter edit, a rewiring, a schema
change) produces a different key; changes elsewhere in the workflow do not.
Matching is collision-safe:
isSameComputationcompares the hash first and, on a hashmatch, confirms with the full JSON, so a hash collision never reuses a result the port
was not computed from.
This is the first piece of the operator output port result cache.
CacheKeyUtilis apure function with no database or storage dependency, and nothing calls it yet, so it
changes no existing behavior. The cache table (companion PR) and the service that uses
both land separately.
Any related issues, documentation, discussions?
Closes #5968. Part of the storage foundation #5882 (umbrella #5881). Design discussion: #5880.
How was this PR tested?
New unit tests in workflow-core (built from
PhysicalPlans directly, no engine testhelpers, so the logic is covered in its own module):
CacheKeyUtilSpeccovers a stablekey, sensitivity to an upstream change, different ports, a source with no upstream,
upstream-only scope, and the three
isSameComputationcases; the newPhysicalPlan.getTransitiveUpstreamSubPlanis tested inWorkflowCoreTypesSpec. Run withsbt "WorkflowCore/testOnly *CacheKeyUtilSpec *WorkflowCoreTypesSpec"— all pass.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 4.8 (Claude Code)