Problem
When a child task needs to spawn peer tasks under the same parent (flat hierarchy), it must manually extract and thread the parent's task ID:
impl TypedExecutor<ScanL1DirTask> for ScanL1DirExecutor {
async fn execute(
&self,
task: ScanL1DirTask,
ctx: DomainTaskContext<'_, Scanner>,
) -> Result<(), TaskError> {
// ... scan directory, discover subdirectories ...
// Must manually read orchestrator ID from own record
let orchestrator_id = ctx.record().parent_id.unwrap();
for subdir in child_dirs {
ctx.domain::<Scanner>()
.submit_with(ScanL1DirTask {
bucket: task.bucket.clone(),
dir_prefix: subdir,
scan_start_ns: task.scan_start_ns,
scope: task.scope.clone(),
})
.parent(orchestrator_id) // ← manual threading
.await?;
}
Ok(())
}
}
This pattern has two issues:
-
Boilerplate and fragility. Every child executor that spawns peers must remember to read ctx.record().parent_id and pass it via .parent(id). If a developer forgets the .parent(id) call, the new task becomes a root task instead of a sibling — silently breaking the orchestrator's child tracking. finalize() fires too early (doesn't wait for the orphaned task) and the progress reporter misses it. This is a subtle bug with no compile-time or runtime error.
-
Unwrap on parent_id. The child has to unwrap() the parent_id Option, which panics if the task is somehow submitted without a parent (e.g. during testing or if the orchestrator pattern is refactored). Defensive code adds a TaskError fallback, but the real fix is not requiring manual access at all.
Concrete use case
We're building a BFS filesystem scanner. An orchestrator (ScanL1Task) spawns a root directory task (ScanL1DirTask) via spawn_child_with(). Each directory task scans one directory, then spawns sibling tasks for each subdirectory — all as direct children of the orchestrator (flat hierarchy, not nested). At 238K directories, every directory task repeats this manual parent-threading pattern.
The flat hierarchy is intentional: the orchestrator's finalize() runs when all directories are done (not when just the root dir is done), and fail_fast(false) means individual directory failures don't cancel the scan. Nesting children under each dir task would create a tree where a mid-level failure cascades and finalize semantics change.
Proposal
Add ctx.spawn_sibling_with(task) that auto-inherits parent_id from the spawning task's own record:
// Before (manual):
let orchestrator_id = ctx.record().parent_id.unwrap();
ctx.domain::<Scanner>()
.submit_with(task)
.parent(orchestrator_id)
.await?;
// After:
ctx.spawn_sibling_with(task).await?;
Semantics:
- Sets
parent_id on the new task to self.record().parent_id (same parent as the spawning task)
- Returns error if the spawning task has no
parent_id (instead of silently creating a root task)
- Returns
SubmitOutcome like other submit methods
- Should support the builder pattern for priority/tags/fail_fast:
ctx.spawn_sibling_with(task).priority(Priority::BACKGROUND).await?
This mirrors the existing spawn_child_with() / submit_with().parent() relationship:
| Method |
parent_id on new task |
submit_with(task) |
None (root task) |
submit_with(task).parent(id) |
Explicit ID |
ctx.spawn_child_with(task) |
Current task's ID |
ctx.spawn_sibling_with(task) |
Current task's parent_id (proposed) |
Problem
When a child task needs to spawn peer tasks under the same parent (flat hierarchy), it must manually extract and thread the parent's task ID:
This pattern has two issues:
Boilerplate and fragility. Every child executor that spawns peers must remember to read
ctx.record().parent_idand pass it via.parent(id). If a developer forgets the.parent(id)call, the new task becomes a root task instead of a sibling — silently breaking the orchestrator's child tracking.finalize()fires too early (doesn't wait for the orphaned task) and the progress reporter misses it. This is a subtle bug with no compile-time or runtime error.Unwrap on parent_id. The child has to
unwrap()theparent_idOption, which panics if the task is somehow submitted without a parent (e.g. during testing or if the orchestrator pattern is refactored). Defensive code adds aTaskErrorfallback, but the real fix is not requiring manual access at all.Concrete use case
We're building a BFS filesystem scanner. An orchestrator (
ScanL1Task) spawns a root directory task (ScanL1DirTask) viaspawn_child_with(). Each directory task scans one directory, then spawns sibling tasks for each subdirectory — all as direct children of the orchestrator (flat hierarchy, not nested). At 238K directories, every directory task repeats this manual parent-threading pattern.The flat hierarchy is intentional: the orchestrator's
finalize()runs when all directories are done (not when just the root dir is done), andfail_fast(false)means individual directory failures don't cancel the scan. Nesting children under each dir task would create a tree where a mid-level failure cascades and finalize semantics change.Proposal
Add
ctx.spawn_sibling_with(task)that auto-inheritsparent_idfrom the spawning task's own record:Semantics:
parent_idon the new task toself.record().parent_id(same parent as the spawning task)parent_id(instead of silently creating a root task)SubmitOutcomelike other submit methodsctx.spawn_sibling_with(task).priority(Priority::BACKGROUND).await?This mirrors the existing
spawn_child_with()/submit_with().parent()relationship:submit_with(task)submit_with(task).parent(id)ctx.spawn_child_with(task)ctx.spawn_sibling_with(task)