Taskvisor 0.8: task ownership from submission to cleanup

Hi all,

In June, I introduced Taskvisor 0.2 as a Tokio task supervisor with restart policies, backoff, lifecycle events, and dynamic task control.

After that release, I kept moving the project forward one practical problem at a time. I wanted final outcomes to be reliable, controller admission to be predictable, shutdown to be bounded, and runtime limits to describe the real load.

Each change made sense on its own, and this worked for a while. By 0.7, however, I felt stuck. Fixing one edge of the task lifecycle often meant adding another rule somewhere else, especially in three places:

Observed state.
I wanted lifecycle events to remain best-effort observability, yet activity queries were built from those events. The public view could lag behind the runtime state it tried to describe.

One completion boundary.
I was using one completion point for several meanings: the final outcome, registry removal, physical actor exit, and controller slot reuse. When grace period expired, 0.7 requested abort and still waited for the actor handle before producing the outcome.

Waiting protected against early reuse, but a synchronous poll or blocking destructor could keep shutdown running past its grace period. Releasing everything at the deadline was not safe either: new work could reuse the same name or slot while the old task was still active.

Capacity beyond queues.
Taskvisor could limit concurrent attempts and internal queues, but it had no supervisor-wide bound for all accepted task and subscriber values retained through the rest of their lifecycle. The existing limits did not describe all user values the runtime still owned.

I kept calling all these moments "finished". Finished for the caller, the registry, the actor, and the destructor were not always the same moment.

Force-abort brought the ownership problem into focus. In 0.7, Taskvisor did not report ForceAborted until the actor handle returned. A synchronous poll or blocking destructor could therefore keep the task name and any controller slot reserved and push shutdown past its grace period.

Reporting the logical outcome at the deadline was only half a fix: releasing the name and slot at the same moment would let old and new work overlap. I needed to separate the logical deadline from the later physical exit without losing ownership of either side.

Around this point, I came across Yosh Wuyts’s Tree-Structured Concurrency. Its view of concurrent work through ownership and lifetime gave me a different way to look at the problem.

I searched further and found Asynchronous clean-up. I then returned to the Tokio task documentation, which makes a small but important distinction: abort() requests cancellation, while physical completion is observed later through the task handle.
That was the distinction I had been missing.

I took this idea back to Taskvisor and mapped every stage that could still own a task: submission, admission, attempts, terminal reporting, physical exit, and final value cleanup.

At every transition, someone still owns the task. Releasing that ownership too early can allow overlap. Holding it without a limit can hide resource pressure or delay shutdown.

This became the main question behind Taskvisor 0.8:

Who owns the task now, and when is it safe to release that ownership?

The main change from 0.7 to 0.8 looks like this:

# 0.7
events ─────────► best-effort activity view
grace expires ──► abort request ──► wait for actor exit
                                              ▼
                                  outcome + resource release

# 0.8
registry + physical-exit tracker ──► authoritative activity
grace expires
     ├── logical ──► ForceAborted + registry removal
     │                           └──► TaskWaiter (if watched)
     └── physical ──► actor exit + terminal match
                                 ▼
                        release task name
                                 ▼
                  hand values to cleanup
                    ├──► physical completion ──► release slot
                    └──► clean destruction ────► return capacity

The ownership boundaries are now explicit. The controller owns a pending payload until registry hand-off. The registry owns admitted membership, identity, and final classification. The task actor owns one task lifecycle and runs one non-overlapping attempt at a time.

After a logical force-abort, a physical-exit tracker keeps the task name and activity reserved until the actor exist. Cleanup workers then retain task and subscriber values until their destruction finishes.

The ownership model also clarified the public contract. TaskWaiter is the direct path to a final TaskOutcome. In 0.8, activity queries read the registry and physical-exit tracker instead of reconstructing state from lifecycle events. Events remain best-effort observability for logs, metrics, tracing, and diagnostics.

A separate ownership-capacity limit now follows accepted task and subscriber lifetimes through cleanup. The registration name now lives in TaskSpec, it belongs to one registration, not to reusable task behavior.

Taskvisor remains an in-process supervisor, not a durable job queue. Aborting a Tokio task cannot interrupt arbitrary synchronous code while it is running, and a grace deadline cannot prove that such code has exited.

The difference in 0.8 is that Taskvisor no longer treats every meaning of "finished" as one event. On force-abort, it can report a logical outcome while continuing to own and track the remaining physical work.


Taskvisor 0.8 is available on

Thanks!