Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/controller/core/admission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
},
events::{Event, EventKind},
identity::TaskId,
reasons,
};

use super::{AdmissionResult, CompletionResult, Controller, RemovalResult, Submission};
Expand Down Expand Up @@ -179,7 +180,7 @@ impl Controller {
| SlotPhase::Terminating { .. },
AdmissionPolicy::DropIfRunning,
) => {
let reason = format!("dropped: slot busy ({})", slot.status_label());
let reason = format!("{} ({})", reasons::DROP_IF_RUNNING, slot.status_label());
self.bus.publish(
Event::new(EventKind::ControllerRejected)
.with_task(Arc::clone(&slot_name))
Expand Down
28 changes: 22 additions & 6 deletions src/core/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,6 @@ impl SupervisorBuilder {
Ok(self)
}

/// Sets how many recent events the broadcast bus keeps.
pub fn with_bus_capacity(mut self, bus_capacity: NonZeroUsize) -> Self {
self.runtime = self.runtime.with_bus_capacity(bus_capacity);
self
}

/// Sets the event-bus capacity from a raw integer.
///
/// # Errors
Expand Down Expand Up @@ -155,6 +149,12 @@ impl SupervisorBuilder {
Ok(self)
}

/// Sets how many recent events the broadcast bus keeps.
pub fn with_bus_capacity(mut self, bus_capacity: NonZeroUsize) -> Self {
self.runtime = self.runtime.with_bus_capacity(bus_capacity);
self
}

/// Replaces the subscribers that receive best-effort lifecycle events.
pub fn with_subscribers(mut self, subscribers: Vec<Arc<dyn Subscribe>>) -> Self {
self.subscribers = subscribers;
Expand All @@ -175,6 +175,22 @@ impl SupervisorBuilder {
///
/// It is safe to call outside Tokio.
/// The method allocates channels and stores configuration, but does not spawn tasks.
///
/// ## Example
///
/// ```rust
/// use std::time::Duration;
/// use taskvisor::{SupervisorBuilder, SupervisorConfig};
///
/// let supervisor = SupervisorBuilder::new(SupervisorConfig::default())
/// .with_grace(Duration::from_secs(15))
/// .build();
///
/// assert_eq!(
/// supervisor.runtime_config().grace(),
/// Duration::from_secs(15)
/// );
/// ```
#[must_use]
pub fn build(self) -> Arc<Supervisor> {
let bus = Bus::new(self.runtime.bus_capacity().get());
Expand Down
3 changes: 3 additions & 0 deletions src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use std::time::Duration;

use thiserror::Error;

/// Default capacity of the event bus and registry command queue.
const DEFAULT_CAPACITY: NonZeroUsize = NonZeroUsize::new(1024).unwrap();

/// Default deadline for draining subscriber queues during shutdown.
const DEFAULT_SUBSCRIBER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);

/// Error from a checked configuration setter.
Expand Down
23 changes: 11 additions & 12 deletions src/core/handle.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! # Manage a running supervisor
//!
//! [`Supervisor::serve`](crate::Supervisor::serve) returns a [`SupervisorHandle`].
//! Use it to add, inspect, stop, and watch tasks while the service is running.
//!
//! Direct state-changing `add*`, `remove*`, and `cancel*` commands use one bounded management queue.
//! A regular method waits for capacity; its `try_*` form fails fast when that queue is full.
Expand Down Expand Up @@ -34,8 +33,8 @@ use super::outcome::TaskWaiter;
/// Dropping the last public `Supervisor` or handle sends best-effort cancellation, but cannot wait for cleanup.
/// Call [`shutdown`](Self::shutdown) for a confirmed shutdown result.
///
/// Use [`remove`](Self::remove) to request a stop and return after the request is accepted.
/// Use [`cancel`](Self::cancel) to wait until the task is joined and its name and identity are released.
/// > Use [`remove`](Self::remove) to request a stop and return after the request is accepted.
/// > Use [`cancel`](Self::cancel) to wait until the task is joined and its name and identity are released.
///
/// ## Example
///
Expand Down Expand Up @@ -132,9 +131,10 @@ impl SupervisorHandle {

/// Adds a task and returns a waiter for its final outcome.
///
/// Registration is the same as [`add`](Self::add). The [`TaskWaiter`] resolves after all retries end,
/// the registry joins the task actor, and registry membership is removed.
/// It uses a direct completion channel, not best-effort lifecycle events.
/// Registration is the same as [`add`](Self::add).
/// The [`TaskWaiter`] resolves after all retries end, the registry joins the task actor, and registry membership is removed.
///
/// > It uses a direct completion channel, not best-effort lifecycle events.
///
/// # Errors
///
Expand Down Expand Up @@ -191,8 +191,8 @@ impl SupervisorHandle {
/// `Ok(false)` means the identity was unknown, already finished, or already claimed by another stop request.
///
/// For a registered task, the method returns before final cleanup.
/// Use [`cancel`](Self::cancel) when you need to wait until termination.
/// Removing queued controller work is complete when this method returns.
/// > Use [`cancel`](Self::cancel) when you need to wait until termination.
///
/// # Errors
///
Expand Down Expand Up @@ -251,8 +251,7 @@ impl SupervisorHandle {
/// Returns the authoritative registry view as `(id, name)` pairs.
///
/// The list comes from the registry and is sorted by [`TaskId`].
/// It includes every registry entry, whether its actor is running, waiting for a permit,
/// backoff, or restart interval, already finished but not cleaned up, or being removed.
/// It includes every registry entry, whether its actor is running, waiting for a permit, backoff, or restart interval, already finished but not cleaned up, or being removed.
///
/// See [`alive_snapshot`](Self::alive_snapshot) for the best-effort list of task names currently marked alive.
pub async fn list(&self) -> Vec<(TaskId, Arc<str>)> {
Expand All @@ -273,7 +272,7 @@ impl SupervisorHandle {
/// Returns whether the cache currently marks any run with this name as alive.
///
/// This best-effort query does not check a specific [`TaskId`] and can miss state after event loss.
/// Use [`list`](Self::list) for registry membership.
/// > Use [`list`](Self::list) for registry membership.
pub async fn is_alive(&self, name: &str) -> bool {
self.core().is_alive(name).await
}
Expand Down Expand Up @@ -471,8 +470,8 @@ impl SupervisorHandle {
/// `Ok(id)` confirms only that the controller queue accepted the submission.
/// Slot admission and runtime registration happen later.
///
/// Use [`try_submit`](Self::try_submit) to fail fast when the controller queue is full.
/// Use [`submit_and_watch`](Self::submit_and_watch) to observe the final outcome, including admission rejection.
/// > Use [`try_submit`](Self::try_submit) to fail fast when the controller queue is full.
/// > Use [`submit_and_watch`](Self::submit_and_watch) to observe the final outcome, including admission rejection.
///
/// Requires the `controller` feature.
///
Expand Down
24 changes: 14 additions & 10 deletions src/core/outcome.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! # Reliable final task results
//!
//! Lifecycle events show live progress, but they are best-effort.
//! Use a [`TaskWaiter`] when application logic needs the final [`TaskOutcome`].
//! Lifecycle events answer "what is happening now?".
//! They may be dropped. Application logic must not reconstruct a final result from them.
//!
//! A waiter uses a direct one-shot channel.
//! Event-bus lag does not affect it.
//! A [`TaskWaiter`] answers "how did this task end?" for one [`TaskId`].
//! It receives one final [`TaskOutcome`] through a direct one-shot channel, outside the event bus.
//! For an admitted task, the registry sends the outcome after joining the actor and removing its membership.
//! Event-bus lag does not affect this path. If the outcome cannot be delivered, [`TaskWaiter::wait`] returns an error instead of guessing the result.
//!
//! ## Successful Direct-Add Flow
//!
Expand All @@ -30,8 +32,7 @@
//! - One waiter follows one [`TaskId`].
//! - For admitted work, it resolves after all retries end and the registry joins the task actor.
//! - Dropping a waiter is safe and does not cancel the task.
//! - If the runtime drops the sender before it creates an outcome,
//! [`TaskWaiter::wait`] returns an error instead of inventing a result.
//! - If the runtime drops the sender before it creates an outcome, [`TaskWaiter::wait`] returns an error instead of inventing a result.
//!
//! Direct [`SupervisorHandle::add_and_watch`](crate::SupervisorHandle::add_and_watch)
//! returns registration errors such as a duplicate name before it gives the
Expand Down Expand Up @@ -121,8 +122,8 @@ pub enum TaskOutcome {

/// The runtime aborted the actor before cooperative stop completed.
///
/// This normally happens after the configured grace period. Last-owner
/// fallback and signal-setup failure cleanup cannot wait for that period.
/// This normally happens after the configured grace period.
/// Last-owner fallback and signal-setup failure cleanup cannot wait for that period.
ForceAborted,

/// The internal actor panicked.
Expand Down Expand Up @@ -214,7 +215,8 @@ impl TaskOutcome {
/// Returns the original error source for [`Failed`](Self::Failed) or [`Fatal`](Self::Fatal).
///
/// Returns `None` when the outcome has no source error.
/// Callers can use `downcast_ref` or pass it to an error-reporting library.
///
/// > Callers can use `downcast_ref` or pass it to an error-reporting library.
#[must_use]
pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Expand Down Expand Up @@ -305,7 +307,9 @@ impl TaskWaiter {
///
/// # Errors
///
/// Returns [`RuntimeError::ShuttingDown`] if the runtime drops its sender before producing an outcome. No final result is available in that case.
/// Returns [`RuntimeError::ShuttingDown`] if the runtime drops its sender before producing an outcome.
///
/// > No final result is available in that case.
pub async fn wait(self) -> Result<TaskOutcome, RuntimeError> {
self.rx.await.map_err(|_| RuntimeError::ShuttingDown)
}
Expand Down
31 changes: 30 additions & 1 deletion src/core/owner.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
//! Tracks the lifetime of public runtime owners.
//! # Public runtime ownership
//!
//! [`RuntimeOwner`] is the shared lease held by [`Supervisor`](crate::Supervisor) and every [`SupervisorHandle`](crate::SupervisorHandle).
//!
//! ```text
//! Supervisor ────────────┐
//! SupervisorHandle ──────┼── Arc<RuntimeOwner> ── Arc<SupervisorCore>
//! cloned handles β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//!
//! internal workers ────────────────────────────── Arc<SupervisorCore>
//! ```
//!
//! Internal workers retain the runtime core directly, but they do not retain `RuntimeOwner`.
//! The last public owner can therefore disappear while cleanup work still holds the core alive.
//!
//! Dropping that last owner cannot wait for graceful cleanup.
//! It invokes [`SupervisorCore::abandon`] to close runtime intake and propagate cancellation.
//! If an explicit shared shutdown operation already exists, that operation keeps ownership of cleanup and the fallback does not replace it.

use std::sync::Arc;

Expand All @@ -9,25 +26,37 @@ use super::SupervisorCore;
/// Internal workers do not hold this value.
/// Its destructor therefore runs when the last public owner disappears and starts best-effort runtime cancellation.
pub(crate) struct RuntimeOwner {
/// Runtime state shared with public owners and internal workers.
///
/// Keeping the core behind a separate `Arc` lets detached shutdown and cleanup work outlive the public ownership lease when necessary.
core: Arc<SupervisorCore>,
}

impl RuntimeOwner {
/// Creates the first public ownership lease for `core`.
///
/// The returned `Arc` is cloned into each [`SupervisorHandle`](crate::SupervisorHandle).
pub(crate) fn new(core: Arc<SupervisorCore>) -> Arc<Self> {
Arc::new(Self { core })
}

/// Borrows the shared runtime core without creating another public owner.
pub(crate) fn core(&self) -> &Arc<SupervisorCore> {
&self.core
}
}

/// Omits runtime internals from the ownership lease's debug representation.
impl std::fmt::Debug for RuntimeOwner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuntimeOwner").finish_non_exhaustive()
}
}

/// Starts the non-blocking last-owner fallback.
///
/// `Drop` cannot await joins or return a shutdown result.
/// Confirmed cleanup must go through [`SupervisorHandle::shutdown`](crate::SupervisorHandle::shutdown).
impl Drop for RuntimeOwner {
fn drop(&mut self) {
self.core.abandon();
Expand Down
35 changes: 33 additions & 2 deletions src/core/registry/completion.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
//! Lossless completion signals shared by registry clients and cleanup owners.
//! # Registry completion signals
//!
//! Natural completion, an explicit remove or cancel, and shutdown can all start
//! terminal removal. One path claims the actor and becomes its join owner.
//! [`RemovalCompletion`] connects callers waiting for cleanup with that owner.
//!
//! ```text
//! Registered ──► Removing ──► join or force-abort ──► remove registry membership
//! β”‚
//! β–Ό
//! RemovalCompletion complete
//! ```

use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
Expand All @@ -8,27 +19,47 @@ use crate::core::outcome::TaskOutcome;
/// Sender used to resolve a watched task with its final [`TaskOutcome`].
pub(crate) type OutcomeTx = oneshot::Sender<TaskOutcome>;

/// Shared terminal signal for callers waiting until registry cleanup is committed.
/// Shared one-shot signal for committed terminal registry cleanup.
///
/// Every clone observes the same completion. The signal becomes complete only
/// after the actor has been joined or force-aborted and its identity and label
/// have been removed from the registry. Watched-outcome delivery and final event
/// publication are attempted before waiters are released.
///
/// This is not the actor's cancellation token. Creating or waiting on this value
/// does not request task cancellation.
#[derive(Clone, Debug)]
pub(crate) struct RemovalCompletion {
/// One-shot latch shared by cleanup owners and waiters.
///
/// The token's cancelled state represents completed registry cleanup.
token: CancellationToken,
}

impl RemovalCompletion {
/// Creates a new incomplete terminal-cleanup signal.
pub(crate) fn new() -> Self {
Self {
token: CancellationToken::new(),
}
}

/// Waits until terminal registry cleanup has been committed.
///
/// If cleanup is already complete, this returns immediately. Dropping this
/// wait does not affect other waiters or the cleanup owner.
pub(crate) async fn wait(&self) {
self.token.cancelled().await;
}

/// Returns `true` when terminal registry cleanup has been committed.
pub(super) fn is_complete(&self) -> bool {
self.token.is_cancelled()
}

/// Marks terminal registry cleanup complete and releases all waiters.
///
/// Repeated calls leave the signal complete.
pub(super) fn complete(&self) {
self.token.cancel();
}
Expand Down
4 changes: 2 additions & 2 deletions src/core/registry/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ impl Registry {
match handle.await {
Ok(()) => true,
Err(error) => {
self.bus.publish(Event::subscriber_panicked(
self.bus.publish(Event::runtime_failure(
"registry",
format!("listener join failed: {error}"),
));
Expand Down Expand Up @@ -236,7 +236,7 @@ impl Registry {
/// A panic while processing one command or completion is reported as a diagnostic event instead of killing the registry listener.
async fn guarded(&self, who: &'static str, fut: impl Future<Output = ()>) {
if let Err(msg) = crate::core::panic_guard::guarded(fut).await {
self.bus.publish(Event::subscriber_panicked(
self.bus.publish(Event::runtime_failure(
who,
format!("listener panic: {msg}"),
));
Expand Down
7 changes: 3 additions & 4 deletions src/core/registry/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub(crate) struct AddBatchItem {
/// Authoritative result of one registry remove command.
///
/// `Ok(true)` means the registry claimed the task and sent cancellation.
/// It does not mean the actor has terminated yet.
/// > It does not mean the actor has terminated yet.
pub(crate) type RemoveReply = Result<bool, RuntimeError>;

/// Receiver for an authoritative registry remove result.
Expand All @@ -32,7 +32,7 @@ pub(crate) type RemoveReplyRx = oneshot::Receiver<RemoveReply>;
/// Registry decision returned to one cancellation caller.
///
/// `claimed` is true only for the caller that changed `Registered` to `Removing`.
/// Every caller that observes the same removal waits on the same terminal completion.
/// > Every caller that observes the same removal waits on the same terminal completion.
pub(crate) struct CancelDecision {
pub(crate) id: TaskId,
pub(crate) claimed: bool,
Expand All @@ -53,8 +53,7 @@ impl CancelDecision {

/// Authoritative result of one registry cancel command.
///
/// `Ok(None)` means no registry entry exists at this command's ordering point:
/// the identity is unknown or terminal cleanup has already removed it.
/// `Ok(None)` means no registry entry exists at this command's ordering point: the identity is unknown or terminal cleanup has already removed it.
pub(crate) type CancelReply = Result<Option<CancelDecision>, RuntimeError>;

/// Receiver for an authoritative registry cancel decision.
Expand Down
3 changes: 2 additions & 1 deletion src/core/registry/removal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::{
core::{actor::ActorExitReason, outcome::TaskOutcome},
events::{Bus, Event, EventKind},
identity::TaskId,
reasons,
};

/// Terminal result passed from the single join owner to registry cleanup.
Expand Down Expand Up @@ -424,7 +425,7 @@ impl Registry {
Event::new(EventKind::TaskRemoved)
.with_task(Arc::clone(&entry.label))
.with_id(id)
.with_reason("force_terminated_after_grace"),
.with_reason(reasons::FORCE_TERMINATED_AFTER_GRACE),
);
}
}
Expand Down
Loading
Loading