1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! # Slot-based admission control
//!
//! The controller is an optional layer before the supervisor registry.
//! It decides when a submitted task may be registered.
//!
//! Use direct `add*` methods when you do not need this admission step.
//! This module requires the `controller` feature.
//!
//! Use the controller when work with the same key must not overlap:
//! - one deployment per environment;
//! - one job per customer or resource;
//! - one rebuild per index;
//! - skip or replace duplicate work while a lane is busy.
//!
//! ## Slots and task names
//!
//! A **slot** is one sequential admission lane. The controller admits by slot, not by task name.
//!
//! A slot is the key returned by [`ControllerSpec::slot_name`].
//! If no slot is set, it defaults to the task name.
//! Use [`ControllerSpec::with_slot`] to group several different task names into one slot.
//!
//! Task names belong to the runtime registry. They must be unique across all
//! currently registered tasks, even when those tasks use different slots.
//!
//! ```text
//! task "deploy-main-42" ┐
//! task "deploy-main-43" ├── slot "deploy-main"
//! task "deploy-main-44" ┘
//! ```
//!
//! These are different runtime tasks, but the controller admits them one at a
//! time through the same slot. Different slots may be occupied at the same
//! time. The supervisor's global concurrency limit still applies to attempts.
//!
//! ## Admission policies
//!
//! When a slot is idle, every policy tries to start admission.
//! When it is busy, the policy decides what happens to the new submission.
//!
//! | Policy | Busy slot behavior |
//! |------------------------------------|------------------------------------------------------------|
//! | [`AdmissionPolicy::Queue`] | enqueue if the per-slot limit allows it; otherwise reject |
//! | [`AdmissionPolicy::Replace`] | create or replace the queue head; retire owner if needed |
//! | [`AdmissionPolicy::DropIfRunning`] | reject the submission |
//!
//! `Replace` changes only the next-item position: it creates or replaces the queue head.
//! A displaced head is rejected; FIFO items behind it stay queued.
//! A watched rejected submission resolves to [`TaskOutcome::Rejected`](crate::TaskOutcome::Rejected).
//!
//! ## Slot states
//!
//! In this table, `Next / Idle` means: start admission for the next queued item, or become `Idle` when no queued item can start.
//!
//! | Current state | Cause | Next state |
//! |---------------|----------------------------------------------------------------|------------------------------------------------------|
//! | `Idle` | a submission starts admission | `Admitting` |
//! | `Admitting` | registry accepts registration | `Running` |
//! | `Admitting` | registry rejects registration | Next / Idle |
//! | `Admitting` | `Replace` arrives | `Terminating` |
//! | `Running` | `Replace` arrives | `Terminating` |
//! | `Running` | terminal registry cleanup completes | Next / Idle |
//! | `Terminating` | pending registration is accepted | stay `Terminating`; request owner removal |
//! | `Terminating` | pending registration is rejected or terminal cleanup completes | Next / Idle |
//! | `Terminating` | another `Replace` arrives | stay `Terminating`; create or replace the queue head |
//!
//! Admitting + Replace is shown as `Terminating` in public snapshots.
//! Taskvisor first waits for the registration decision.
//! If registration succeeds, it then removes that owner before starting the replacement.
//!
//! Only `Replace` changes the public status to `Terminating`.
//! ID-based `remove*` and `cancel*` requests do not change the status by themselves.
//! The slot advances when the registry reports terminal cleanup.
//! `Queue` and `DropIfRunning` also leave the current owner's status unchanged.
//!
//! After a registered owner, the next queued task starts only after terminal registry cleanup.
//! At that point, the old managed runner has been joined and its task name is free.
//! If registration is rejected, no registered owner exists; the controller can try the next queued item as soon as it receives that direct decision.
//! Events are only for observability; they do not drive slot state.
//!
//! ## Public API
//!
//! - Build submissions with [`ControllerSpec::queue`], [`ControllerSpec::replace`], or [`ControllerSpec::drop_if_running`].
//! - Configure with [`SupervisorBuilder::with_controller`](crate::SupervisorBuilder::with_controller).
//! - Submit with [`SupervisorHandle::submit`](crate::SupervisorHandle::submit) or [`SupervisorHandle::submit_and_watch`](crate::SupervisorHandle::submit_and_watch).
//! Their `try_*` forms return immediately instead of waiting when the command channel is full.
//! - When application correlation must exist before lifecycle events can start,
//! call [`SupervisorHandle::prepare_submission`](crate::SupervisorHandle::prepare_submission),
//! store its [`TaskId`](crate::TaskId), then consume the returned [`PreparedSubmission`].
//! - Remove or cancel by the [`TaskId`](crate::TaskId) returned from submission.
//! - Read current slot state with [`SupervisorHandle::controller_snapshot`](crate::SupervisorHandle::controller_snapshot).
//!
//! Slots control admission only. There is no slot-wide cancel/remove operation.
//! Canceling a registered owner by ID or name does not automatically purge a
//! queued replacement for the same slot.
//!
//! ## Example
//!
//! ```rust,no_run
//! use taskvisor::{
//! ControllerConfig, ControllerSpec, Supervisor, SupervisorConfig, TaskFn, TaskSpec,
//! };
//!
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let supervisor = Supervisor::builder(SupervisorConfig::default())
//! .with_controller(ControllerConfig::default())
//! .build();
//! let handle = supervisor.serve();
//!
//! let task = TaskFn::arc("refresh-customer-42", |_ctx| async { Ok(()) });
//! let request = ControllerSpec::queue(TaskSpec::once(task)).with_slot("customer-42");
//! let (_id, waiter) = handle.submit_and_watch(request).await?;
//!
//! assert!(waiter.wait().await?.is_success());
//! handle.shutdown().await?;
//! # Ok(())
//! # }
//! ```
//!
//! A successful controller submission call confirms only that the controller command queue accepted the submission.
//! Slot admission and registry registration happen later.
//! Use `submit_and_watch` or `try_submit_and_watch` when the final result matters.
//!
//! The returned `TaskId` is allocated before runtime admission.
//! If admission succeeds, the runtime uses the same ID.
//! Before admission, it still identifies queued work for cancellation, outcomes, and event correlation.
//! A [`PreparedSubmission`] exposes that ID before controller intake. Preparing
//! alone does not enqueue work or publish an event.
//!
//! [`ControllerConfig::queue_capacity`] bounds the controller command queue and separately caps registry-backed remove/cancel operations.
//! A new `Queue` submission is rejected when the slot's pending depth is already [`ControllerConfig::max_slot_queue`] or greater.
//! The current owner is not part of this depth, but a replacement head is.
//! `Replace` itself may create or replace that head even when the limit is zero.
//!
//! When the controller or registry rejects a watched submission before registration, its waiter resolves to [`TaskOutcome::Rejected`](crate::TaskOutcome::Rejected).
//! After registration, the waiter resolves to the runtime task's terminal outcome.
//!
//! ## Events
//!
//! Controller-specific event kinds are:
//! - [`EventKind::ControllerSlotTransition`](crate::EventKind::ControllerSlotTransition)
//! - [`EventKind::ControllerSubmitted`](crate::EventKind::ControllerSubmitted)
//! - [`EventKind::ControllerRejected`](crate::EventKind::ControllerRejected)
//!
//! Removing a submission that is still in a slot queue also publishes the standard [`EventKind::TaskRemoveRequested`](crate::EventKind::TaskRemoveRequested).
//! A registry rejection, such as a duplicate task name, publishes the standard [`EventKind::TaskAddFailed`](crate::EventKind::TaskAddFailed).
//!
//! Events are best-effort observability. For one reliable final result, use `submit_and_watch` or `try_submit_and_watch`.
//!
//! ## Invariants
//!
//! - At most one registered runtime task may own a slot.
//! - Queued submissions are not handed to the runtime until they become the slot owner.
//! - `Queue` preserves FIFO order among submissions that remain pending.
//! - `Replace` creates or overwrites only the queue head; later items stay in order.
//! - The next owner starts only after reliable registry cleanup of the old owner.
//! - Runtime shutdown closes controller intake, resolves pending work, and joins the controller loop before the shared shutdown result is returned.
//!
//! A snapshot is a rolling observability view, not a transaction.
//! See [`ControllerSnapshot`] for its consistency limits.
pub use ;
pub use AdmissionPolicy;
pub use ControllerConfig;
pub use Controller;
pub use ControllerError;
pub use PreparedSubmission;
pub use ControllerSpec;