Skip to main content

Module controller

Module controller 

Source
Available on crate feature controller only.
Expand description

§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.

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.

PolicyBusy slot behavior
AdmissionPolicy::Queueenqueue if the per-slot limit allows it; otherwise reject
AdmissionPolicy::Replacecreate or replace the queue head; retire owner if needed
AdmissionPolicy::DropIfRunningreject 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.

§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 stateCauseNext state
Idlea submission starts admissionAdmitting
Admittingregistry accepts registrationRunning
Admittingregistry rejects registrationNext / Idle
AdmittingReplace arrivesTerminating
RunningReplace arrivesTerminating
Runningterminal registry cleanup completesNext / Idle
Terminatingpending registration is acceptedstay Terminating; request owner removal
Terminatingpending registration is rejected or terminal cleanup completesNext / Idle
Terminatinganother Replace arrivesstay 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

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

use taskvisor::{
    ControllerConfig, ControllerSpec, Supervisor, SupervisorConfig, TaskFn, TaskSpec,
};

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?;

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. After registration, the waiter resolves to the runtime task’s terminal outcome.

§Events

Controller-specific event kinds are:

Removing a submission that is still in a slot queue also publishes the standard EventKind::TaskRemoveRequested. A registry rejection, such as a duplicate task name, publishes the standard 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.

Structs§

ControllerConfig
Queue limits for the controller.
ControllerSnapshot
Best-effort rolling snapshot of controller slots.
ControllerSpec
Describes one task submission through the controller.
PreparedSubmission
A single-use controller submission with a preallocated TaskId.
SlotView
Point-in-time view of one controller slot.

Enums§

AdmissionPolicy
Policy for handling a submission when its target slot is busy.
ControllerError
Errors returned by controller configuration, startup, and submission operations.
SlotStatusKind
Public status of one controller slot.