Skip to content

Cleanly Enforce Input Port Dependencies - #3460

Merged
Xiao-zhen-Liu merged 121 commits into
masterfrom
xiaozhen-cleanify-input-port-dep
Jun 13, 2025
Merged

Xiao-zhen-Liu merged 121 commits into
masterfrom
xiaozhen-cleanify-input-port-dep

Conversation

@Xiao-zhen-Liu

@Xiao-zhen-Liu Xiao-zhen-Liu commented Jun 4, 2025 •

Copy link
Copy Markdown
Contributor

This PR redesigns our mechanisms for enforcing an operator's input port dependency relationships.

Background and Motivation: Input Port Dependency Relationships

Besides blocking output ports that must be materialized (which creates a dependency relationship on the outgoing edges of a blocking output port, a.k.a., a blocking edge), which is precisely explained in our Pasta paper, many Texera operators also contain another kind of dependency relationship, which are those between two input ports of the same operator. Examples include :

  • HashJoinProbe: Build Port (a Dependee port, i.e., a port that is depended on by another port) -> Probe Port (a Depender port, i.e., a port that depends on another port)
  • ML Inference UDFs: Model Port -> Data Port

A few more notations:

  • In our context, Dependee/Depender ports are two names used specifically for the input port dependency relationship.
  • A dependee edge is the incoming edge of a dependee port.

So far we have been using a hack to enforce this kind of dependency relationship. We can take HashJoinProbe as a running example. In terms of scheduling, we have been treating a dependee edge the same as a blocking edge in the sense that it also converts to a region edge. But different from a blocking edge, we do not materialize a dependee edge.

Instead, we:

  • Let HashJoinProbe belong to both the dependee regions (the regions of the output port of the incoming edges of a dependee port) and depender regions (similarly defined).
  • We let a dependee port belong to the dependee region, and let the depender port belong to the depender region.
  • We execute the dependee region that only includes the dependee port, first.
  • Then we execute the depender region that includes the depender port and the output port(s).

This hack design has been causing many issues. The most critical issue is that it prevents a worker/region to be terminated when it is finished because we might need it for the next region.

New Design

In this PR, we use a new and cleaner design:

  • We materialize a dependee edge. Essentially, in the scheduling phase, a dependee edge is now exactly the same as a blocking edge.
  • In the execution phase, we use a two-phase execution scheme, with the 1st including only the dependee ports and the 2nd including all other ports (depender ports, input ports with no dependencies, output ports). This essentially is splitting a region into 2 mini-regions.

Note we have these assumption:

  • We only allow input port dependencies where the input ports of a region can be grouped as two layers, with one layer of “dependee” ports and another layer of “depender” ports. We do not allow the case where an input port can both be a dependee and a depender.
  • We only allow depender ports to send data to output ports. Depenee input ports cannot send data to output ports.

This is a temporary design that fits all the current use cases and can unblock region separation. We can generalize it in the future.

Code Changes

  • Hash Join Probe is modified to require HashPartition for its builder port.
  • Both schedulers are modified to include materializations on dependee edges.
  • RegionExecutionCoordinator and WorkflowExecutionCoordinator are updated with the two-phase execution scheme.

More details can be found in the code documentations.

Testing

The same testing workflow from #3425 can be used for testing in this PR.

Future TODOs

  • Ports can be removed from Region's internal structures as they are now redundant and can be derived from Operators.
  • Need to replace mutable.HashMap with concurrent.TrieMap in InputManager.scala and OutputManager.scala for thread safety.

@Yicong-Huang Yicong-Huang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much better! Some minor comments are not resolved.

Comment thread core/amber/src/main/python/core/architecture/packaging/output_manager.py Outdated
Xiao-zhen-Liu and others added 3 commits June 13, 2025 12:25
# Conflicts:
#	core/amber/src/main/scala/edu/uci/ics/amber/engine/architecture/worker/managers/InputPortMaterializationReaderThread.scala
…e/scheduling/RegionExecutionCoordinator.scala

Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com>
@Xiao-zhen-Liu
Xiao-zhen-Liu merged commit af212ce into master Jun 13, 2025
@Xiao-zhen-Liu
Xiao-zhen-Liu deleted the xiaozhen-cleanify-input-port-dep branch June 13, 2025 19:48
Xiao-zhen-Liu added a commit that referenced this pull request Jun 17, 2025
#3460 introduced 2-phase execution model for regions. This requires the
`InputManager` to check if an input port reader thread is finished
before starting it. But #3460 only implemented that logic for Java
workers.

This PR adds the same logic on Python workers.

Caveat: In Java `InputManager` we rely on `isPortCompleted` to do this
check and let that method check a reader thread's completion. However in
Python `InputManager`, `isPortCompleted` belongs to `WorkerPort`, which
does not currently include `InputPortMaterializationReaderRunnable`.
Hence I am currently exposing the reader thread completion check logic
in set_up_input_port_mat_reader_threads. We can do a future PR to make
the code cleaner and more unified.
Xiao-zhen-Liu added a commit that referenced this pull request Jun 20, 2025
### Implementation Issue

There is an issue with the current Output Port Materialization Writer
Thread's `closeOutputStorageWriterIfNeeded` method where we terminate
all the writer threads for all the ports when calling
`closeOutputStorageWriterIfNeeded` for only 1 port.

### Bug

The consequence of this incorrect logic is that an operator with
multiple output ports will not terminate all its output ports if there
are multiple output ports having materialization reader threads. Before
#3460 this issue was not exposed because we have no such use cases.
After #3460, A split operator connecting to a training operator will
have both its output ports materialized, causing Split operator to
misbehave.

### Fix
This PR fixes that issue by letting each output port only terminate its
own thread.
SarahAsad23 pushed a commit to madisonmlin/texera that referenced this pull request May 20, 2026
This PR redesigns our mechanisms for enforcing an operator's input port
dependency relationships.

### Background and Motivation: Input Port Dependency Relationships

Besides blocking output ports that must be materialized (which creates a
dependency relationship on the outgoing edges of a blocking output port,
a.k.a., a blocking edge), which is precisely explained in our Pasta
paper, many Texera operators also contain another kind of dependency
relationship, which are those between two input ports of the same
operator. Examples include :
- HashJoinProbe: `Build Port` (a **Dependee** port, i.e., a port that is
depended on by another port) -> `Probe Port` (a **Depender** port, i.e.,
a port that depends on another port)
- ML Inference UDFs: `Model Port -> Data Port`

A few more notations:

- _In our context, Dependee/Depender ports are two names used
specifically for the input port dependency relationship._
- _A **dependee edge** is the incoming edge of a dependee port._

So far we have been using a hack to enforce this kind of dependency
relationship. We can take HashJoinProbe as a running example. In terms
of scheduling, we have been treating a dependee edge the same as a
blocking edge in the sense that it also converts to a region edge. But
different from a blocking edge, we do not materialize a dependee edge.

Instead, we:
- Let HashJoinProbe belong to both the dependee regions (the regions of
the output port of the incoming edges of a dependee port) and depender
regions (similarly defined).
- We let a dependee port belong to the dependee region, and let the
depender port belong to the depender region.
- We execute the dependee region that only includes the dependee port,
first.
- Then we execute the depender region that includes the depender port
and the output port(s).

This hack design has been causing many issues. The most critical issue
is that it prevents a worker/region to be terminated when it is finished
because we _might_ need it for the next region.

### New Design

In this PR, we use a new and cleaner design:
- We materialize a dependee edge. Essentially, in the scheduling phase,
a dependee edge is now exactly the same as a blocking edge.
- In the execution phase, we use a two-phase execution scheme, with the
1st including only the dependee ports and the 2nd including all other
ports (depender ports, input ports with no dependencies, output ports).
This essentially is splitting a region into 2 mini-regions.

Note we have these assumption:

- We only allow input port dependencies where the input ports of a
region can be grouped as two layers, with one layer of “dependee” ports
and another layer of “depender” ports. We do not allow the case where an
input port can both be a dependee and a depender.
- We only allow depender ports to send data to output ports. Depenee
input ports cannot send data to output ports.

This is a temporary design that fits all the current use cases and can
unblock region separation. We can generalize it in the future.

### Code Changes

- Hash Join Probe is modified to require `HashPartition` for its builder
port.
- Both schedulers are modified to include materializations on dependee
edges.
- `RegionExecutionCoordinator` and `WorkflowExecutionCoordinator` are
updated with the two-phase execution scheme.

More details can be found in the code documentations.

### Testing
The same testing workflow from apache#3425 can be used for testing in this PR.

### Future TODOs

- Ports can be removed from `Region`'s internal structures as they are
now redundant and can be derived from Operators.
- Need to replace `mutable.HashMap` with `concurrent.TrieMap` in
`InputManager.scala` and `OutputManager.scala` for thread safety.

---------

Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com>
SarahAsad23 pushed a commit to madisonmlin/texera that referenced this pull request May 20, 2026
…che#3471)

apache#3460 introduced 2-phase execution model for regions. This requires the
`InputManager` to check if an input port reader thread is finished
before starting it. But apache#3460 only implemented that logic for Java
workers.

This PR adds the same logic on Python workers.

Caveat: In Java `InputManager` we rely on `isPortCompleted` to do this
check and let that method check a reader thread's completion. However in
Python `InputManager`, `isPortCompleted` belongs to `WorkerPort`, which
does not currently include `InputPortMaterializationReaderRunnable`.
Hence I am currently exposing the reader thread completion check logic
in set_up_input_port_mat_reader_threads. We can do a future PR to make
the code cleaner and more unified.
SarahAsad23 pushed a commit to madisonmlin/texera that referenced this pull request May 20, 2026
### Implementation Issue

There is an issue with the current Output Port Materialization Writer
Thread's `closeOutputStorageWriterIfNeeded` method where we terminate
all the writer threads for all the ports when calling
`closeOutputStorageWriterIfNeeded` for only 1 port.

### Bug

The consequence of this incorrect logic is that an operator with
multiple output ports will not terminate all its output ports if there
are multiple output ports having materialization reader threads. Before
apache#3460 this issue was not exposed because we have no such use cases.
After apache#3460, A split operator connecting to a training operator will
have both its output ports materialized, causing Split operator to
misbehave.

### Fix
This PR fixes that issue by letting each output port only terminate its
own thread.
yangzhang75 pushed a commit to yangzhang75/texera that referenced this pull request Jun 22, 2026
This PR redesigns our mechanisms for enforcing an operator's input port
dependency relationships.

### Background and Motivation: Input Port Dependency Relationships

Besides blocking output ports that must be materialized (which creates a
dependency relationship on the outgoing edges of a blocking output port,
a.k.a., a blocking edge), which is precisely explained in our Pasta
paper, many Texera operators also contain another kind of dependency
relationship, which are those between two input ports of the same
operator. Examples include :
- HashJoinProbe: `Build Port` (a **Dependee** port, i.e., a port that is
depended on by another port) -> `Probe Port` (a **Depender** port, i.e.,
a port that depends on another port)
- ML Inference UDFs: `Model Port -> Data Port`

A few more notations:

- _In our context, Dependee/Depender ports are two names used
specifically for the input port dependency relationship._
- _A **dependee edge** is the incoming edge of a dependee port._

So far we have been using a hack to enforce this kind of dependency
relationship. We can take HashJoinProbe as a running example. In terms
of scheduling, we have been treating a dependee edge the same as a
blocking edge in the sense that it also converts to a region edge. But
different from a blocking edge, we do not materialize a dependee edge.

Instead, we:
- Let HashJoinProbe belong to both the dependee regions (the regions of
the output port of the incoming edges of a dependee port) and depender
regions (similarly defined).
- We let a dependee port belong to the dependee region, and let the
depender port belong to the depender region.
- We execute the dependee region that only includes the dependee port,
first.
- Then we execute the depender region that includes the depender port
and the output port(s).

This hack design has been causing many issues. The most critical issue
is that it prevents a worker/region to be terminated when it is finished
because we _might_ need it for the next region.

### New Design

In this PR, we use a new and cleaner design:
- We materialize a dependee edge. Essentially, in the scheduling phase,
a dependee edge is now exactly the same as a blocking edge.
- In the execution phase, we use a two-phase execution scheme, with the
1st including only the dependee ports and the 2nd including all other
ports (depender ports, input ports with no dependencies, output ports).
This essentially is splitting a region into 2 mini-regions.

Note we have these assumption:

- We only allow input port dependencies where the input ports of a
region can be grouped as two layers, with one layer of “dependee” ports
and another layer of “depender” ports. We do not allow the case where an
input port can both be a dependee and a depender.
- We only allow depender ports to send data to output ports. Depenee
input ports cannot send data to output ports.

This is a temporary design that fits all the current use cases and can
unblock region separation. We can generalize it in the future.

### Code Changes

- Hash Join Probe is modified to require `HashPartition` for its builder
port.
- Both schedulers are modified to include materializations on dependee
edges.
- `RegionExecutionCoordinator` and `WorkflowExecutionCoordinator` are
updated with the two-phase execution scheme.

More details can be found in the code documentations.

### Testing
The same testing workflow from apache#3425 can be used for testing in this PR.

### Future TODOs

- Ports can be removed from `Region`'s internal structures as they are
now redundant and can be derived from Operators.
- Need to replace `mutable.HashMap` with `concurrent.TrieMap` in
`InputManager.scala` and `OutputManager.scala` for thread safety.

---------

Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com>
yangzhang75 pushed a commit to yangzhang75/texera that referenced this pull request Jun 22, 2026
…che#3471)

apache#3460 introduced 2-phase execution model for regions. This requires the
`InputManager` to check if an input port reader thread is finished
before starting it. But apache#3460 only implemented that logic for Java
workers.

This PR adds the same logic on Python workers.

Caveat: In Java `InputManager` we rely on `isPortCompleted` to do this
check and let that method check a reader thread's completion. However in
Python `InputManager`, `isPortCompleted` belongs to `WorkerPort`, which
does not currently include `InputPortMaterializationReaderRunnable`.
Hence I am currently exposing the reader thread completion check logic
in set_up_input_port_mat_reader_threads. We can do a future PR to make
the code cleaner and more unified.
yangzhang75 pushed a commit to yangzhang75/texera that referenced this pull request Jun 22, 2026
### Implementation Issue

There is an issue with the current Output Port Materialization Writer
Thread's `closeOutputStorageWriterIfNeeded` method where we terminate
all the writer threads for all the ports when calling
`closeOutputStorageWriterIfNeeded` for only 1 port.

### Bug

The consequence of this incorrect logic is that an operator with
multiple output ports will not terminate all its output ports if there
are multiple output ports having materialization reader threads. Before
apache#3460 this issue was not exposed because we have no such use cases.
After apache#3460, A split operator connecting to a training operator will
have both its output ports materialized, causing Split operator to
misbehave.

### Fix
This PR fixes that issue by letting each output port only terminate its
own thread.
renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Oct 15, 2026
…s END is processed (apache#8938)

### What changes were proposed in this PR?

`InputManager.isPortCompleted` now treats a port read from
materialization the same as a port fed by a link: it counts as completed
once END_CHANNEL has been processed on it. `EndChannelHandler` marks
that by setting `WorkerPort.completed`.

Before, such a port reported its reader thread's `finished` flag. The
thread sets that flag only after enqueuing END. So a DP thread waiting
on the queue, typically the worker that got no tuples, could run
`EndChannelHandler` before the flag was set. It then saw the port as not
completed and skipped `finalizeOutput`. Nothing called it again, so the
worker never finished and its region hung until the test deadline. That
is the intermittent `TimeoutException: 1.minutes+30.seconds` in
`LoopIntegrationSpec`.

```
Before:  reader enqueues END -> DP processes END, flag not set yet -> output never finalized -> region hangs
After:   reader enqueues END -> DP processes END, port completed   -> output finalized       -> region completes
```

Two related effects of the change:

- The flag could also be set while END was still queued. A worker with a
second materialized input no longer finalizes before that port's END has
been processed.
- `startInputPortReaderThreads` uses the same check to skip ports that
already completed. The second `StartWorker` of a two-phase region
arrives only after the coordinator has the port's `PortCompleted`, which
the worker sends after processing END. So it skips the same ports as
before.
- Nothing reads the reader thread's `finished` flag any more, so it is
removed, and it can't stand in for port completion again. The
reader-thread tests that read it now check the END_CHANNEL the reader
enqueues instead.

### Any related issues, documentation, discussions?

Closes apache#8937. The two `LoopIntegrationSpec` cases added in apache#8639 hit the
race most often; the race itself dates from apache#3460.

### How was this PR tested?

Tests first: the first commit adds the tests and fails on `main`, and
the second commit makes them pass.

- `InputManagerSpec`: a materialized port is completed once END has been
processed, even before its reader thread has returned. It is not
completed while END is still queued, even after the thread has returned.
- `EndChannelHandlerSpec`: when END arrives on a materialized input port
before its reader thread has returned, the worker emits the output
`FinalizePort` and `FinalizeExecutor`. On `main` it emits only the input
port's `FinalizePort`, which is the hang.

```
sbt "WorkflowExecutionService/testOnly *InputManagerSpec *EndChannelHandlerSpec *StartHandlerSpec *DataProcessorSpec *EndHandlerSpec *StartChannelHandlerSpec *AssignPortHandlerSpec *DPThreadSpec *WorkerSpec"
sbt "WorkflowExecutionService/scalafmtCheckAll" "WorkflowExecutionService/scalafixAll --check"
```

`InputPortMaterializationReaderThreadSpec` reads real Iceberg tables.
Locally only its two cases that don't need storage ran; CI runs the
rest.

`LoopIntegrationSpec`, run locally on Windows with the sbt JVM and its
Python workers pinned to 3 CPUs (affinity mask `0x7`), one case per JVM:

| Case | `main` | This PR |
|---|---|---|
| bind a `$i` reference after two branches of the loop body meet again |
3 of 5 runs hung on the first attempt; 1 also hung on the retry and
failed as on CI | 10 of 10 passed on the first attempt, 42–44 s each |
| bind the inner loop's variable where it shadows the outer loop's |
(fails on CI, see apache#8937) | 10 of 10 passed on the first attempt, 64–70 s
each |

Unpinned, the whole spec passes with this PR: 8 of 8 cases, 29–59 s
each, no retries.

```
AMBER_TEST_FILTER= STORAGE_ICEBERG_CATALOG_TYPE=postgres sbt "WorkflowExecutionService/testOnly *LoopIntegrationSpec -- -z \"after two branches of the loop body meet again\""
AMBER_TEST_FILTER=integration-only STORAGE_ICEBERG_CATALOG_TYPE=postgres sbt "WorkflowExecutionService/testOnly *LoopIntegrationSpec"
```

### Was this PR authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Claude Opus 5.5)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants