aglinxinyuan opened a new issue, #8698:
URL: https://github.com/apache/texera/issues/8698

   ### What happened?
   
   `MainLoop._send_ecm_to_data_channels` builds one ECM per output channel and 
sends each with its own `OutputManager.emit_ecm(to, ecm)` call, and `emit_ecm` 
calls `flush(to, ecm)` on the partitioner of every output link. The 
round-robin, hash, range and broadcast partitioners yield only when `to` is one 
of their receivers. `OneToOnePartitioner.flush` ignores `to` and yields its 
pending batch (if any) and the ECM on every call. So whichever channel is 
flushed first takes the one-to-one link's pending tuples, and every channel 
that is not the link's receiver gets an extra copy of its ECM. This applies to 
StartChannel, EndChannel, and the ECMs a worker forwards from upstream 
(checkpoint, state-retrieval and reconfiguration ECMs), which go through the 
same `emit_ecm` call one channel at a time.
   
   Default settings get there. Stable Merge Sort is a `manyToOnePhysicalOp` and 
keeps its `SinglePartition` input requirement. A Python UDF's output is 
`UnknownPartition`, which does not satisfy it, so the link from a Python UDF 
into a Stable Merge Sort is a `OneToOnePartitioning`. A Python UDF (one worker, 
the default) whose output port feeds a Stable Merge Sort and any other operator 
therefore has a one-to-one link next to another output link. With a round-robin 
link to R1 and R2 added before a one-to-one link to C, and tuples 1, 2, 3 
pending at EndChannel:
   
   ```
           Now                          Expected
   R1   [1, 3] END [1, 2, 3] END     [1, 3] END
   R2   [2] END END                  [2] END
   C    END                          [1, 2, 3] END
   ```
   
   With the links added the other way round, C gets its tuples, and R1 and R2 
each get an extra END ahead of their own tuples and END. The order follows the 
order in which the worker receives its links' `AddPartitioning` requests, and 
the coordinator sends those concurrently.
   
   The extra END is not harmless. A receiver's alignment keeps, per ECM id, the 
set of channels the ECM has arrived on, and both ENDs carry the id 
`EndChannel`, so a repeat on the same channel looks like a fresh arrival. A 
receiver whose port has only this channel aligns on both ENDs, and the 
misrouted tuples between them arrive after its port has completed. A Python 
receiver whose last input port this was hangs: `complete()` stopped its 
`DataProcessor` at the first END, so the next tuple or END leaves `MainLoop` 
waiting in `_switch_context`, and the worker stops answering control messages 
such as `EndWorker`. By the code, a Scala receiver instead processes those 
tuples and runs its end-of-input handling a second time: `onFinish` again, a 
second `PortCompleted`, and, if that was its last input port, a second END to 
every downstream channel. With a second upstream worker, the early END plus the 
other worker's END complete the port before this worker's data behind the early 
END is read
 .
   
   This worked while end of input was one pass in which every partitioner sent 
its own payloads to its own receivers. #3245 added the per-channel loop, at 
first only for the markers a worker forwards. #3445 moved its receiver check 
from `OutputManager` into each partitioner's `flush(to, ...)`, and the 
one-to-one partitioner's `flush` never got it. #3432 then sent StartChannel and 
EndChannel through the same loop.
   
   The input-port materialization reader depends on `flush` ignoring the 
receiver. It builds its `OneToOnePartitioner` with the worker's 
`ActorVirtualIdentity`, and the partitioner looks for the channel whose 
sender's name equals the id it is given. No channel matches: the id is not a 
name string, and every channel the coordinator gives a reader is sent by the 
reader's virtual actor (`getFromActorIdForInputPortStorage`), not by the 
worker. So the partitioner never sets `receiver`. A replay shorter than one 
batch still finishes, because its tuples go out with the EndChannel flush. Once 
a replay reaches one batch (the workflow's data transfer batch size, 400 by 
default), `add_tuple_to_batch` raises `AttributeError` and `run()` only logs 
it. None of the tuples reach the operator, EndChannel is never sent, nothing 
restarts the reader, and the execution stays running. Default settings get 
there too: a one-worker 2-in Python UDF requires a single partition on its 
`model` port, and that por
 t always reads from materialized storage because the `tuples` port depends on 
it, so a `model` input of 400 tuples or more leaves the run hanging.
   
   ### How to reproduce?
   
   Run from `amber/src/main/python`, with the generated `proto` package in 
place:
   
   ```python
   from core.architecture.packaging.output_manager import OutputManager
   from core.models import Schema, Tuple
   from core.models.payload import DataFrame
   from core.util import set_one_of
   from proto.org.apache.texera.amber.core import (
       ActorVirtualIdentity,
       ChannelIdentity,
       PhysicalLink,
       PortIdentity,
   )
   from proto.org.apache.texera.amber.engine.architecture.rpc import 
EmbeddedControlMessage
   from proto.org.apache.texera.amber.engine.architecture.sendsemantics import (
       OneToOnePartitioning,
       Partitioning,
       RoundRobinPartitioning,
   )
   
   me = "Worker:WF0-S-main-0"
   
   
   def channel(to):
       return ChannelIdentity(ActorVirtualIdentity(me), 
ActorVirtualIdentity(to), False)
   
   
   om = OutputManager(me)
   om.add_output_port(PortIdentity(0, False), Schema(raw_schema={"k": 
"INTEGER"}))
   om.add_partitioning(  # a link to a two-worker operator
       PhysicalLink(to_port_id=PortIdentity(1)),
       set_one_of(
           Partitioning,
           RoundRobinPartitioning(batch_size=10, channels=[channel("R1"), 
channel("R2")]),
       ),
   )
   om.add_partitioning(  # a link to a Stable Merge Sort
       PhysicalLink(to_port_id=PortIdentity(2)),
       set_one_of(
           Partitioning, OneToOnePartitioning(batch_size=10, 
channels=[channel("C")])
       ),
   )
   for k in (1, 2, 3):
       list(om.tuple_to_batch(Tuple({"k": k})))
   
   # What MainLoop._send_ecm_to_data_channels does at EndChannel:
   ecm = EmbeddedControlMessage()
   for ch in om.get_output_channel_ids():
       sent = [
           p.frame.to_pydict()["k"] if isinstance(p, DataFrame) else "END"
           for p in om.emit_ecm(ch.to_worker_id, ecm)
       ]
       print(ch.to_worker_id.name, sent)
   ```
   
   It prints
   
   ```
   R1 [[1, 3], 'END', [1, 2, 3], 'END']
   R2 [[2], 'END', 'END']
   C ['END']
   ```
   
   where each receiver should get its own tuples and one END: `R1 [[1, 3], 
'END']`, `R2 [[2], 'END']`, `C [[1, 2, 3], 'END']`.
   
   For the reader, replay 400 tuples through a one-to-one partitioning shaped 
like the one the coordinator sends (one channel from the reader's virtual actor 
to the worker):
   
   ```python
   from unittest.mock import MagicMock, patch
   
   from core.models import Schema, Tuple
   from core.models.internal_queue import ECMElement
   from core.storage.runnables.input_port_materialization_reader_runnable 
import (
       InputPortMaterializationReaderRunnable,
   )
   from core.util import set_one_of
   from core.util.virtual_identity import 
get_from_actor_id_for_input_port_storage
   from proto.org.apache.texera.amber.core import ActorVirtualIdentity, 
ChannelIdentity
   from proto.org.apache.texera.amber.engine.architecture.sendsemantics import (
       OneToOnePartitioning,
       Partitioning,
   )
   
   uri = "vfs:///wf/0/exec/0/result/op-a"
   worker = ActorVirtualIdentity("Worker:WF0-udf-main-0")
   # The partitioning ResourceAllocator.scala sends a reader of a one-worker
   # operator: one channel from the reader's virtual actor to the worker.
   partitioning = OneToOnePartitioning(
       batch_size=400,
       channels=[
           ChannelIdentity(
               get_from_actor_id_for_input_port_storage(uri, worker), worker, 
False
           )
       ],
   )
   queue = MagicMock()
   reader = InputPortMaterializationReaderRunnable(
       uri, queue, worker, set_one_of(Partitioning, partitioning)
   )
   
   schema = Schema(raw_schema={"k": "INTEGER"})
   result, states = MagicMock(), MagicMock()
   result.get.return_value = iter(Tuple({"k": k}) for k in range(400))
   states.get.return_value = iter([])
   with patch(
       
"core.storage.runnables.input_port_materialization_reader_runnable.DocumentFactory"
   ) as factory:
       factory.open_document.side_effect = [(result, schema), (states, None)]
       reader.run()
   
   print("finished:", reader.finished())
   sent = [
       c.args[0].payload.id.id if isinstance(c.args[0], ECMElement) else "data"
       for c in queue.put.call_args_list
   ]
   print("sent:", sent)
   ```
   
   It prints `finished: False` and `sent: ['StartChannel']`: no data and no 
EndChannel.
   
   ### Version/Branch
   
   1.4.0-incubating-SNAPSHOT (main)
   
   ### Commit Hash (Optional)
   
   d102e26978cf0a377992b7d3ded03eaccd5d5c75
   
   ### What browsers are you seeing the problem on?
   
   _No response_
   
   ### Relevant log output
   
   ```shell
   # The reader repro, in the Python worker's stderr:
   ERROR | 
core.storage.runnables.input_port_materialization_reader_runnable:run:183 - 
'OneToOnePartitioner' object has no attribute 'receiver'
   ```
   


-- 
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