Xiao-zhen-Liu commented on code in PR #6913:
URL: https://github.com/apache/texera/pull/6913#discussion_r3696858250


##########
amber/src/main/python/core/runnables/main_loop.py:
##########
@@ -374,9 +374,23 @@ def _process_state_frame(self, frame: StateFrame) -> None:
             return
 
         if isinstance(executor, LoopEndOperator):
-            # Matching LoopEnd (in_counter == 0): it will consume this state
-            # and jump back. Remember which LoopStart to jump to (it rides
-            # the envelope) for complete()/_jump_to_loop_start.
+            if not frame.loop_start_id:
+                # An UNstamped counter-0 state at a LoopEnd is not the loop's
+                # own boundary state -- it was produced by a loop-body
+                # operator's produce_state_on_start/finish (a public API on
+                # both engine sides), which emits with the "no loop" envelope.
+                # A real loop state is always stamped: the matching LoopStart
+                # stamps its own id on every iteration's output state.
+                # Forward it downstream unchanged, skipping the operator, like
+                # any default pass-through: consuming it would clobber the
+                # captured back-jump id with "" and hand run_update a State
+                # with no `table` payload (#discussion_r3648708075).

Review Comment:
   `(#discussion_r3648708075)` is the first reference of this kind under 
`amber/src/main` - the tests use them, production code doesn't. A bare id isn't 
resolvable by anyone reading this in a year. Either write the full URL or drop 
it; the comment above already says why.



##########
amber/src/main/python/core/runnables/main_loop.py:
##########
@@ -374,9 +374,23 @@ def _process_state_frame(self, frame: StateFrame) -> None:
             return
 
         if isinstance(executor, LoopEndOperator):
-            # Matching LoopEnd (in_counter == 0): it will consume this state
-            # and jump back. Remember which LoopStart to jump to (it rides
-            # the envelope) for complete()/_jump_to_loop_start.
+            if not frame.loop_start_id:
+                # An UNstamped counter-0 state at a LoopEnd is not the loop's
+                # own boundary state -- it was produced by a loop-body
+                # operator's produce_state_on_start/finish (a public API on
+                # both engine sides), which emits with the "no loop" envelope.
+                # A real loop state is always stamped: the matching LoopStart
+                # stamps its own id on every iteration's output state.
+                # Forward it downstream unchanged, skipping the operator, like
+                # any default pass-through: consuming it would clobber the
+                # captured back-jump id with "" and hand run_update a State
+                # with no `table` payload (#discussion_r3648708075).
+                self._emit_and_save_state(state, in_counter, 
frame.loop_start_id)

Review Comment:
   Two different situations produce an unstamped counter-0 frame here, and this 
treats them the same:
   
   1. a body operator's own boundary state - forwarding is right;
   2. the real loop state that lost its stamp somewhere upstream.
   
   Case 2 isn't hypothetical: it's exactly the bug the "loop whose body 
contains a JVM (Scala) operator" e2e was written for 
(`LoopIntegrationSpec.scala:286-292` - the Scala hop "zeroed the counter and 
blanked the id"). Before this change that failed loudly with `no loop-back 
state URI configured for LoopStart ''`. After it, the state is forwarded, 
`run_update` never runs, `_loop_table` stays `None`, `condition()` returns 
`False`, and the loop quietly stops after one iteration - the workflow 
completes and reports success with one row instead of three. The e2e still 
catches it, but as `expected 3, got 1` rather than a message naming the cause.
   
   Cheap way to keep the loud failure: a real loop state always carries the 
reserved `table` key (`LoopStartOperator.produce_state_on_finish`, 
`operator.py:414-418`), and an operator-originated boundary state won't. So 
`not frame.loop_start_id and _TABLE_KEY in state` is a lost stamp - raise there 
with the old message instead of forwarding, and forward only the rest.



##########
amber/src/test/integration/org/apache/texera/amber/engine/e2e/LoopIntegrationSpec.scala:
##########
@@ -44,6 +44,7 @@ import org.apache.texera.amber.operator.LogicalOp
 import org.apache.texera.amber.operator.limit.LimitOpDesc
 import org.apache.texera.amber.operator.loop.{LoopEndOpDesc, LoopStartOpDesc}
 import org.apache.texera.amber.operator.sleep.SleepOpDesc
+import org.apache.texera.amber.operator.udf.python.PythonUDFOpDescV2

Review Comment:
   Import is out of alphabetical order (`udf.python` before 
`source.scan.text`). scalafix has no `OrganizeImports` rule here so nothing 
will flag it.



##########
amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/promisehandlers/StartChannelHandler.scala:
##########
@@ -42,6 +42,12 @@ trait StartChannelHandler {
     try {
       val outputState = dp.executor.produceStateOnStart(portId.id)
       if (outputState.isDefined) {
+        // Deliberate "no loop" envelope defaults (loopCounter = 0,
+        // loopStartId = ""): this is operator-ORIGINATED boundary state, not
+        // a forwarded loop state, so it carries no LoopStart stamp. The
+        // Python LoopEnd runtime keys on that missing stamp to pass such
+        // states through instead of consuming them (see
+        // main_loop._process_state_frame).

Review Comment:
   Six identical lines here and in `EndChannelHandler.scala:46-51`. Two would 
carry it - "operator-originated boundary state, so no LoopStart stamp; see 
`main_loop._process_state_frame` for how the LoopEnd treats it" - and the long 
version already lives at the place that acts on it, so it won't drift out of 
sync.



##########
amber/src/main/python/core/runnables/main_loop.py:
##########
@@ -374,9 +374,23 @@ def _process_state_frame(self, frame: StateFrame) -> None:
             return
 
         if isinstance(executor, LoopEndOperator):

Review Comment:
   (About the LoopStart branch just above, at line 367 - outside the diff so I 
can't anchor it there.)
   
   The same mix-up exists on that side and the PR doesn't say what happens to 
it. An unstamped counter-0 frame falls through to `process_input_state`, and 
`LoopStartOperator.process_state` does `self.state.update(state)` 
(`operator.py:381`). So:
   
   - `Source -> statefulOp -> LoopStart`: the op's keys are merged into the 
loop variables. An op emitting `{"i": ...}` silently overwrites the loop 
counter; one emitting `{"table": ...}` trips `_reserved_name_error` at 
`produce_state_on_finish`.
   - `OuterStart -> statefulOp -> InnerStart -> ...`: the op's boundary state 
is absorbed by the inner LoopStart and never reaches downstream - the opposite 
of what the LoopEnd now does with the identical frame.
   
   I assume the merge is deliberate (that's how upstream state seeds the loop), 
but with the branch below now doing the opposite for the same shape of frame, 
that intent should be one sentence in this comment. If it isn't deliberate, 
it's a follow-up.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to