diff --git a/.github/crates.txt b/.github/crates.txt new file mode 100644 index 0000000..606d813 --- /dev/null +++ b/.github/crates.txt @@ -0,0 +1 @@ +taskvisor diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml index b814aa3..0d2aa2b 100644 --- a/.github/workflows/manual.yml +++ b/.github/workflows/manual.yml @@ -15,3 +15,7 @@ jobs: ci: name: ci uses: soltiHQ/actions/.github/workflows/rust-ci.yml@v1 + + docs: + name: docs + uses: soltiHQ/actions/.github/workflows/docs-ci.yml@v1 diff --git a/.github/workflows/pr-action.yml b/.github/workflows/pr-action.yml index 4e6a221..c303b86 100644 --- a/.github/workflows/pr-action.yml +++ b/.github/workflows/pr-action.yml @@ -17,3 +17,7 @@ jobs: ci: name: ci uses: soltiHQ/actions/.github/workflows/rust-ci.yml@v1 + + docs: + name: docs + uses: soltiHQ/actions/.github/workflows/docs-ci.yml@v1 diff --git a/.github/workflows/tag-publish.yml b/.github/workflows/tag-publish.yml index c4053c9..2f3fd8a 100644 --- a/.github/workflows/tag-publish.yml +++ b/.github/workflows/tag-publish.yml @@ -19,6 +19,20 @@ jobs: name: release uses: soltiHQ/actions/.github/workflows/rust-release.yml@v1 with: - crate: taskvisor + crates-file: .github/crates.txt secrets: crates-io-token: ${{ secrets.CRATES_IO_TOKEN }} + + docs: + name: docs + needs: release + uses: soltiHQ/actions/.github/workflows/docs-notify.yml@v1 + with: + site-repository: soltiHQ/site + site-workflow: docs-release.yml + site-ref: main + source-repository: ${{ github.repository }} + source-ref: ${{ github.ref_name }} + client-id: ${{ vars.DOCS_SITE_APP_CLIENT_ID }} + secrets: + private-key: ${{ secrets.DOCS_SITE_APP_PRIVATE_KEY }} diff --git a/Cargo.lock b/Cargo.lock index 71082e0..6bda2c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,7 +707,7 @@ dependencies = [ [[package]] name = "taskvisor" -version = "0.8.0" +version = "0.8.1" dependencies = [ "anstream", "anstyle", diff --git a/Cargo.toml b/Cargo.toml index d922b98..1beadb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taskvisor" -version = "0.8.0" +version = "0.8.1" edition = "2024" rust-version = "1.90.0" diff --git a/README.md b/README.md index 7c9c8f7..f0549a9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ It turns ordinary async work into a managed lifecycle with backoff, timeouts, ca When work competes for the same application key, the optional controller queues it, replaces older work, or rejects it. Conflict policy is evaluated per key; supervisor-wide limits still apply. -[Quick start](#quick-start) · [User guide](guide.md) · [API docs](https://docs.rs/taskvisor) · [Examples](examples/README.md) · [Benchmarks](benches/README.md) +[Quick start](#quick-start) · [User guide](docs/index.md) · [API docs](https://docs.rs/taskvisor) · [Examples](examples/README.md) · [Benchmarks](benches/README.md) ## The retry loop you stop maintaining @@ -146,7 +146,7 @@ The `controller` feature is enabled by default. A supervisor uses controller admission only when it is built with `SupervisorBuilder::with_controller`. See [tenant_sync.rs](examples/tenant_sync.rs) for a complete latest-wins workflow across separate tenant slots. -The [user guide](guide.md#coordinate-work-by-key) explains queue ordering, replacement, rejection, slot identity, and controller limits. +The [user guide](docs/keyed-admission.md) explains queue ordering, replacement, rejection, slot identity, and controller limits. ## When Taskvisor fits @@ -183,7 +183,7 @@ Taskvisor makes its process boundary explicit: - periodic tasks use a delay after completion, not a calendar or cron schedule; - controller slots coordinate work inside one supervisor. -Read the [full production boundaries](guide.md#production-boundaries) before deploying a service. +Read the [full production boundaries](docs/production-boundaries.md) before deploying a service. ## Examples and documentation @@ -199,8 +199,8 @@ The repository contains 18 complete runnable programs. The [examples guide](examples/README.md) provides the complete learning path, run commands, feature flags, and stop behavior. -Use the [user guide](guide.md) for application workflows and production boundaries, then open the [API documentation](https://docs.rs/taskvisor) for exact contracts. -Optional `tracing`, `logging`, `tokio-util-interop`, and `test-util` integrations are covered in the [installation guide](guide.md#install-taskvisor). +Use the [user guide](docs/index.md) for application workflows and production boundaries, then open the [API documentation](https://docs.rs/taskvisor) for exact contracts. +The [installation guide](docs/installation.md) lists the optional `tracing`, `logging`, `tokio-util-interop`, and `test-util` features. Use the [API documentation](https://docs.rs/taskvisor) for each integration's exact public contract. ## Benchmarks diff --git a/Taskfile.yml b/Taskfile.yml index e18de0a..4981fe6 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,9 +1,15 @@ version: '3' includes: + docs: + taskfile: https://raw.githubusercontent.com/soltiHQ/actions/v1/taskfiles/docs/Taskfile.yml rust: taskfile: https://raw.githubusercontent.com/soltiHQ/actions/v1/taskfiles/rust/Taskfile.yml +vars: + docs_version: + sh: sed -n 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)"/\1/p' Cargo.toml | head -n 1 + tasks: ci/fmt: desc: Run 'cargo fmt --check'. @@ -38,12 +44,10 @@ tasks: vars: { TEST_ARGS: '--all-features --locked' } ci/test-unit: - desc: Run unit tests ('cargo test --lib') and doctests ('cargo test --doc'). + desc: Run unit tests ('cargo test --lib'). cmds: - task: rust:test vars: { TEST_ARGS: '--lib --all-features --locked' } - - task: rust:test - vars: { TEST_ARGS: '--doc --all-features --locked' } ci/test-integration: desc: Run integration tests ('cargo test --test "*"'). @@ -57,7 +61,7 @@ tasks: - task: rust:audit ci/docs: - desc: Run 'rustdoc' for the taskvisor crate. Fails on broken doc links in any feature config. + desc: Validate API documentation and the versioned user guide. cmds: - task: rust:doc vars: { DOC_ARGS: '--locked --no-deps' } @@ -65,6 +69,23 @@ tasks: vars: { DOC_ARGS: '--locked --no-deps --all-features' } - task: rust:docs vars: { DOCS_ARGS: '--all-features --locked' } + - task: docs:validate + vars: + VERSION: '{{.docs_version}}' + PRODUCT: taskvisor + TITLE: Taskvisor + REPOSITORY: https://github.com/soltiHQ/taskvisor + VERSION_PROVIDER: cargo + VERSION_PACKAGE: taskvisor + REFERENCE_LABEL: API reference + REFERENCE_URL: https://docs.rs/taskvisor/{version}/taskvisor/ + CARGO_SNIPPET_SOURCES: README.md docs/installation.md + LINK_SOURCES: docs README.md guide.md src/lib.rs + - task: docs:links/external + vars: + LINK_SOURCES: docs README.md guide.md src/lib.rs + - task: rust:test + vars: { TEST_ARGS: '--doc --all-features --locked' } ci/build: desc: Build all examples for a package. Pass CRATE. diff --git a/docs/cancellation-and-shutdown.md b/docs/cancellation-and-shutdown.md new file mode 100644 index 0000000..4715179 --- /dev/null +++ b/docs/cancellation-and-shutdown.md @@ -0,0 +1,60 @@ +--- +title: Cancellation and shutdown +description: Make task operations cancellation-aware and join Taskvisor's bounded shutdown workflow. +--- + +# Cancellation and shutdown + +Cancellation starts cooperatively. A resident task must observe `TaskContext`: + +```rust +use taskvisor::{TaskContext, TaskError}; + +async fn do_work() -> Result<(), TaskError> { + // Application work goes here. + Ok(()) +} + +async fn run_one_operation(ctx: &TaskContext) -> Result<(), TaskError> { + ctx.run_until_cancelled(do_work()).await? +} + +async fn run_with_more_branches(ctx: &TaskContext) -> Result<(), TaskError> { + tokio::select! { + _ = ctx.cancelled() => Err(TaskError::Canceled), + result = do_work() => result, + } +} +``` + +`run_until_cancelled` drops the wrapped future when cancellation wins. +Cancellation wins a tie, and an already-cancelled context does not poll the wrapped future. +Use it only when dropping that future is a safe way to cancel the exact operation. +Check the operation's cancellation-safety contract; an external commit, acknowledgement, or partially consumed input may need an explicit protocol. +The Tokio sleep in [graceful_worker.rs](../examples/graceful_worker.rs) is a simple drop-safe example. + +An attempt timeout also drops the attempt future. +It does not undo side effects that already happened. +A blocking future destructor can delay attempt release beyond the configured timeout. + +`cancel_with_timeout` and `cancel_by_name_with_timeout` limit how long the caller waits for registered task cleanup. +Controller ordering, command-queue admission, and the registry claim happen outside that timer. +A timeout stops this caller's wait; it does not undo cancellation or change the supervisor grace period. +If task completion is observed at the timeout boundary, completion wins. +Queued controller work is removed directly, and `cancel_with_timeout` does not apply its wait timer to that path. +A watched queued submission then resolves to `Rejected` with `RejectionKind::RemovedFromQueue`, not to `Canceled`. +The matching `try_*` methods make command-queue admission fail fast; their remaining behavior is unchanged. + +The joined shutdown workflow has concurrent parts: + +- It closes admission and signals runtime and controller shutdown. +- The registry requests cancellation for registered tasks, waits through the configured grace period, and commits `ForceAborted` for tasks that did not stop in time. +- The controller rejects pending submissions as its loop exits; this can overlap the registry grace period. +- Taskvisor joins the remaining runtime and controller cleanup, then drains subscriber queues up to their separate deadline. + +Taskvisor cannot interrupt synchronous code in the middle of a poll. +After the grace period, the final outcome may be `ForceAborted` while that synchronous code is still physically running. +The supervisor keeps ownership until it returns. + +`handle.shutdown().await` joins the shared shutdown workflow and returns its result. +Dropping the final public owner can request cancellation, but a destructor cannot await cleanup or report its errors. diff --git a/docs/common-mistakes.md b/docs/common-mistakes.md new file mode 100644 index 0000000..57e3007 --- /dev/null +++ b/docs/common-mistakes.md @@ -0,0 +1,24 @@ +--- +title: Common mistakes +description: Avoid incorrect assumptions about task results, admission, cancellation, blocking work, and side effects. +--- + +# Common mistakes + +- Treating `run().await == Ok(())` as proof that every task succeeded. +- Treating `submit().await?` as positive slot admission. +- Using best-effort events for application decisions. +- Forgetting to observe cancellation in a resident task. +- Treating a controller slot as a registered task name. +- Running blocking or CPU-heavy work on Tokio worker threads. +- Assuming a timeout or force-abort can undo external side effects. + +## Continue learning + +| Resource | Next step | +|---------------------------------------------------|----------------------------------------------------| +| [Examples guide](../examples/README.md) | Choose a complete runnable scenario. | +| [API documentation](https://docs.rs/taskvisor) | Read exact contracts for public types and methods. | +| [Benchmark guide](../benches/README.md) | Run and interpret the Criterion suites. | +| [Contributor map](../src/ARCHITECTURE.md) | Follow runtime ownership and source boundaries. | + diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..146b56f --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,72 @@ +--- +title: Configure Taskvisor +description: Configure runtime limits, inherited task behavior, per-task overrides, subscriber queues, and keyed admission limits. +--- + +# Configure Taskvisor + +Configuration is split by concern: + +```text +SupervisorConfig ──► runtime-wide limits and shutdown +TaskDefaults ──────► inherited task behavior +TaskSpec ──────────► per-task overrides +ControllerConfig ──► keyed-admission limits +Subscribe ─────────► per-subscriber event queue capacity +``` + +```rust +use std::num::{NonZeroU32, NonZeroUsize}; +use std::sync::Arc; +use std::time::Duration; +use taskvisor::{Supervisor, SupervisorConfig, TaskDefaults}; + +fn configured_supervisor() -> Arc { + let runtime = SupervisorConfig::default() + .with_grace(Duration::from_secs(30)) + .with_subscriber_shutdown_timeout(Duration::from_secs(5)) + .with_max_concurrent(NonZeroUsize::new(16)) + .with_ownership_capacity(NonZeroUsize::new(4096)); + + let tasks = TaskDefaults::default() + .with_timeout(Duration::from_secs(20)) + .with_max_retries(NonZeroU32::new(5).unwrap()); + + Supervisor::builder(runtime) + .with_task_defaults(tasks) + .build() +} +``` + +Main defaults: + +| Setting | Default | +|---------------------------|----------------------------------------------------------------------------------------| +| Graceful task shutdown | 60 seconds. | +| Subscriber drain | 5 seconds, shared by all subscriber queues. | +| Concurrent task attempts | Unlimited. | +| Registered-task limit | 1024. | +| Ownership capacity | 1024 per supervisor across accepted tasks and subscribers. | +| Event bus capacity | 1024. | +| Subscriber queue capacity | 1024 per subscriber; override through `queue_capacity`. | +| Registry command capacity | 1024. | +| Restart policy | On retryable failure. | +| Failure backoff | 200 ms initial base, capped at 30 s, with equal jitter; the first delay is 100–200 ms. | +| Attempt timeout | None. | +| Failure retry limit | Unlimited. | + +Three limits answer different questions: + +| Limit | What it bounds | +|------------------------|-------------------------------------------------------------------------------------------------------| +| `max_concurrent` | Attempts physically running at the same time. | +| `max_registered_tasks` | Registered and removing tasks through terminal cleanup; force-aborted work can remain charged longer. | +| `ownership_capacity` | Accepted task and subscriber values still owned through physical cleanup. | + +`SupervisorConfig::with_ownership_capacity(None)` removes the ownership count bound. +Cleanup still uses a bounded worker set, but retained values and cleanup backlog can then grow without a count limit. + +During cleanup handoff, one task can temporarily consume two `max_registered_tasks` units. + +Capacity values are non-zero where zero would make the runtime unusable. +Checked `try_with_*` methods accept raw integers and return a configuration error for invalid values. diff --git a/docs/defining-tasks.md b/docs/defining-tasks.md new file mode 100644 index 0000000..2182c18 --- /dev/null +++ b/docs/defining-tasks.md @@ -0,0 +1,40 @@ +--- +title: Define a task +description: Define Taskvisor work with an async closure or a reusable task type. +--- + +# Define a task + +Use `TaskFn` for an async closure: + +```rust +use taskvisor::{TaskFn, TaskRef}; + +let task: TaskRef = TaskFn::arc(|_ctx| async { + println!("one attempt"); + Ok(()) +}); +``` + +Implement `Task` when a reusable type should hold state or dependencies across attempts. +Each call to `Task::spawn` must return a fresh future. +Keep synchronous work in `spawn` short; put the actual operation inside the returned future. + +A shared `TaskRef` can back several registrations. Registrations that overlap in one supervisor need different names. +A name can be reused after the earlier registration releases it. +The registrations receive different task IDs, and their `spawn` calls may run concurrently when configured attempt capacity permits. +Shared task state must support that use. + +After a force-abort, Taskvisor may keep the name reserved until it observes that the task attempt has physically returned. + +Keep blocking and CPU-heavy work away from Tokio worker threads. +Use a suitable blocking executor, worker pool, or external runtime. +Also keep the destructor of an attempt future short: Taskvisor drops that future synchronously when the attempt ends or is canceled. + +Runnable examples: + +- [basic.rs](../examples/basic.rs) uses `TaskFn` for one static task; +- [task_type.rs](../examples/task_type.rs) implements `Task` for reusable state; +- [queue_consumer.rs](../examples/queue_consumer.rs) supervises a cancellation-aware receive loop; +- [cpu_job.rs](../examples/cpu_job.rs) moves CPU work to Rayon and explains the cancellation limit. + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..d686b6b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,24 @@ +--- +title: Taskvisor user guide +description: Choose the Taskvisor workflow that fits an application and follow its production boundaries. +--- + +# Taskvisor user guide + +This guide explains how to use Taskvisor in an application and how to choose between its public workflows. +For exact method signatures, error variants, and edge-case contracts, use the [API documentation](https://docs.rs/taskvisor). + +Taskvisor is an in-process runtime. Tasks, queued submissions, task IDs, events, and watched outcomes do not survive process exit. +Use durable external storage when work must resume after a restart. + +- New to Taskvisor? Run the [Quick start](../README.md#quick-start). +- Looking for a complete program? Follow the [examples guide](../examples/README.md). +- Changing Taskvisor itself? Start with the [contributor map](../src/ARCHITECTURE.md). + +## In this guide + +- Start: [mental model](mental-model.md), [installation](installation.md), [task definition](defining-tasks.md), and [task behavior](lifecycle-policies.md). +- Run: [supervisor entry points and runtime management](running-and-managing.md), then [cancellation and shutdown](cancellation-and-shutdown.md). +- Extend: [outcomes and events](outcomes-and-events.md), [per-key coordination](keyed-admission.md), and [configuration](configuration.md). +- Deploy: [production boundaries](production-boundaries.md) and [common mistakes](common-mistakes.md). + diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..311eefd --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,41 @@ +--- +title: Install Taskvisor +description: Install Taskvisor and select the controller, observability, interop, or test features needed by an application. +--- + +# Install Taskvisor + +The default install includes the controller API: + +```toml +taskvisor = "0.8" +``` + +The controller has no runtime effect until a supervisor is built with `with_controller`. + +| Feature | Default | Adds | +|----------------------|---------|--------------------------------------------------------| +| `controller` | Yes. | Slot-based admission control. | +| `tracing` | No. | `TracingBridge` for the `tracing` ecosystem. | +| `logging` | No. | `LogWriter` for simple readable lifecycle output. | +| `tokio-util-interop` | No. | Access to the raw cancellation token in `TaskContext`. | +| `test-util` | No. | Constructors intended for external integration tests. | + +Enable an optional integration: + +```toml +taskvisor = { version = "0.8", features = ["tracing"] } +``` + +Build without keyed admission: + +```toml +taskvisor = { version = "0.8", default-features = false } +``` + +## Test helpers + +With `test-util` enabled, `TaskContext::detached` and `TaskContext::detached_cancelled` create contexts for direct task-code tests. +`TaskId::for_tests` creates a fresh process-local ID. +`TaskOutcome::failed_for_tests`, `TaskOutcome::fatal_for_tests`, and `TaskOutcome::rejected_for_tests` construct non-exhaustive outcome variants for assertions. +Use the [API documentation](https://docs.rs/taskvisor) for their exact contracts. diff --git a/docs/keyed-admission.md b/docs/keyed-admission.md new file mode 100644 index 0000000..b6b302e --- /dev/null +++ b/docs/keyed-admission.md @@ -0,0 +1,83 @@ +--- +title: Coordinate work by key +description: Queue, replace, or reject competing work through Taskvisor controller slots. +--- + +# Coordinate work by key + +This section requires the `controller` feature. +It is enabled by default, but each supervisor must install a controller explicitly: + +```rust +use taskvisor::{ControllerConfig, Supervisor, SupervisorConfig}; + +let _supervisor = Supervisor::builder(SupervisorConfig::default()) + .with_controller(ControllerConfig::default()) + .build(); +``` + +Direct `add*` methods bypass controller admission. +`submit*` methods accept a `ControllerSpec`, apply its slot policy, and hand admitted work to the runtime registry. + +| Identity | Scope | +|-----------------|----------------------------------------------------------| +| `TaskId` | One process-local registration or controller submission. | +| Task name | Registry key inside one supervisor. | +| Controller slot | Admission key inside one supervisor controller. | + +Different task names can share a slot. +Without an explicit `with_slot`, the task name is also the slot. +A queued submission owns its task ID but does not own a registered task name yet. + +A controller slot can remain occupied while admission, task execution, or physical release is pending. +An occupied slot does not always mean that a task body is currently polling. + +| Policy | Busy-slot behavior | +|-----------------|----------------------------------------------------------------------------------------| +| `Queue` | Append to the bounded FIFO queue. A later `Replace` can still displace the queue head. | +| `Replace` | Request owner retirement and create or replace the queue head. | +| `DropIfRunning` | Reject the incoming submission without changing the owner or queue. | + +A replacement is not guaranteed to become the next owner. +A newer `Replace` can supersede it before admission, and later registry admission can still reject it. +`Replace` changes only the queue head and preserves the FIFO tail. +It does not use the per-slot `max_slot_queue` limit, but creating a new head can still reach `max_total_pending`. + +```rust +use taskvisor::{ControllerSpec, TaskFn, TaskRef, TaskSpec}; + +let task: TaskRef = TaskFn::arc(|_ctx| async { Ok(()) }); +let request = ControllerSpec::queue(TaskSpec::once("customer-42-job", task)) + .with_slot("customer-42"); + +assert_eq!(request.task_spec().name(), "customer-42-job"); +assert_eq!(request.slot_name(), "customer-42"); +``` + +`submit().await?` confirms command intake only. `submit_and_watch` returns a task ID and waiter. +The waiter resolves to `Rejected` if admission fails or to the registered task's final outcome if admission succeeds. + +`prepare_submission` allocates a task ID before intake. +It does not reserve a name, slot, queue position, or runtime capacity. + +`controller_snapshot` is a rolling diagnostic view. +It reads slots independently and can already be stale when returned. +Do not treat it as a transaction boundary. + +Attempt timeout starts only after registry admission and after `Task::spawn` returns the attempt future. +It does not limit time spent in a controller queue. Controller submission has no built-in end-to-end deadline. + +Slots govern admission, not cancellation. +There is no slot-wide cancel or remove operation. +Stop queued work by task ID and registered work by task ID or task name. +Removing or canceling controller work that is still queued or waiting for registry-command capacity removes it directly before it runs. +Its watcher resolves to `Rejected` with `RejectionKind::RemovedFromQueue`, not to `Canceled`. + +`ControllerConfig` bounds command intake, per-slot queues, total pending work, tracked slots, registry-capacity waits, and concurrent identity operations. +See its [API documentation](https://docs.rs/taskvisor/latest/taskvisor/controller/struct.ControllerConfig.html) for the exact defaults and rejection mapping. + +Runnable controller examples: + +- [controller_slots.rs](../examples/controller_slots.rs) compares all three policies; +- [controller_admission.rs](../examples/controller_admission.rs) watches admission and rejection; +- [tenant_sync.rs](../examples/tenant_sync.rs) keeps the newest waiting revision per tenant. diff --git a/docs/lifecycle-policies.md b/docs/lifecycle-policies.md new file mode 100644 index 0000000..1d49ecf --- /dev/null +++ b/docs/lifecycle-policies.md @@ -0,0 +1,67 @@ +--- +title: Choose task behavior +description: Configure success repetition, retryable failures, backoff, attempt timeouts, and retry limits. +--- + +# Choose task behavior + +`TaskSpec` selects what follows success or a retryable failure: + +| Constructor | After success | After a retryable failure | +|---------------------------|---------------------------------------|----------------------------------------------------------| +| `TaskSpec::once` | Stop. | Stop. | +| `TaskSpec::restartable` | Stop. | Retry if the policy and retry limit allow. | +| `TaskSpec::periodic` | Repeat; wait for a non-zero interval. | Retry through failure backoff if the retry limit allows. | +| `TaskSpec::from_defaults` | Use `TaskDefaults`. | Use `TaskDefaults`. | + +One task ID runs attempts sequentially. Two attempts for that ID never overlap. + +| Attempt result | Meaning | +|-----------------------|--------------------------------------------------------------------------| +| `Ok(())` | Success. The restart policy decides whether another attempt follows. | +| `TaskError::Fail` | Retryable failure. | +| `TaskError::Timeout` | Retryable timeout reported by task code. | +| `TaskError::Fatal` | Permanent failure; stop without retry. | +| `TaskError::Canceled` | Cooperative cancellation; stop without retry. | +| Configured timeout | Drop the attempt future; report a retryable timeout if cleanup succeeds. | + +A returned `TaskError::Timeout` follows the ordinary attempt-failure event path. +A configured attempt deadline drops the attempt future. +If cleanup succeeds, it produces the distinct `AttemptTimedOut` lifecycle event and a retryable timeout. +These two timeout failures remain subject to the restart policy and retry limit. +If dropping the attempt future panics, Taskvisor instead produces `AttemptFailed` and ends with a final `Panicked` outcome without retrying. + +With panic unwinding enabled, a panic while creating or polling the attempt future becomes a retryable failure. +A panic during protected cleanup can instead produce a final `Panicked` outcome. +`panic = "abort"` cannot be caught. + +A retry limit counts retries after the first failed attempt. +A limit of three therefore allows at most four consecutive failed attempts. +A successful attempt resets the failure streak. + +```rust +use std::num::NonZeroU32; +use std::time::Duration; +use taskvisor::{BackoffPolicy, JitterPolicy, TaskRef, TaskSpec}; + +fn supervised(name: &str, task: TaskRef) -> TaskSpec { + TaskSpec::restartable(name, task) + .with_backoff( + BackoffPolicy::exponential(Duration::from_millis(200)) + .with_max(Duration::from_secs(30)) + .with_jitter(JitterPolicy::Equal), + ) + .with_timeout(Duration::from_secs(10)) + .with_max_retries(NonZeroU32::new(3).unwrap()) +} +``` + +Equal jitter chooses a delay between half of the current base delay and the full base delay. +This spreads retries that would otherwise happen together. +Per-task settings override values inherited from `TaskDefaults`. + +A non-zero periodic interval starts after a successful attempt completes. +It is fixed-delay scheduling, not a wall-clock or cron schedule. +Passing `Duration::ZERO` removes the configured interval; Taskvisor still applies its internal fast-loop guard. + +See [periodic.rs](../examples/periodic.rs), [restart_policies.rs](../examples/restart_policies.rs), and [configuration.rs](../examples/configuration.rs). diff --git a/docs/mental-model.md b/docs/mental-model.md new file mode 100644 index 0000000..e7d0c77 --- /dev/null +++ b/docs/mental-model.md @@ -0,0 +1,38 @@ +--- +title: Mental model +description: Understand Taskvisor tasks, specifications, identities, supervision, and observation paths. +--- + +# Mental model + +A task defines executable work. A task specification gives that work a name and lifecycle policy. +The supervisor owns registration, attempts, cancellation, and cleanup. + +```text +Task / TaskFn ──► TaskSpec + ├── add* ──► registry + └── ControllerSpec + └── submit* ──► controller + ├── admitted ──► registry + └── rejected ──► watched TaskWaiter + +registry ──► supervised attempts + ├── watched ──► TaskWaiter + └── observed ─► Event subscribers +``` + +| Value | Role | +|--------------------|---------------------------------------------------------------------| +| `Task` or `TaskFn` | Creates a fresh future for each attempt. | +| `TaskSpec` | Gives work its registry name and execution policy. | +| `Supervisor` | Owns one Taskvisor runtime. | +| `SupervisorHandle` | Manages a running supervisor. | +| `TaskId` | Identifies one process-local registration or controller submission. | +| Task name | Uniquely identifies registry membership inside one supervisor. | +| Controller slot | Coordinates submissions that must not own the same key together. | +| `TaskWaiter` | Delivers one direct in-process final outcome. | +| `Event` | Describes lifecycle activity through best-effort delivery. | + +Direct `add*` methods send a `TaskSpec` to the runtime registry. +Controller `submit*` methods first apply a per-slot admission policy, then hand admitted work to the same registry. + diff --git a/docs/outcomes-and-events.md b/docs/outcomes-and-events.md new file mode 100644 index 0000000..1c87751 --- /dev/null +++ b/docs/outcomes-and-events.md @@ -0,0 +1,50 @@ +--- +title: Final outcomes and lifecycle events +description: Use reliable in-process final outcomes for decisions and best-effort events for observability. +--- + +# Final outcomes and lifecycle events + +Taskvisor has two result paths with different contracts: + +| Path | Contract | Use it for | +|--------------------------------|-------------------------------------------------|-----------------------------------------| +| `TaskWaiter` and `TaskOutcome` | One direct final result, outside the event bus. | Application decisions. | +| `Subscribe` and `Event` | Best-effort bounded delivery. | Logs, metrics, traces, and live status. | + +A watched outcome is independent of event loss while the process and runtime remain alive. +It is not durable storage. +`TaskWaiter::wait` can return `OutcomeUnavailable` if its completion channel closes unexpectedly. + +Final outcomes distinguish: + +| Outcome | Meaning | +|----------------|----------------------------------------------------------------------| +| `Completed` | The final attempt succeeded and the restart policy stopped the task. | +| `Failed` | Retryable failure stopped under policy or retry limit. | +| `Fatal` | The task reported a permanent failure. | +| `Canceled` | Cancellation was requested or reported. | +| `ForceAborted` | Taskvisor stopped waiting before cooperative termination completed. | +| `Panicked` | The actor or a protected user-value cleanup boundary panicked. | +| `Rejected` | Admission rejected the work, or queued controller work was removed. | + +Use stable outcome and rejection kinds for branching, metrics, and alerts. +Treat reason strings as diagnostic text. +A panic while polling task code becomes a retryable task failure instead of `Panicked`. +Removing watched controller work before it runs produces `Rejected` with `RejectionKind::RemovedFromQueue`, not `Canceled`. + +`ForceAborted` normally follows the configured grace period. +Last-owner fallback and signal-setup failure cleanup cannot wait for that period. +The physical attempt can remain active until synchronous task code returns control to Tokio. + +The shared event bus and every subscriber queue are bounded. +Events can be lost at the shared bus or in an individual subscriber queue. +When the shared bus is full, it drops the oldest event and retains the newest one. +When one subscriber queue is full, Taskvisor drops the incoming event for that subscriber. +Each subscriber receives callbacks serially in its own order, but two different subscribers can run at the same time. + +Subscriber callbacks are synchronous and run outside Tokio worker threads. Keep them short. +Forward async or long blocking work to an application-owned queue. +Overflow and shutdown deadlines can drop events; overflow diagnostics report loss when possible. + +See [outcomes.rs](../examples/outcomes.rs), [custom_subscriber.rs](../examples/custom_subscriber.rs), [logging.rs](../examples/logging.rs) (requires `logging`), [tracing.rs](../examples/tracing.rs) (requires `tracing`), and [metrics.rs](../examples/metrics.rs). diff --git a/docs/production-boundaries.md b/docs/production-boundaries.md new file mode 100644 index 0000000..e142efa --- /dev/null +++ b/docs/production-boundaries.md @@ -0,0 +1,41 @@ +--- +title: Production boundaries +description: Understand Taskvisor durability, cancellation, observability, scheduling, and ownership boundaries before deployment. +--- + +# Production boundaries + +## In-process state + +- Runtime state, task IDs, controller queues, watched outcomes, and events are not durable. +- Taskvisor does not recover work after process failure. +- A watched outcome belongs to the current caller and process. + +## Cooperative cancellation + +- Long-running tasks must observe `TaskContext`. +- Synchronous task code cannot be interrupted in the middle of a poll. +- `ForceAborted` can be delivered before the physical attempt returns. +- Attempt timeout drops the future but cannot undo external side effects. + +## Best-effort observability + +- The shared event bus and subscriber queues can drop events. +- Subscriber callbacks already running cannot be interrupted at the drain deadline. +- Use watched outcomes rather than events for application correctness. + +## Scheduling and coordination scope + +- Periodic work uses a delay after completion, not cron or missed-run recovery. +- Controller coordination is local to one supervisor. +- A controller slot is not a cancellation key. +- Supervisor-local budgets do not isolate operating-system CPU, memory, or thread limits. + +## Owned user values + +- Accepted tasks and configured subscribers consume ownership capacity through physical cleanup. +- Blocking destructors for retained task or subscriber values occupy cleanup workers until they return. +- A panic while those values are destroyed permanently retires one unit from a finite ownership capacity. +- Removing the ownership limit allows retained user values and cleanup backlog to grow without a count bound. + +The crate forbids unsafe Rust with `#![forbid(unsafe_code)]`. diff --git a/docs/running-and-managing.md b/docs/running-and-managing.md new file mode 100644 index 0000000..6b466fa --- /dev/null +++ b/docs/running-and-managing.md @@ -0,0 +1,89 @@ +--- +title: Run and manage Taskvisor +description: Choose a supervisor entry point and manage registered or controller-submitted work at runtime. +--- + +# Choose how the supervisor runs + +Choose an entry point based on how tasks are supplied and who requests shutdown: + +| Entry point | Use it when | +|-----------------------------------|-------------------------------------------------------------| +| `Supervisor::run` | The initial batch finishes naturally. | +| `Supervisor::run_until` | The application owns the future that requests shutdown. | +| `Supervisor::run_with_os_signals` | Taskvisor should install process signal handlers. | +| `Supervisor::serve` | Work is discovered or managed while the service is running. | + +`run`, `run_until`, and `run_with_os_signals` submit one initial batch through all-or-nothing registry admission. +Admission can reject the full batch. +`run_until` can begin shutdown before the batch commits, and `run_with_os_signals` can enter cleanup before the commit if signal-listener setup fails. +An `Ok(())` return confirms that the shared supervisor lifecycle and cleanup workflow completed; it does not mean every task succeeded. +Use watched work when application logic needs each final result. + +Tasks already registered through `serve` keep the registry non-empty and participate in the static lifecycle. +A batch rejected by the registry after the static lifecycle commits consumes that lifecycle; errors before the commit leave it available for another static run. +Registry rejection does not stop tasks that were added earlier through `serve`. +Dropping a static run future after its lifecycle commits does not stop admitted tasks or start shutdown. +A handle returned by `serve` can still request shutdown. + +These three methods share one static lifecycle. +After one commits, another static run on the same supervisor returns `RuntimeError::AlreadyRunning`. + +`run` and `run_until` do not install operating-system signal handlers. +`run_with_os_signals` is the explicit process-wide opt-in. +An embedded application that already owns signals should use `run_until` or request shutdown through a dynamic handle. + +On Unix, dropping Taskvisor's signal listeners does not restore the default signal disposition. +The application remains responsible for signal handling after the method returns. + +`serve` starts the same runtime without a static batch and returns a `SupervisorHandle`. +It does not install signal handlers. +Call `handle.shutdown().await` when the application wants the joined cleanup result. + +Create a supervisor with `Supervisor::new` when runtime configuration and subscribers are enough. +Use `Supervisor::builder` when the application also needs task defaults, a controller, or typed construction errors through `try_build`. + +Runnable entry-point examples: + +- [basic.rs](../examples/basic.rs) uses `run`; +- [application_shutdown.rs](../examples/application_shutdown.rs) uses `run_until`; +- [graceful_worker.rs](../examples/graceful_worker.rs) uses `run_with_os_signals`; +- [dynamic_tasks.rs](../examples/dynamic_tasks.rs) uses `serve`. + +## Manage tasks at runtime + +A dynamic handle separates task registration from task completion. + +| Operation | What an `Ok` result means | +|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| +| `add` | The runtime registry accepted the task. The first attempt may not have started yet. | +| `add_and_watch` | Registration succeeded and the caller received a final-outcome waiter. | +| `submit` | The controller accepted the command. Slot admission happens later. | +| `submit_and_watch` | Command intake succeeded and the caller received a waiter for rejection or the admitted task's final outcome. | +| `TaskWaiter::wait` | A direct final in-process outcome was delivered. | +| `remove` | The boolean says whether this call created the stop claim. Registered cleanup may continue; queued work is removed before return. | +| `cancel` | The boolean says whether this call created the stop claim. For registered work, registry membership and the final outcome are settled before return. | + +`false` can mean the work was unknown, already finished, or already claimed by another stop request. +A `cancel` call that joins an existing removal waits for the same cleanup and also returns `false`. + +The `submit*` methods require the `controller` Cargo feature and a supervisor built with a controller. +Without an installed controller, they return `ControllerError::NotConfigured`. + +Use `TaskId` for one exact registration or controller submission. +Use `remove_by_name` and `cancel_by_name` for registered work addressed by task name. +Controller work that is still queued does not own a registered task name; stop it with the task ID returned by `submit*`. + +`list` returns registry membership. +It includes tasks waiting for attempt capacity, in retry backoff, running, or completing cleanup. +`alive_snapshot` and `is_alive` answer a different question: whether a physical attempt is still active. +Both are point-in-time snapshots and may be stale as soon as concurrent work changes. + +Regular `add*` calls wait for ownership admission and registry-command capacity. +Their `try_add*` forms fail fast at both boundaries, then still wait for the registry decision. +Controller `submit*` calls wait for ownership admission and controller-command capacity; their `try_submit*` forms fail fast at both boundaries and return after command intake. +Regular stop operations wait for the required management intake resources, while their `try_*` forms fail fast at those boundaries. +Later registry or controller decisions can still reject work after command intake. +The exact boundary and error are documented on each method in the [API reference](https://docs.rs/taskvisor/latest/taskvisor/core/struct.SupervisorHandle.html). + +See [dynamic_tasks.rs](../examples/dynamic_tasks.rs) for one complete management flow. diff --git a/docs/site.yml b/docs/site.yml new file mode 100644 index 0000000..7bcac3b --- /dev/null +++ b/docs/site.yml @@ -0,0 +1,38 @@ +schema: 1 +product: taskvisor +title: Taskvisor +compatibility_line: "0.8" +repository: https://github.com/soltiHQ/taskvisor + +version: + provider: cargo + package: taskvisor + +reference: + label: API reference + url: https://docs.rs/taskvisor/{version}/taskvisor/ + +navigation: + - title: Start + pages: + - index + - mental-model + - installation + - defining-tasks + - lifecycle-policies + + - title: Run + pages: + - running-and-managing + - cancellation-and-shutdown + + - title: Extend + pages: + - outcomes-and-events + - keyed-admission + - configuration + + - title: Deploy + pages: + - production-boundaries + - common-mistakes diff --git a/guide.md b/guide.md index 928c2e5..9412901 100644 --- a/guide.md +++ b/guide.md @@ -1,525 +1,5 @@ # Taskvisor user guide -This guide explains how to use Taskvisor in an application and how to choose between its public workflows. -For exact method signatures, error variants, and edge-case contracts, use the [API documentation](https://docs.rs/taskvisor). +The user guide is maintained as versioned source pages in [`docs/`](docs/index.md). -Taskvisor is an in-process runtime. Tasks, queued submissions, task IDs, events, and watched outcomes do not survive process exit. -Use durable external storage when work must resume after a restart. - -- New to Taskvisor? Run the [Quick start](README.md#quick-start). -- Looking for a complete program? Follow the [examples guide](examples/README.md). -- Changing Taskvisor itself? Start with the [contributor map](src/ARCHITECTURE.md). - -## In this guide - -- Start: [mental model](#mental-model), [installation](#install-taskvisor), [task definition](#define-a-task), and [task behavior](#choose-task-behavior). -- Run: [supervisor entry points](#choose-how-the-supervisor-runs), [runtime management](#manage-tasks-at-runtime), and [cancellation](#cancellation-and-shutdown). -- Extend: [outcomes and events](#final-outcomes-and-lifecycle-events), [per-key coordination](#coordinate-work-by-key), and [configuration](#configure-taskvisor). -- Deploy: [production boundaries](#production-boundaries) and [common mistakes](#common-mistakes). - -## Mental model - -A task defines executable work. A task specification gives that work a name and lifecycle policy. -The supervisor owns registration, attempts, cancellation, and cleanup. - -```text -Task / TaskFn ──► TaskSpec - ├── add* ──► registry - └── ControllerSpec - └── submit* ──► controller - ├── admitted ──► registry - └── rejected ──► watched TaskWaiter - -registry ──► supervised attempts - ├── watched ──► TaskWaiter - └── observed ─► Event subscribers -``` - -| Value | Role | -|--------------------|---------------------------------------------------------------------| -| `Task` or `TaskFn` | Creates a fresh future for each attempt. | -| `TaskSpec` | Gives work its registry name and execution policy. | -| `Supervisor` | Owns one Taskvisor runtime. | -| `SupervisorHandle` | Manages a running supervisor. | -| `TaskId` | Identifies one process-local registration or controller submission. | -| Task name | Uniquely identifies registry membership inside one supervisor. | -| Controller slot | Coordinates submissions that must not own the same key together. | -| `TaskWaiter` | Delivers one direct in-process final outcome. | -| `Event` | Describes lifecycle activity through best-effort delivery. | - -Direct `add*` methods send a `TaskSpec` to the runtime registry. -Controller `submit*` methods first apply a per-slot admission policy, then hand admitted work to the same registry. - -## Install Taskvisor - -The default install includes the controller API: - -```toml -taskvisor = "0.8" -``` - -The controller has no runtime effect until a supervisor is built with `with_controller`. - -| Feature | Default | Adds | -|----------------------|---------|--------------------------------------------------------| -| `controller` | Yes. | Slot-based admission control. | -| `tracing` | No. | `TracingBridge` for the `tracing` ecosystem. | -| `logging` | No. | `LogWriter` for simple readable lifecycle output. | -| `tokio-util-interop` | No. | Access to the raw cancellation token in `TaskContext`. | -| `test-util` | No. | Constructors intended for external integration tests. | - -Enable an optional integration: - -```toml -taskvisor = { version = "0.8", features = ["tracing"] } -``` - -Build without keyed admission: - -```toml -taskvisor = { version = "0.8", default-features = false } -``` - -## Define a task - -Use `TaskFn` for an async closure: - -```rust -use taskvisor::{TaskFn, TaskRef}; - -let task: TaskRef = TaskFn::arc(|_ctx| async { - println!("one attempt"); - Ok(()) -}); -``` - -Implement `Task` when a reusable type should hold state or dependencies across attempts. -Each call to `Task::spawn` must return a fresh future. -Keep synchronous work in `spawn` short; put the actual operation inside the returned future. - -A shared `TaskRef` can back several registrations. Registrations that overlap in one supervisor need different names. -A name can be reused after the earlier registration releases it. -The registrations receive different task IDs, and their `spawn` calls may run concurrently when configured attempt capacity permits. -Shared task state must support that use. - -After a force-abort, Taskvisor may keep the name reserved until it observes that the task attempt has physically returned. - -Keep blocking and CPU-heavy work away from Tokio worker threads. -Use a suitable blocking executor, worker pool, or external runtime. -Also keep the destructor of an attempt future short: Taskvisor drops that future synchronously when the attempt ends or is canceled. - -Runnable examples: - -- [basic.rs](examples/basic.rs) uses `TaskFn` for one static task; -- [task_type.rs](examples/task_type.rs) implements `Task` for reusable state; -- [queue_consumer.rs](examples/queue_consumer.rs) supervises a cancellation-aware receive loop; -- [cpu_job.rs](examples/cpu_job.rs) moves CPU work to Rayon and explains the cancellation limit. - -## Choose task behavior - -`TaskSpec` selects what follows success or a retryable failure: - -| Constructor | After success | After a retryable failure | -|---------------------------|-------------------------------------|----------------------------------------------------------| -| `TaskSpec::once` | Stop. | Stop. | -| `TaskSpec::restartable` | Stop. | Retry if the policy and retry limit allow. | -| `TaskSpec::periodic` | Wait after completion, then repeat. | Retry through failure backoff if the retry limit allows. | -| `TaskSpec::from_defaults` | Use `TaskDefaults`. | Use `TaskDefaults`. | - -One task ID runs attempts sequentially. Two attempts for that ID never overlap. - -| Attempt result | Meaning | -|-----------------------|----------------------------------------------------------------------| -| `Ok(())` | Success. The restart policy decides whether another attempt follows. | -| `TaskError::Fail` | Retryable failure. | -| `TaskError::Fatal` | Permanent failure; stop without retry. | -| `TaskError::Canceled` | Cooperative cancellation; stop without retry. | -| Attempt timeout | Retryable timeout failure. | - -With panic unwinding enabled, a panic while creating or polling the attempt future becomes a retryable failure. -A panic during protected cleanup can instead produce a final `Panicked` outcome. -`panic = "abort"` cannot be caught. - -A retry limit counts retries after the first failed attempt. -A limit of three therefore allows at most four consecutive failed attempts. -A successful attempt resets the failure streak. - -```rust -use std::num::NonZeroU32; -use std::time::Duration; -use taskvisor::{BackoffPolicy, JitterPolicy, TaskRef, TaskSpec}; - -fn supervised(name: &str, task: TaskRef) -> TaskSpec { - TaskSpec::restartable(name, task) - .with_backoff( - BackoffPolicy::exponential(Duration::from_millis(200)) - .with_max(Duration::from_secs(30)) - .with_jitter(JitterPolicy::Equal), - ) - .with_timeout(Duration::from_secs(10)) - .with_max_retries(NonZeroU32::new(3).unwrap()) -} -``` - -Equal jitter chooses a delay between half of the current base delay and the full base delay. -This spreads retries that would otherwise happen together. -Per-task settings override values inherited from `TaskDefaults`. - -A periodic interval starts after a successful attempt completes. -It is fixed-delay scheduling, not a wall-clock or cron schedule. - -See [periodic.rs](examples/periodic.rs), [restart_policies.rs](examples/restart_policies.rs), and [configuration.rs](examples/configuration.rs). - -## Choose how the supervisor runs - -Choose an entry point based on how tasks are supplied and who requests shutdown: - -| Entry point | Use it when | -|-----------------------------------|-------------------------------------------------------------| -| `Supervisor::run` | The initial batch finishes naturally. | -| `Supervisor::run_until` | The application owns the future that requests shutdown. | -| `Supervisor::run_with_os_signals` | Taskvisor should install process signal handlers. | -| `Supervisor::serve` | Work is discovered or managed while the service is running. | - -`run`, `run_until`, and `run_with_os_signals` submit one initial batch through all-or-nothing registry admission. -Admission can reject the full batch; `run_until` can begin shutdown before the batch commits. -Their return value describes the shared supervisor lifecycle and cleanup workflow; it does not mean every task succeeded. -Use watched work when application logic needs each final result. - -These three methods share one static lifecycle. -After one commits, another static run on the same supervisor returns `RuntimeError::AlreadyRunning`. - -`run` and `run_until` do not install operating-system signal handlers. -`run_with_os_signals` is the explicit process-wide opt-in. -An embedded application that already owns signals should use `run_until` or request shutdown through a dynamic handle. - -On Unix, dropping Taskvisor's signal listeners does not restore the default signal disposition. -The application remains responsible for signal handling after the method returns. - -`serve` starts the same runtime without a static batch and returns a `SupervisorHandle`. -It does not install signal handlers. -Call `handle.shutdown().await` when the application wants the joined cleanup result. - -Create a supervisor with `Supervisor::new` when runtime configuration and subscribers are enough. -Use `Supervisor::builder` when the application also needs task defaults, a controller, or typed construction errors through `try_build`. - -Runnable entry-point examples: - -- [basic.rs](examples/basic.rs) uses `run`; -- [application_shutdown.rs](examples/application_shutdown.rs) uses `run_until`; -- [graceful_worker.rs](examples/graceful_worker.rs) uses `run_with_os_signals`; -- [dynamic_tasks.rs](examples/dynamic_tasks.rs) uses `serve`. - -## Manage tasks at runtime - -A dynamic handle separates task registration from task completion. - -| Operation | What an `Ok` result means | -|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| `add` | The runtime registry accepted the task. The first attempt may not have started yet. | -| `add_and_watch` | Registration succeeded and the caller received a final-outcome waiter. | -| `submit` | The controller accepted the command. Slot admission happens later. | -| `submit_and_watch` | Command intake succeeded and the caller received a waiter for rejection or the admitted task's final outcome. | -| `TaskWaiter::wait` | A direct final in-process outcome was delivered. | -| `remove` | The boolean says whether this call created the stop claim. Registered cleanup may continue; queued work is removed before return. | -| `cancel` | The boolean says whether this call created the stop claim. For registered work, registry membership and the final outcome are settled before return. | - -`false` can mean the work was unknown, already finished, or already claimed by another stop request. -A `cancel` call that joins an existing removal waits for the same cleanup and also returns `false`. - -Use `TaskId` for one exact registration or controller submission. -Use `remove_by_name` and `cancel_by_name` for registered work addressed by task name. -Controller work that is still queued does not own a registered task name; stop it with the task ID returned by `submit*`. - -`list` returns registry membership. -It includes tasks waiting for attempt capacity, in retry backoff, running, or completing cleanup. -`alive_snapshot` and `is_alive` answer a different question: whether a physical attempt is still active. -Both are point-in-time snapshots and may be stale as soon as concurrent work changes. - -State-changing async methods wait for capacity at their command boundary. -Their `try_*` variants fail immediately when the required capacity is unavailable. -After command admission, they still wait for the normal decision. -The exact boundary and error are documented on each method in the [API reference](https://docs.rs/taskvisor/latest/taskvisor/core/struct.SupervisorHandle.html). - -See [dynamic_tasks.rs](examples/dynamic_tasks.rs) for one complete management flow. - -## Cancellation and shutdown - -Cancellation starts cooperatively. A resident task must observe `TaskContext`: - -```rust -use taskvisor::{TaskContext, TaskError}; - -async fn do_work() -> Result<(), TaskError> { - // Application work goes here. - Ok(()) -} - -async fn run_one_operation(ctx: &TaskContext) -> Result<(), TaskError> { - ctx.run_until_cancelled(do_work()).await? -} - -async fn run_with_more_branches(ctx: &TaskContext) -> Result<(), TaskError> { - tokio::select! { - _ = ctx.cancelled() => Err(TaskError::Canceled), - result = do_work() => result, - } -} -``` - -`run_until_cancelled` drops the wrapped future when cancellation wins. -Use it only when dropping that future is a safe way to cancel the exact operation. -Check the operation's cancellation-safety contract; an external commit, acknowledgement, or partially consumed input may need an explicit protocol. -The Tokio sleep in [graceful_worker.rs](examples/graceful_worker.rs) is a simple drop-safe example. - -An attempt timeout also drops the attempt future. -It does not undo side effects that already happened. -A blocking future destructor can delay attempt release beyond the configured timeout. - -The joined shutdown path: - -1. Closes admission for new work. -2. Rejects pending controller work and requests cancellation for registered tasks. -3. Waits through the configured grace period. -4. Commits `ForceAborted` for tasks that did not stop in time. -5. Finishes runtime cleanup and drains subscriber queues up to their separate deadline. - -Taskvisor cannot interrupt synchronous code in the middle of a poll. -After the grace period, the final outcome may be `ForceAborted` while that synchronous code is still physically running. -The supervisor keeps ownership until it returns. - -`handle.shutdown().await` joins the shared shutdown workflow and returns its result. -Dropping the final public owner can request cancellation, but a destructor cannot await cleanup or report its errors. - -## Final outcomes and lifecycle events - -Taskvisor has two result paths with different contracts: - -| Path | Contract | Use it for | -|--------------------------------|-------------------------------------------------|-----------------------------------------| -| `TaskWaiter` and `TaskOutcome` | One direct final result, outside the event bus. | Application decisions. | -| `Subscribe` and `Event` | Best-effort bounded delivery. | Logs, metrics, traces, and live status. | - -A watched outcome is independent of event loss while the process and runtime remain alive. -It is not durable storage. -`TaskWaiter::wait` can return `OutcomeUnavailable` if its completion channel closes unexpectedly. - -Final outcomes distinguish: - -| Outcome | Meaning | -|----------------|-------------------------------------------------------------------------| -| `Completed` | The final attempt succeeded and the restart policy stopped the task. | -| `Failed` | Retryable failure stopped under policy or retry limit. | -| `Fatal` | The task reported a permanent failure. | -| `Canceled` | Cancellation was requested or reported. | -| `ForceAborted` | Cooperative stop did not finish within the allowed wait. | -| `Panicked` | The managed lifecycle or protected cleanup panicked. | -| `Rejected` | Controller or registry admission rejected the work before its body ran. | - -Use stable outcome and rejection kinds for branching, metrics, and alerts. -Treat reason strings as diagnostic text. - -The shared event bus and every subscriber queue are bounded. -Events can be lost at the shared bus or in an individual subscriber queue. -Each subscriber receives callbacks serially in its own order, but two different subscribers can run at the same time. - -Subscriber callbacks are synchronous and run outside Tokio worker threads. Keep them short. -Forward async or long blocking work to an application-owned queue. -Overflow and shutdown deadlines can drop events; overflow diagnostics report loss when possible. - -See [outcomes.rs](examples/outcomes.rs), [custom_subscriber.rs](examples/custom_subscriber.rs), [logging.rs](examples/logging.rs), [tracing.rs](examples/tracing.rs), and [metrics.rs](examples/metrics.rs). - -## Coordinate work by key - -This section requires the `controller` feature. -It is enabled by default, but each supervisor must install a controller explicitly: - -```rust -use taskvisor::{ControllerConfig, Supervisor, SupervisorConfig}; - -let _supervisor = Supervisor::builder(SupervisorConfig::default()) - .with_controller(ControllerConfig::default()) - .build(); -``` - -Direct `add*` methods bypass controller admission. -`submit*` methods accept a `ControllerSpec`, apply its slot policy, and hand admitted work to the runtime registry. - -| Identity | Scope | -|-----------------|----------------------------------------------------------| -| `TaskId` | One process-local registration or controller submission. | -| Task name | Registry key inside one supervisor. | -| Controller slot | Admission key inside one supervisor controller. | - -Different task names can share a slot. -Without an explicit `with_slot`, the task name is also the slot. -A queued submission owns its task ID but does not own a registered task name yet. - -A controller slot can remain occupied while admission, task execution, or physical release is pending. -An occupied slot does not always mean that a task body is currently polling. - -| Policy | Busy-slot behavior | -|-----------------|----------------------------------------------------------------------------------------| -| `Queue` | Append to the bounded FIFO queue. A later `Replace` can still displace the queue head. | -| `Replace` | Request owner retirement and create or replace the queue head. | -| `DropIfRunning` | Reject the incoming submission without changing the owner or queue. | - -A replacement is not guaranteed to become the next owner. -A newer `Replace` can supersede it before admission, and later registry admission can still reject it. - -```rust -use taskvisor::{ControllerSpec, TaskFn, TaskRef, TaskSpec}; - -let task: TaskRef = TaskFn::arc(|_ctx| async { Ok(()) }); -let request = ControllerSpec::queue(TaskSpec::once("customer-42-job", task)) - .with_slot("customer-42"); - -assert_eq!(request.task_spec().name(), "customer-42-job"); -assert_eq!(request.slot_name(), "customer-42"); -``` - -`submit().await?` confirms command intake only. `submit_and_watch` returns a task ID and waiter. -The waiter resolves to `Rejected` if admission fails or to the registered task's final outcome if admission succeeds. - -`prepare_submission` allocates a task ID before intake. -It does not reserve a name, slot, queue position, or runtime capacity. - -`controller_snapshot` is a rolling diagnostic view. -It reads slots independently and can already be stale when returned. -Do not treat it as a transaction boundary. - -Attempt timeout starts only after registry admission and after `Task::spawn` returns the attempt future. -It does not limit time spent in a controller queue. Controller submission has no built-in end-to-end deadline. - -Slots govern admission, not cancellation. -There is no slot-wide cancel or remove operation. -Stop queued work by task ID and registered work by task ID or task name. - -`ControllerConfig` bounds command intake, per-slot queues, total pending work, tracked slots, registry-capacity waits, and concurrent identity operations. -See its [API documentation](https://docs.rs/taskvisor/latest/taskvisor/controller/struct.ControllerConfig.html) for the exact defaults and rejection mapping. - -Runnable controller examples: - -- [controller_slots.rs](examples/controller_slots.rs) compares all three policies; -- [controller_admission.rs](examples/controller_admission.rs) watches admission and rejection; -- [tenant_sync.rs](examples/tenant_sync.rs) keeps the newest waiting revision per tenant. - -## Configure Taskvisor - -Configuration has four levels: - -```text -SupervisorConfig ──► runtime-wide limits and shutdown -TaskDefaults ──────► inherited task behavior -TaskSpec ──────────► per-task overrides -ControllerConfig ──► keyed-admission limits -``` - -```rust -use std::num::{NonZeroU32, NonZeroUsize}; -use std::sync::Arc; -use std::time::Duration; -use taskvisor::{Supervisor, SupervisorConfig, TaskDefaults}; - -fn configured_supervisor() -> Arc { - let runtime = SupervisorConfig::default() - .with_grace(Duration::from_secs(30)) - .with_subscriber_shutdown_timeout(Duration::from_secs(5)) - .with_max_concurrent(NonZeroUsize::new(16)) - .with_ownership_capacity(NonZeroUsize::new(4096)); - - let tasks = TaskDefaults::default() - .with_timeout(Duration::from_secs(20)) - .with_max_retries(NonZeroU32::new(5).unwrap()); - - Supervisor::builder(runtime) - .with_task_defaults(tasks) - .build() -} -``` - -Main defaults: - -| Setting | Default | -|---------------------------|------------------------------------------------------------| -| Graceful task shutdown | 60 seconds. | -| Subscriber drain | 5 seconds, shared by all subscriber queues. | -| Concurrent task attempts | Unlimited. | -| Registered-task limit | 1024. | -| Ownership capacity | 1024 per supervisor across accepted tasks and subscribers. | -| Event bus capacity | 1024. | -| Registry command capacity | 1024. | -| Restart policy | On retryable failure. | -| Failure backoff | Exponential from 200 ms to 30 seconds with equal jitter. | -| Attempt timeout | None. | -| Failure retry limit | Unlimited. | - -Three limits answer different questions: - -| Limit | What it bounds | -|------------------------|-------------------------------------------------------------------------------------------------------| -| `max_concurrent` | Attempts physically running at the same time. | -| `max_registered_tasks` | Registered and removing tasks through terminal cleanup; force-aborted work can remain charged longer. | -| `ownership_capacity` | Accepted task and subscriber values still owned through physical cleanup. | - -`SupervisorConfig::with_ownership_capacity(None)` removes the ownership count bound. -Cleanup still uses a bounded worker set, but retained values and cleanup backlog can then grow without a count limit. - -Capacity values are non-zero where zero would make the runtime unusable. -Checked `try_with_*`methods accept raw integers and return a configuration error for invalid values. - -## Production boundaries - -### In-process state - -- Runtime state, task IDs, controller queues, watched outcomes, and events are not durable. -- Taskvisor does not recover work after process failure. -- A watched outcome belongs to the current caller and process. - -### Cooperative cancellation - -- Long-running tasks must observe `TaskContext`. -- Synchronous task code cannot be interrupted in the middle of a poll. -- `ForceAborted` can be delivered before the physical attempt returns. -- Attempt timeout drops the future but cannot undo external side effects. - -### Best-effort observability - -- The shared event bus and subscriber queues can drop events. -- Subscriber callbacks already running cannot be interrupted at the drain deadline. -- Use watched outcomes rather than events for application correctness. - -### Scheduling and coordination scope - -- Periodic work uses a delay after completion, not cron or missed-run recovery. -- Controller coordination is local to one supervisor. -- A controller slot is not a cancellation key. -- Supervisor-local budgets do not isolate operating-system CPU, memory, or thread limits. - -### Owned user values - -- Accepted tasks and configured subscribers consume ownership capacity through physical cleanup. -- Blocking user destructors occupy cleanup workers until they return. -- A panic in a user destructor permanently retires one unit from a finite ownership capacity. -- Removing the ownership limit allows retained user values and cleanup backlog to grow without a count bound. - -The crate forbids unsafe Rust with `#![forbid(unsafe_code)]`. - -## Common mistakes - -- Treating `run().await == Ok(())` as proof that every task succeeded. -- Treating `submit().await?` as positive slot admission. -- Using best-effort events for application decisions. -- Forgetting to observe cancellation in a resident task. -- Treating a controller slot as a registered task name. -- Running blocking or CPU-heavy work on Tokio worker threads. -- Assuming a timeout or force-abort can undo external side effects. - -## Continue learning - -| Resource | Next step | -|------------------------------------------------|----------------------------------------------------| -| [Examples guide](examples/README.md) | Choose a complete runnable scenario. | -| [API documentation](https://docs.rs/taskvisor) | Read exact contracts for public types and methods. | -| [Benchmark guide](benches/README.md) | Run and interpret the Criterion suites. | -| [Contributor map](src/ARCHITECTURE.md) | Follow runtime ownership and source boundaries. | +Start with the [guide index](docs/index.md), or open the [API documentation](https://docs.rs/taskvisor) for exact method signatures, error variants, and edge-case contracts. diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000..5bfc616 --- /dev/null +++ b/lychee.toml @@ -0,0 +1,3 @@ +remap = [ + 'https://github\.com/soltiHQ/taskvisor/blob/main/(.*) file:///workspace/$1', +] diff --git a/src/ARCHITECTURE.md b/src/ARCHITECTURE.md index b2be96c..5cda0c6 100644 --- a/src/ARCHITECTURE.md +++ b/src/ARCHITECTURE.md @@ -3,7 +3,7 @@ This document is the entry point for contributors and reviewers. It explains what each part of the project owns, how the parts connect, and where to begin a change. -For application usage, start with the [README](../README.md), the [user guide](../guide.md), the [crate documentation](https://docs.rs/taskvisor), and the [examples guide](../examples/README.md). +For application usage, start with the [README](../README.md), the [user guide](../docs/index.md), the [crate documentation](https://docs.rs/taskvisor), and the [examples guide](../examples/README.md). Exact contracts live in the Rust source and its module-level documentation. ## Architecture at a glance @@ -190,7 +190,7 @@ Remaining retained task and subscriber values move to [`core/deferred_drop/`](co | Shared shutdown order or grace behavior | [`core/runtime/shutdown_workflow/`](core/runtime/shutdown_workflow), [`core/runtime/lifecycle/`](core/runtime/lifecycle), [`core/registry/removal/`](core/registry/removal), [`controller/engine/lifecycle/shutdown.rs`](controller/engine/lifecycle/shutdown.rs) | [`tests/shutdown.rs`](../tests/shutdown.rs), [`tests/ownership.rs`](../tests/ownership.rs) | | Operating-system signal handling | [`core/shutdown.rs`](core/shutdown.rs), [`core/supervisor.rs`](core/supervisor.rs) | [`tests/signal_ownership.rs`](../tests/signal_ownership.rs) | | Ownership limits or deferred cleanup | [`core/config.rs`](core/config.rs), [`core/builder.rs`](core/builder.rs), [`core/deferred_drop/`](core/deferred_drop), [`core/registry/removal/`](core/registry/removal) | [`tests/ownership.rs`](../tests/ownership.rs), [`tests/shutdown.rs`](../tests/shutdown.rs) | -| User-facing documentation or workflows | [`README.md`](../README.md), [`guide.md`](../guide.md), [`examples/`](../examples), [`lib.rs`](lib.rs) | Example compilation and crate docs | +| User-facing documentation or workflows | [`README.md`](../README.md), [`docs/`](../docs/index.md), [`examples/`](../examples), [`lib.rs`](lib.rs) | Example compilation and crate docs | ## Read and validate a change diff --git a/src/events/bus.rs b/src/events/bus.rs index ca222c5..9c8822d 100644 --- a/src/events/bus.rs +++ b/src/events/bus.rs @@ -17,8 +17,9 @@ //! The receiver gets that count with the next retained event. This lets the relay emit one overflow //! diagnostic before it continues normal delivery. //! -//! The bus stays disabled when the runtime has no event consumer. When the relay shuts down, it closes -//! publication and transfers retained values out of the ring lock. Events never control runtime state. +//! The bus stays disabled when the runtime has no event consumer. +//! When the relay shuts down, it closes publication and transfers retained values out of the ring lock. +//! Events never control runtime state. use std::{ collections::VecDeque, diff --git a/src/events/event.rs b/src/events/event.rs index 26785a3..e2d1285 100644 --- a/src/events/event.rs +++ b/src/events/event.rs @@ -1,9 +1,9 @@ //! Defines the event values delivered to subscribers. //! -//! Runtime components create an [`Event`] to describe a lifecycle action. Ordinary events enter the bounded -//! bus and may reach subscriber callbacks. Internal overflow and relay-failure diagnostics can enter subscriber -//! lanes directly. No event feeds back into task management or registry cleanup. Applications normally read -//! events through [`Subscribe`](crate::Subscribe), not construct them. +//! Runtime components create an [`Event`] to describe a lifecycle action. Ordinary events enter the bounded bus and may reach +//! subscriber callbacks. Internal overflow and relay-failure diagnostics can enter subscriber lanes directly. +//! No event feeds back into task management or registry cleanup. +//! Applications normally read events through [`Subscribe`](crate::Subscribe), not construct them. //! //! ```text //! runtime action @@ -20,11 +20,12 @@ //! provide typed categories where free-form text would be unsafe for machine decisions. //! //! Every event contains `kind`, `at`, and `seq`. Other fields depend on the event kind. -//! Read the variant documentation before using an optional field. Duration builders store whole -//! milliseconds and clamp values above `u32::MAX` milliseconds. +//! Read the variant documentation before using an optional field. +//! Duration builders store whole milliseconds and clamp values above `u32::MAX` milliseconds. //! -//! `seq` is an increasing process-local construction sequence. Concurrent effects and callbacks may -//! occur in another order. The sequence is not persisted and panics on exhaustion instead of wrapping. +//! `seq` is an increasing process-local construction sequence. +//! Concurrent effects and callbacks may occur in another order. +//! The sequence is not persisted and panics on exhaustion instead of wrapping. //! //! # Interpreting an event //! diff --git a/src/events/mod.rs b/src/events/mod.rs index 322c967..a631f34 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -1,9 +1,8 @@ //! Exposes Taskvisor's best-effort lifecycle stream for observability. //! -//! The registry, task actors, controller, and shutdown workflow publish ordinary [`Event`] values to -//! an internal bounded bus. The runtime relay forwards retained events to the bounded queue of each -//! [`Subscribe`](crate::Subscribe) implementation. Internal subscriber diagnostics can start -//! at the relay or a subscriber lane and bypass the shared bus. +//! The registry, task actors, controller, and shutdown workflow publish ordinary [`Event`] values to an internal bounded bus. +//! The runtime relay forwards retained events to the bounded queue of each [`Subscribe`](crate::Subscribe) implementation. +//! Internal subscriber diagnostics can start at the relay or a subscriber lane and bypass the shared bus. //! //! ```text //! runtime components @@ -25,9 +24,9 @@ //! | Final outcome for watched work | [`TaskWaiter`](crate::TaskWaiter) | //! | Result of a management command | The management method's returned result | //! -//! The stream is observational, not a reliable confirmation channel. Bus overflow and subscriber -//! queue pressure can drop events. Missing an event does not mean the action did not happen, -//! and runtime state never depends on delivery. +//! The stream is observational, not a reliable confirmation channel. +//! Bus overflow and subscriber queue pressure can drop events. +//! Missing an event does not mean the action did not happen, and runtime state never depends on delivery. //! //! [`EventKind`] identifies what happened. [`Event`] carries its metadata. //! [`TaskOutcomeKind`](crate::TaskOutcomeKind), [`BackoffSource`], and [`RejectionKind`] provide diff --git a/src/identity.rs b/src/identity.rs index e849aa8..ae7b751 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -12,14 +12,15 @@ //! └── controller submit ──► TaskId + slot ───────► controller ──► registry //! ``` //! -//! Taskvisor allocates the ID before the first admission decision. The same ID follows queued work, -//! every retry, terminal cleanup, and controller rejection. Several task names may use the same controller slot. +//! Taskvisor allocates the ID before the first admission decision. +//! The same ID follows queued work, every retry, terminal cleanup, and controller rejection. +//! Several task names may use the same controller slot. //! //! A name can be reused after registry membership ends and Taskvisor has observed the physical //! exit of any force-aborted actor with that name. Reuse allocates a new [`TaskId`]. -//! IDs come from a process-local `u64` sequence, are not persisted, and cannot be reconstructed -//! through the public API. Returned IDs are never zero and never wrap. The next allocation after -//! exhaustion panics. Store a separate application ID when identity must survive a process restart. +//! IDs come from a process-local `u64` sequence, are not persisted, and cannot be reconstructed through the public API. +//! Returned IDs are never zero and never wrap. The next allocation after exhaustion panics. +//! Store a separate application ID when identity must survive a process restart. use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/src/lib.rs b/src/lib.rs index f6df94c..db0859d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ //! - Observability: [custom subscriber], [logging], [tracing], and [metrics]. //! - Keyed admission: [controller slots], [controller admission], and [tenant sync]. //! -//! [user guide]: https://github.com/soltiHQ/taskvisor/blob/main/guide.md +//! [user guide]: https://github.com/soltiHQ/taskvisor/blob/main/docs/index.md //! [examples guide]: https://github.com/soltiHQ/taskvisor/blob/main/examples/README.md //! [basic]: https://github.com/soltiHQ/taskvisor/blob/main/examples/basic.rs //! [task type]: https://github.com/soltiHQ/taskvisor/blob/main/examples/task_type.rs @@ -205,10 +205,65 @@ See [`AdmissionPolicy`] for the exact queue, replace, and reject behavior. #[doc = include_str!("../README.md")] struct ReadmeDoctests; -/// Compiles runnable Rust code blocks in `guide.md` as doctests. +/// Compiles runnable Rust code blocks in the guide index as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/index.md")] +struct GuideIndexDoctests; + +/// Compiles runnable Rust code blocks in the mental-model guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/mental-model.md")] +struct MentalModelGuideDoctests; + +/// Compiles runnable Rust code blocks in the installation guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/installation.md")] +struct InstallationGuideDoctests; + +/// Compiles runnable Rust code blocks in the task-definition guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/defining-tasks.md")] +struct DefiningTasksGuideDoctests; + +/// Compiles runnable Rust code blocks in the lifecycle-policy guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/lifecycle-policies.md")] +struct LifecyclePoliciesGuideDoctests; + +/// Compiles runnable Rust code blocks in the runtime-management guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/running-and-managing.md")] +struct RunningAndManagingGuideDoctests; + +/// Compiles runnable Rust code blocks in the cancellation guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/cancellation-and-shutdown.md")] +struct CancellationAndShutdownGuideDoctests; + +/// Compiles runnable Rust code blocks in the outcome and event guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/outcomes-and-events.md")] +struct OutcomesAndEventsGuideDoctests; + +/// Compiles runnable Rust code blocks in the keyed-admission guide as doctests. #[cfg(all(doctest, feature = "controller"))] -#[doc = include_str!("../guide.md")] -struct GuideDoctests; +#[doc = include_str!("../docs/keyed-admission.md")] +struct KeyedAdmissionGuideDoctests; + +/// Compiles runnable Rust code blocks in the configuration guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/configuration.md")] +struct ConfigurationGuideDoctests; + +/// Compiles runnable Rust code blocks in the production-boundaries guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/production-boundaries.md")] +struct ProductionBoundariesGuideDoctests; + +/// Compiles runnable Rust code blocks in the common-mistakes guide as doctests. +#[cfg(doctest)] +#[doc = include_str!("../docs/common-mistakes.md")] +struct CommonMistakesGuideDoctests; pub mod core; pub use core::{ diff --git a/src/policies/mod.rs b/src/policies/mod.rs index 70f9f89..bb96dc1 100644 --- a/src/policies/mod.rs +++ b/src/policies/mod.rs @@ -10,13 +10,13 @@ //! ▼ //! task actor //! ├── success ──► RestartPolicy ──► stop or repeat -//! ├── retryable failure ──► RestartPolicy + retry limit -//! │ │ retry allowed -//! │ ▼ -//! │ BackoffPolicy -//! │ │ base delay -//! │ ▼ -//! │ JitterPolicy ──► retry delay +//! ├── retryable failure ──────────► RestartPolicy + retry limit +//! │ │ retry allowed +//! │ ▼ +//! │ BackoffPolicy +//! │ │ base delay +//! │ ▼ +//! │ JitterPolicy ──► retry delay //! └── fatal or canceled ──► stop //! ``` //! @@ -36,8 +36,8 @@ //! A periodic success uses its configured interval instead. //! //! The built-in defaults use [`RestartPolicy::OnFailure`], exponential backoff from `200ms` to `30s`, -//! equal jitter, no attempt timeout, and no retry-count limit. Named backoff constructors have no -//! jitter unless it is added explicitly. +//! equal jitter, no attempt timeout, and no retry-count limit. +//! Named backoff constructors have no jitter unless it is added explicitly. //! //! The retry limit counts retries after the first failed attempt in one failure streak. //! Success resets the count. Fatal errors and cancellation always stop. diff --git a/src/policies/restart.rs b/src/policies/restart.rs index 4083f34..9f9bd1d 100644 --- a/src/policies/restart.rs +++ b/src/policies/restart.rs @@ -11,9 +11,9 @@ //! | `OnFailure` | Stop | Retry if budget allows | //! | `Always` | Repeat; use interval if set | Retry if budget allows | //! -//! Failure timing belongs to [`BackoffPolicy`](crate::BackoffPolicy). The retry limit can stop an otherwise -//! eligible failure retry. It does not limit successful repeats under `Always`. Fatal errors, task cancellation, -//! and runtime cancellation always stop the task. +//! Failure timing belongs to [`BackoffPolicy`](crate::BackoffPolicy). +//! The retry limit can stop an otherwise eligible failure retry. +//! It does not limit successful repeats under `Always`. Fatal errors, task cancellation, and runtime cancellation always stop the task. /// Restart eligibility applied after one task attempt. /// diff --git a/src/reasons.rs b/src/reasons.rs index db9b6ba..187b4f5 100644 --- a/src/reasons.rs +++ b/src/reasons.rs @@ -6,9 +6,9 @@ //! └── detail ────► reason text built from these fragments //! ``` //! -//! Registry and controller paths reuse these strings when they build event and -//! watched-outcome payloads. The text is diagnostic only and has no stability -//! guarantee. Consumers must branch on typed categories instead of parsing it. +//! Registry and controller paths reuse these strings when they build event and watched-outcome payloads. +//! The text is diagnostic only and has no stability guarantee. +//! Consumers must branch on typed categories instead of parsing it. /// Name conflict detected during registry admission. pub(crate) const ALREADY_EXISTS: &str = "a registered task already uses this name"; diff --git a/src/subscribers/embedded/log.rs b/src/subscribers/embedded/log.rs index 9e266d9..d651e6e 100644 --- a/src/subscribers/embedded/log.rs +++ b/src/subscribers/embedded/log.rs @@ -7,10 +7,9 @@ //! ``` //! //! Each line starts with the event sequence and the stable [`EventKind::as_label`] value. -//! Event-specific fields follow as `key=value`. Free-form text is quoted, escaped, and -//! truncated after 4096 characters. The complete line format is intended for people and -//! is not a stable data format. It is not a complete serialization of [`Event`]; -//! use a custom subscriber or `TracingBridge` when every typed field is needed. +//! Event-specific fields follow as `key=value`. Free-form text is quoted, escaped, and truncated after 4096 characters. +//! The complete line format is intended for people and is not a stable data format. +//! It is not a complete serialization of [`Event`]; use a custom subscriber or `TracingBridge` when every typed field is needed. use crate::events::{Event, EventKind}; use crate::subscribers::Subscribe; diff --git a/src/subscribers/embedded/mod.rs b/src/subscribers/embedded/mod.rs index ca66fbd..b662674 100644 --- a/src/subscribers/embedded/mod.rs +++ b/src/subscribers/embedded/mod.rs @@ -11,8 +11,9 @@ //! └── tracing ──► TracingBridge ──► tracing event //! ``` //! -//! Enable `logging` for readable standard output. Enable `tracing` to emit structured fields into -//! the application's active tracing dispatcher. The parent module re-exports each type when its feature is enabled. +//! Enable `logging` for readable standard output. +//! Enable `tracing` to emit structured fields into the application's active tracing dispatcher. +//! The parent module re-exports each type when its feature is enabled. #[cfg(feature = "logging")] mod log; #[cfg(feature = "logging")] diff --git a/src/subscribers/embedded/tracing.rs b/src/subscribers/embedded/tracing.rs index 647b20f..9d92e45 100644 --- a/src/subscribers/embedded/tracing.rs +++ b/src/subscribers/embedded/tracing.rs @@ -7,8 +7,8 @@ //! event relay ──► subscriber queue ──► TracingBridge ──► tracing dispatcher //! ``` //! -//! Each callback emits one `tracing` event with target `taskvisor`. The `event` field -//! contains [`EventKind::as_label`], and `event_seq` preserves the Taskvisor sequence. +//! Each callback emits one `tracing` event with target `taskvisor`. +//! The `event` field contains [`EventKind::as_label`], and `event_seq` preserves the Taskvisor sequence. //! `event_unix_ms` is set when the timestamp is at or after the Unix epoch. //! Set payload fields use these names: //!