This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6729-32b8fa8040b3c6eb1d99def04e92e1b53a4991d6 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 2cef6ce1e56b7cd3dbd0214ee573304e9944978c Author: Xiaozhen Liu <[email protected]> AuthorDate: Tue Aug 18 08:06:42 2026 +0000 feat(amber): carry cache-reuse status as a metrics flag (#6729) ### What changes were proposed in this PR? This PR was reworked after the discussion in #5880. The old version added a new CACHE_REUSED state; the review threads below refer to that version. The discussion concluded that a reused operator should just report COMPLETED, because every place that checks state treats completed and reused the same way. What still needs to travel is one bit: whether the operator's results came from the cache. So the PR now adds only that bit: - A `reused_from_cache` boolean on `OperatorMetrics`. A reused operator still reports COMPLETED. - A logical operator counts as reused only when all of its physical operators are (`aggregateMetrics`). - The statistics websocket event and the TS `OperatorStatistics` type carry the flag to the frontend. No UI changes here; that is #5886. Nothing sets the flag yet. The producer comes with #5884, so with an empty cache the engine behaves exactly like main. ### Any related issues, documentation, discussions? Part of #5881. Design discussion: #5880. Related: #5883 and #5884. ### How was this PR tested? New unit tests in ExecutionUtilsSpec cover the all-physical-operators rule and the flag staying false when nothing sets it. Existing specs pass unchanged, scalafmt is clean, and the frontend production build passes. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude (Claude Code) --- .../engine/common/executionruntimestate.proto | 4 ++++ .../coordinator/execution/ExecutionUtils.scala | 9 +++++++- .../event/OperatorStatisticsUpdateEvent.scala | 6 +++++- .../texera/web/service/ExecutionStatsService.scala | 21 +++---------------- .../coordinator/execution/ExecutionUtilsSpec.scala | 24 ++++++++++++++++++++-- .../websocket/event/TexeraWebSocketEventSpec.scala | 5 ++++- .../workspace/types/execute-workflow.interface.ts | 2 ++ 7 files changed, 48 insertions(+), 23 deletions(-) diff --git a/amber/src/main/protobuf/org/apache/texera/amber/engine/common/executionruntimestate.proto b/amber/src/main/protobuf/org/apache/texera/amber/engine/common/executionruntimestate.proto index e712b3adc8..e384e80c80 100644 --- a/amber/src/main/protobuf/org/apache/texera/amber/engine/common/executionruntimestate.proto +++ b/amber/src/main/protobuf/org/apache/texera/amber/engine/common/executionruntimestate.proto @@ -82,6 +82,10 @@ message OperatorStatistics{ message OperatorMetrics{ architecture.rpc.WorkflowAggregatedState operator_state = 1 [(scalapb.field).no_box = true]; OperatorStatistics operator_statistics = 2 [(scalapb.field).no_box = true]; + // True when the operator's results were reused from the operator port cache + // instead of being computed by workers. Provenance of a completed operator, + // not a distinct state; the operator still reports COMPLETED. + bool reused_from_cache = 3; } message ExecutionStatsStore { diff --git a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtils.scala b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtils.scala index 666aeece42..303fba9a1f 100644 --- a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtils.scala +++ b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtils.scala @@ -77,7 +77,14 @@ object ExecutionUtils { dataProcessingTimeSum, controlProcessingTimeSum, idleTimeSum - ) + ), + // Fully-reused semantics: partial reuse is possible (HashJoin's build and + // probe sit in different regions), and a partially reused operator reports + // false; per-port detail comes from the cache entries. The input holds the + // operators that currently have a region execution, so this shares the + // state field's transient window until all regions exist. Non-empty here, + // so the forall cannot hold vacuously. + reusedFromCache = metrics.forall(_.reusedFromCache) ) } diff --git a/amber/src/main/scala/org/apache/texera/web/model/websocket/event/OperatorStatisticsUpdateEvent.scala b/amber/src/main/scala/org/apache/texera/web/model/websocket/event/OperatorStatisticsUpdateEvent.scala index d4aa6117c9..af91f65784 100644 --- a/amber/src/main/scala/org/apache/texera/web/model/websocket/event/OperatorStatisticsUpdateEvent.scala +++ b/amber/src/main/scala/org/apache/texera/web/model/websocket/event/OperatorStatisticsUpdateEvent.scala @@ -30,7 +30,11 @@ case class OperatorAggregatedMetrics( numWorkers: Long, aggregatedDataProcessingTime: Long, aggregatedControlProcessingTime: Long, - aggregatedIdleTime: Long + aggregatedIdleTime: Long, + // Provenance: the operator completed by reusing cached results (no workers ran). + // Deliberately no default: a new construction site must decide the flag + // explicitly instead of silently sending false. + reusedFromCache: Boolean ) case class OperatorStatisticsUpdateEvent(operatorStatistics: Map[String, OperatorAggregatedMetrics]) diff --git a/amber/src/main/scala/org/apache/texera/web/service/ExecutionStatsService.scala b/amber/src/main/scala/org/apache/texera/web/service/ExecutionStatsService.scala index ac33478451..2c173c25a8 100644 --- a/amber/src/main/scala/org/apache/texera/web/service/ExecutionStatsService.scala +++ b/amber/src/main/scala/org/apache/texera/web/service/ExecutionStatsService.scala @@ -125,7 +125,8 @@ class ExecutionStatsService( metrics.operatorStatistics.numWorkers, metrics.operatorStatistics.dataProcessingTime, metrics.operatorStatistics.controlProcessingTime, - metrics.operatorStatistics.idleTime + metrics.operatorStatistics.idleTime, + reusedFromCache = metrics.reusedFromCache ) (x._1, res) }) @@ -237,23 +238,7 @@ class ExecutionStatsService( val updatedLastMetrics = lastPersistedMetrics ++ newKeys.map(_ -> defaultMetrics) // Combine new metrics with old metrics for keys that are no longer present - val completeMetricsMap = newMetrics ++ oldKeys.map(key => key -> updatedLastMetrics(key)) - - // Transform the complete metrics map to ensure consistent structure - completeMetricsMap.map { - case (key, metrics) => - key -> OperatorMetrics( - metrics.operatorState, - OperatorStatistics( - metrics.operatorStatistics.inputMetrics, - metrics.operatorStatistics.outputMetrics, - metrics.operatorStatistics.numWorkers, - metrics.operatorStatistics.dataProcessingTime, - metrics.operatorStatistics.controlProcessingTime, - metrics.operatorStatistics.idleTime - ) - ) - } + newMetrics ++ oldKeys.map(key => key -> updatedLastMetrics(key)) } private def storeRuntimeStatistics( diff --git a/amber/src/test/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtilsSpec.scala b/amber/src/test/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtilsSpec.scala index 237936fa06..3bdb6c2e86 100644 --- a/amber/src/test/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtilsSpec.scala +++ b/amber/src/test/scala/org/apache/texera/amber/engine/architecture/coordinator/execution/ExecutionUtilsSpec.scala @@ -190,11 +190,13 @@ class ExecutionUtilsSpec extends AnyFlatSpec { numWorkers: Int = 0, dataTime: Long = 0, controlTime: Long = 0, - idleTime: Long = 0 + idleTime: Long = 0, + reused: Boolean = false ): OperatorMetrics = OperatorMetrics( state, - OperatorStatistics(input, output, numWorkers, dataTime, controlTime, idleTime) + OperatorStatistics(input, output, numWorkers, dataTime, controlTime, idleTime), + reusedFromCache = reused ) "ExecutionUtils.aggregateMetrics" should "return UNINITIALIZED defaults when given no metrics" in { @@ -337,4 +339,22 @@ class ExecutionUtilsSpec extends AnyFlatSpec { assert(result.operatorStatistics.numWorkers == 3) assert(result.operatorStatistics.dataProcessingTime == 12) } + + // -- aggregateMetrics: reused-from-cache provenance ---------------------- + + it should "report reusedFromCache only when every physical operator is reused" in { + val reusedA = metricsWith(WorkflowAggregatedState.COMPLETED, reused = true) + val reusedB = metricsWith(WorkflowAggregatedState.COMPLETED, reused = true) + val computed = metricsWith(WorkflowAggregatedState.COMPLETED) + + assert(ExecutionUtils.aggregateMetrics(List(reusedA, reusedB)).reusedFromCache) + assert(!ExecutionUtils.aggregateMetrics(List(reusedA, computed)).reusedFromCache) + assert(!ExecutionUtils.aggregateMetrics(List(computed)).reusedFromCache) + } + + it should "default reusedFromCache to false for empty input" in { + // Empty input takes the early-return path, whose default is false; metrics + // that no producer has marked keep that default too. + assert(!ExecutionUtils.aggregateMetrics(Iterable.empty).reusedFromCache) + } } diff --git a/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala b/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala index 6a1e063d59..eeeb7f89a9 100644 --- a/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala @@ -123,7 +123,10 @@ class TexeraWebSocketEventSpec extends AnyFlatSpec with Matchers { numWorkers = 17L, aggregatedDataProcessingTime = 18L, aggregatedControlProcessingTime = 19L, - aggregatedIdleTime = 20L + aggregatedIdleTime = 20L, + // Non-default on purpose: the symmetric round trip below only pins this + // field on the wire if a drop would change the value read back. + reusedFromCache = true ) private val resultRow = objectMapper.createObjectNode().put("city", "Irvine") diff --git a/frontend/src/app/workspace/types/execute-workflow.interface.ts b/frontend/src/app/workspace/types/execute-workflow.interface.ts index 8bb7696edf..54d1a2d6ff 100644 --- a/frontend/src/app/workspace/types/execute-workflow.interface.ts +++ b/frontend/src/app/workspace/types/execute-workflow.interface.ts @@ -81,6 +81,8 @@ export enum OperatorState { export interface OperatorStatistics extends Readonly<{ operatorState: OperatorState; + // Provenance: the operator completed by reusing cached results (no workers ran). + reusedFromCache?: boolean; aggregatedInputRowCount: number; aggregatedInputSize?: number; inputPortMetrics: Record<string, number>;
