Add ready_stage for always-run setup-task synchronization points
- Dominant language
- Python
- Stars
- 11
- Forks
- 8
- Avg merge
- 9h 26m
- Merged PRs (30d)
- 6
Description
## Summary
Add a `ready_stage` abstraction alongside `setup_task`. A ready stage is an ordered point in a setup-task chain that always executes when traversal reaches it, rather than being skipped based on stamps, source time, hashes, invalidators, or `check_completed`.
The main use case is a reliable cross-object synchronization point: a dependency on another object's ready stage must run that stage every time, after first running any earlier setup tasks on the related object whose normal `should_run_task()` checks say they need to run.
## Proposed semantics
A `ready_stage` should be usable in a `SetupTaskMixin` class similarly to a `setup_task`, and participate in the same ordering model (`order` / `before` and class/MRO collection).
When `run_setup_tasks(stop_after=stage)` reaches a ready stage:
1. Earlier setup tasks are considered in order and run only when their existing `should_run_task()` logic says they should run.
2. The ready stage itself always runs.
3. Traversal stops at the stage when it is the `stop_after` target; later tasks are not considered.
4. Exceptions propagate normally, and the standard task start/run/fail instrumentation remains useful.
5. Each successful run has a completion time that can participate in dependency-last-run propagation.
A `cross_object_dependency` whose target is a ready stage should always call the related object's task chain through that stage. This should not depend on whether the stage has previously run or has a stamp. After the stage completes, later tasks in the depending object should see the stage's completion time, so they rerun when appropriate.
This is distinct from `ready=True` on `cross_object_dependency`: that option brings the entire related `AsyncInjectable` to ready before checking the target task, while a ready stage is an explicit ordered barrier within the related setup-task chain.
## Motivation
Today, `cross_object_dependency.check_completed_func()` asks the target wrapper's `should_run_task()` whether work is needed and calls `relationship.run_setup_tasks(stop_after=task)` only when that answer is true. A conventional `setup_task` therefore cannot express “always establish this stage now,” because its persisted completion mechanisms make later evaluations skip it.
A stage boundary is useful when earlier tasks remain incremental but some finalization/refresh step must always occur before another object evaluates or executes its own dependent work.
## API considerations
- Export `ready_stage` from `carthage.setup_tasks` alongside `setup_task`.
- Prefer a distinct wrapper type or an explicit capability on `TaskWrapperBase`, so `cross_object_dependency` can recognize stage semantics without special-casing function names.
- Define how direct calls to a bound ready-stage method behave; the least surprising behavior is the same as a bound setup task—execute the stage and emit normal task events.
- Inspection should identify a ready stage as always runnable without invoking it as a side effect.
- Preserve cycle detection and existing `ready=True` behavior.
## Suggested tests
- A normal traversal runs only invalidated prior setup tasks, always runs the stage, and does not run tasks after `stop_after=stage`.
- Repeating the traversal skips still-valid prior tasks but runs the stage again.
- A cross-object dependency on the stage performs that same traversal even after a previous successful run.
- The stage's new completion time invalidates the appropriate later tasks in the depending object.
- Stage failure prevents dependent tasks from running and is observable through task-failure instrumentation.
- Ordering, inheritance, dry-run, readonly, and concurrent/cyclic dependency behavior are explicitly covered or documented.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.