comphead commented on PR #2670:
URL: 
https://github.com/apache/datafusion-ballista/pull/2670#issuecomment-6452684383

   Thanks @andygrove, splitting this out of #2657 makes it much easier to 
review. Notes below, most important first. Items marked (unverified) are 
expectations I did not test.
   
   ### Correctness and design
   
   1. **`remove_job` does not release a producer blocked on an unclaimed queue 
in the real writer.** `registry.rs` documents that `remove_job` closes 
unclaimed queues, and 
`removing_the_job_releases_a_producer_blocked_on_an_unclaimed_queue` passes 
because the test drops its `Arc<ProducerOutputs>`. In `execute_pipelined` 
(`writer.rs:232-272`) the writer keeps `outputs` alive until the task ends, so 
the receivers stay open and the producer waits out the claim timeout instead. 
Closing the receivers explicitly in `remove_job` and `remove_stage` 
(`rx.close()`) would fix it. The test should hold the `Arc` the way the writer 
does.
   2. **`fetch_pipelined_partition` may retry after the server has answered.** 
The `stream.message().await` error arm goes through `is_transport_failure`, so 
a reset after the response headers arrive becomes `GrpcConnectionError` and is 
retried. If the executor claims the queue before it answers (the docs imply it 
does, but the server side is not in this PR), the retry finds a claimed or 
released queue. It then waits the full registration timeout and reports "timed 
out waiting for pipelined producer to start", and the batches the first stream 
took are lost. Consider mapping only errors from `do_get().await` to 
`GrpcConnectionError`. A test that resets the connection after the headers 
would show which case applies.
   3. **`is_transport_failure` matches `status.message() == "transport 
error"`.** String matching is fragile. `Unavailable` is also retried even when 
the server sent it, for example from a draining executor. Source-chain 
detection alone may be enough.
   4. **Memory is outside the DataFusion `MemoryPool`.** Queues 
(`queue_capacity` batches per output partition per task), the per-output 
`BatchCoalescer`s and the broadcast `ReplayBuffer` (kept until `remove_job`) 
are untracked. Is accounting planned for a later PR? Also, where is the 
broadcast size bound enforced? The comment points at the planner threshold, 
which is not in this PR.
   5. **The claim timeout is an absolute deadline from `ProducerOutputs::new`, 
not an idle timeout.** A consumer plan that calls `execute` on a child lazily, 
after a long first phase, would hit "no consumer opened output partition" while 
the producer's queue is full. The hash join probe side is opened eagerly, so it 
is fine. Other plan shapes may not be (unverified). A doc note on the plan 
shapes this relies on would help.
   6. **A second `open` of a released queue gives a misleading error.** Once 
all queues are claimed, `release` drops the slot. A repeated open then creates 
a new `Waiting` slot and fails after the timeout with "timed out ... to start". 
An explicit "already read" error would be clearer, and a test for it is missing.
   7. **`create_grpc_server`: `.max_concurrent_streams(None)` appears to be a 
no-op.** tonic 0.14.6 defaults it to `None` (`transport/server/mod.rs:128`) and 
passes it through to hyper unchanged. It is fine as documentation, but the 
comment reads as if it changes behavior for the scheduler and executor servers.
   8. **`SharedMap` in `executor/src/client_pool.rs` is unbounded.** Slots are 
never removed, and `evict_idle` ignores them. With executor churn, slots for 
dead `(host, port, config)` keys accumulate for the life of the process. 
Removing the slot on discard, and evicting slots with no outside `Arc` after 
the idle timeout, would bound it.
   9. **No task or executor loss recovery.** Queues are one-shot, so any loss 
fails the job. This is documented in the config text and looks like a 
deliberate first step. It would be worth stating in #2652 as a non-goal. For 
comparison, Flink restarts the pipelined region and Spark retries a stage from 
shuffle files.
   
   ### Reuse and simplification
   
   - `open_remote` copies `with_retry` and `is_retriable_fetch_error` 
(`shuffle_reader.rs:943-980`). Making those `pub(crate)` removes a second retry 
policy.
   - `fetch_pipelined_partition` repeats the `do_get` and schema-read sequence 
in `execute_do_get`. A shared helper that takes the error mapper would do.
   - `ProducerKey` duplicates `TaskKey` (`serde/scheduler/mod.rs`). Using 
`TaskKey` avoids a `String` allocation per lookup.
   - `output_partition_ids` (writer and `Routing`) re-derives what 
`compute_global_output_partition_ids` computes, and nothing outside `writer.rs` 
calls it. Keep one.
   - The hash-partitioning proto encode is copied from `SortShuffleWriterExec` 
(`serde/mod.rs` ~1222 vs ~1176). A small helper would serve both.
   - `with_partitions` repeats the slicing in `task_builder.rs`, and 
`filter_map` silently drops out-of-range indexes where 
`slice_by_global_partition` errors. Only tests call it today, so it could move 
to the scheduler PR.
   - Writer plumbing: `Arc<HashMap<usize, Output>>` and `Arc<AtomicUsize>` are 
threaded through five signatures, so one `Outputs` struct would do. 
`Option<Mutex<BatchCoalescer>>` plus `.expect("hash outputs always coalesce")` 
carries an invariant at runtime. `finish_outputs` does not use `&self`. 
`Routing::Hash` rebuilds `Partitioning::Hash` for every input.
   - Small items:
     - `try_new_broadcast` returns `Result` but cannot fail.
     - `ProducerOutputs::schema()` is `pub` and used once.
     - `producer_count` is only used by tests.
     - The producer failure is stored twice, in `ProducerOutputs.failure` and 
in `ReplayBuffer.outcome`.
     - The `io-util` tokio feature is only needed by `copy_bidirectional` in 
`tests.rs`, so it can go in dev-dependencies.
   - Metric names differ from the siblings (`ShuffleWriteMetrics`, 
`local_partitions` and `remote_partitions`).
   - Question on design: DataFusion's `RepartitionExec` already hash-partitions 
and coalesces. Was reimplementing it in the writer (about 120 lines plus the 
proto field and serde) a deliberate choice to avoid a channel hop?
   
   ### Efficiency
   
   - Each output's `Mutex<BatchCoalescer>` is shared by all input tasks and 
held during the copy. Rows are also copied twice, once by `partition_iter` and 
once by the coalescer. Per-input coalescers would avoid the lock at a memory 
cost (unverified, needs a benchmark).
   - `drive_hashed` awaits sends in order, so one full queue (default capacity 
2) stalls the other partitions of that input.
   - `shared()` allocates the key (host `String` and config clone) and takes 
the DashMap shard lock and the `connecting` mutex on every open, even when a 
connection exists. Checking the existing connection first would keep the hot 
path read-only.
   - `execute()` builds `GrpcClientConfig` and `shared_connection_config` even 
when every producer is local.
   - `RegistryState::closed` clones `job_id` for each lookup under the registry 
mutex.
   - Each `PipelinedPartitionLocation` carries a full `ExecutorMetadata`, and 
`execute` deep-clones the `Vec` per partition. This follows 
`PartitionLocation`, but plan size grows with P x T on wide jobs.
   - Small items: a `HashMap` lookup per piece where partition ids are dense, 
and `get_array_memory_size()` per batch for the bytes metric.
   - Per-stream throughput is roughly stream window / RTT, so about 400 Mbit/s 
at 10 ms RTT with the 512 KiB default (my arithmetic, not measured). A note on 
how the default was chosen would help.
   
   ### Tests
   
   - Overlap: `tests::producer_fails_when_no_consumer_opens_its_full_queue` vs 
`registry::producer_gives_up_on_a_full_queue_nobody_opens`, and 
`tests::broadcast_outputs_replay_to_every_consumer_task` vs 
`registry::broadcast_output_replays_to_every_consumer`.
   - `queue_can_be_opened_once_and_is_released_once_claimed` never opens a 
second time, so the "once" half is untested.
   - `pipelined_queue_capacity_is_never_zero` only checks `.max(1)`.
   - `unused_port()` binds, drops and reuses the port, which is racy. Keeping 
the listener alive avoids that.
   - `ProducerOutputs::new(schema(), [0], false, 1, None, WAIT)` is repeated 
about ten times. A helper would shorten the tests.
   


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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to