Skip to main content

boson/
lib.rs

1//! Boson is a Rust job-work runtime: durable background tasks, retries, rate limits, and
2//! pluggable persistence behind [`QueueBackend`].
3//!
4//! Wire a backend once with [`Boson::builder()`], define handlers with [`task`], then enqueue
5//! with typed `send_with` (after [`configure`]) or [`Boson::enqueue`]. Swap `mem`, `sqlite`,
6//! `postgres`, or fleet crates (`boson-backend-redis` / `boson-backend-nats`) without changing
7//! task code.
8//!
9//! ## Features
10//!
11//! - **Typed task handlers** — [`task`] macro with policy attributes and generated `send_with`
12//! - **Composable persistence** — inject [`MemQueueBackend`], [`SqliteQueueBackend`],
13//!   [`PostgresQueueBackend`](https://docs.rs/boson-backend-postgres), or fleet backends on
14//!   [`BosonBuilder`]
15//! - **Embedded or remote workers** — one process can enqueue and drain, or many hosts can
16//!   enqueue while a separate worker binary claims jobs (see
17//!   [Mode 2](#mode-2--remote-worker-two-binaries))
18//! - **Leases and pools** — multi-process coordination via [`BosonBuilder::lease_ttl_secs`] and
19//!   [`WorkerSettings`]
20//! - **HTTP admin (optional)** — nest [`boson_router`] at [`NEST_PATH`] when the `axum` feature
21//!   is enabled
22//!
23//! *Background jobs without locking you into one queue store.*
24//!
25//! This crate ships with **no default features** (`default = []`). Enable explicitly:
26//!
27//! - `mem` — [`MemQueueBackend`] for tests and local Mode 1
28//! - `sqlite` — [`SqliteQueueBackend`] durable single-host (or shared-file Mode 2)
29//! - `postgres` — [`PostgresQueueBackend`](https://docs.rs/boson-backend-postgres) shared durable state
30//! - `telemetry-console` — marker for console ops log ([`ConsoleOpsLog`] is always re-exported)
31//! - `axum` — HTTP admin API ([`boson_router`], [`BosonState`], [`NEST_PATH`])
32//!
33//! Fleet backends (`boson-backend-redis`, `boson-backend-nats`) are separate workspace crates.
34//!
35// Maintainer doc rules (not rendered in public docs):
36// - Task docs: #[task], macro attrs, send_with — link to boot items instead of duplicating.
37// - Boot docs: BosonBuilder, auto_registry, configure, backend, telemetry, axum — once per process.
38// - Backend docs: QueueBackend trait, reference adapters — link to How to implement.
39//!
40//! # Getting started
41//!
42//! You always define and enqueue tasks the same way (`#[task]`, `send_with`). What changes is
43//! **which process runs the worker loop**.
44//!
45//! ## Choose your topology
46//!
47//! - **[Mode 1 — Embedded](#mode-1--embedded-one-binary)** — one binary enqueues and drains.
48//!   Start here.
49//! - **[Mode 2 — Remote worker](#mode-2--remote-worker-two-binaries)** — API/host processes
50//!   enqueue; a separate **worker** binary (or fleet) claims and runs jobs.
51//!
52//! After you pick a mode, continue with [define tasks](#3-define-tasks) (shared by every mode).
53//!
54//! ## Mode 1 — Embedded (one binary)
55//!
56//! This process enqueues jobs **and** runs the worker loop (or drives [`ManualWorker`] in tests).
57//! There is no second binary. Default lease TTL is `0` (no distributed lease coordination).
58//!
59//! ```text
60//! Your app ──enqueue──► Boson ──worker loop──► mem / `SQLite` / Postgres / …
61//! ```
62//!
63//! | Backend | Type | Feature / crate | Topology | When to use |
64//! |---------|------|-----------------|----------|-------------|
65//! | In-memory | [`MemQueueBackend`] | `mem` | embedded only | Local experiments and tests |
66//! | `SQLite` | [`SqliteQueueBackend`] | `sqlite` | embedded (or Mode 2 on one host) | Durable single host |
67//! | Postgres | [`PostgresQueueBackend`](https://docs.rs/boson-backend-postgres) | `postgres` | embedded or remote | Shared durable state |
68//! | Redis | `RedisQueueBackend` | [`boson-backend-redis`](https://docs.rs/boson-backend-redis) | remote / fleet | Broker-backed multi-host |
69//! | NATS | `NatsQueueBackend` | [`boson-backend-nats`](https://docs.rs/boson-backend-nats) | remote / fleet | Broker-backed multi-host |
70//!
71//! **In-memory first run** (feature `mem`):
72//!
73//! ```rust,no_run
74//! # #[cfg(feature = "mem")]
75//! # {
76//! use std::sync::Arc;
77//!
78//! use boson::{
79//!     configure, task, Boson, ExecutionContext, JsonExecutionContextFactory, MemQueueBackend,
80//! };
81//!
82//! #[task(name = "greet")]
83//! async fn greet(ctx: Box<dyn ExecutionContext>, name: String) -> boson_core::Result<()> {
84//!     let _ = (ctx, name);
85//!     Ok(())
86//! }
87//!
88//! # async fn run() -> boson_core::Result<()> {
89//! let boson = Boson::builder()
90//!     .queue_backend(Arc::new(MemQueueBackend::new()))
91//!     .execution_context_factory(JsonExecutionContextFactory)
92//!     .auto_registry()
93//!     .build()?; // background worker loop
94//! configure(boson);
95//!
96//! Greet::send_with(
97//!     serde_json::json!({"System": {"operation": "demo"}}),
98//!     GreetParams { name: "world".into() },
99//! )
100//! .await?;
101//! # Ok(())
102//! # }
103//! # }
104//! ```
105//!
106//! For step-driven tests, use [`BosonBuilder::without_worker`] + [`BosonBuilder::build_manual`]
107//! and [`ManualWorker::try_run_next`] instead of [`BosonBuilder::build`].
108//!
109//! Runnable: `task_macro`, `minimal_enqueue`, `idempotency_and_rate_limit`
110//! (`cargo run -p uf-boson --example <name> --features mem`).
111//! Then continue with [define tasks](#3-define-tasks).
112//!
113//! ## Mode 2 — Remote worker (two binaries)
114//!
115//! Use this when HTTP/API processes should **enqueue only**, and a dedicated worker process (or
116//! many workers) should **claim and run** jobs against **shared** persistence.
117//!
118//! [`MemQueueBackend`] cannot cross process boundaries — Mode 2 needs `SQLite` (shared path),
119//! Postgres, Redis, or NATS.
120//!
121//! ```text
122//! Enqueue binary(ies) ──send_with──► shared QueueBackend ◄──claim── Worker binary(ies)
123//! ```
124//!
125//! ### What you create
126//!
127//! | Piece | Purpose |
128//! |-------|---------|
129//! | Shared task crate (recommended) | Same `#[task]` handlers + inventory on the worker |
130//! | Enqueue binary | Boots Boson **without** a worker loop; [`auto_registry`](BosonBuilder::auto_registry) so
131//!   descriptors exist for `send_with`; calls [`configure`] + `send_with` |
132//! | Worker binary | Same backend URL/path; [`auto_registry`](BosonBuilder::auto_registry); unique
133//!   [`worker_id`](BosonBuilder::worker_id); **`lease_ttl_secs > 0`**; [`build`](BosonBuilder::build) |
134//! | Shared backend | `SQLite` path, Postgres URL, or Redis/NATS fleet |
135//!
136//! ### Enqueue binary
137//!
138//! This process must **not** spawn the drain loop. Use [`BosonBuilder::without_worker`] then
139//! [`BosonBuilder::build`], still call [`BosonBuilder::auto_registry`] (enqueue looks up task
140//! descriptors for priority/pool/policies), install with [`configure`], and enqueue.
141//!
142//! **Pick a shared backend** (each link has a Mode 2 enqueue-binary example):
143//!
144//! | Backend | Feature / crate | Mode 2 enqueue example |
145//! |---------|-----------------|------------------------|
146//! | `SQLite` | `sqlite` | [`SqliteQueueBackend` — enqueue](../boson_backend_sqlite/index.html#mode-2--enqueue-binary) |
147//! | Postgres | `postgres` | [`PostgresQueueBackend` — enqueue](../boson_backend_postgres/index.html#mode-2--enqueue-binary) |
148//! | Redis | [`boson-backend-redis`](https://docs.rs/boson-backend-redis) | [Redis — enqueue](../boson_backend_redis/index.html#mode-2--enqueue-binary) |
149//! | NATS | [`boson-backend-nats`](https://docs.rs/boson-backend-nats) | [NATS — enqueue](../boson_backend_nats/index.html#mode-2--enqueue-binary) |
150//!
151//! `SQLite` sketch (same pattern on every backend page above):
152//!
153//! ```rust,no_run
154//! # #[cfg(feature = "sqlite")]
155//! # {
156//! use std::sync::Arc;
157//!
158//! use boson::{
159//!     configure, Boson, JsonExecutionContextFactory, SqliteQueueBackend,
160//! };
161//!
162//! # async fn boot_enqueue() -> boson_core::Result<()> {
163//! let path = std::env::var("BOSON_SQLITE_PATH").unwrap_or_else(|_| "/tmp/boson-remote.db".into());
164//! let backend = SqliteQueueBackend::new(&path).await?;
165//! let boson = Boson::builder()
166//!     .queue_backend(Arc::new(backend))
167//!     .execution_context_factory(JsonExecutionContextFactory)
168//!     .auto_registry() // descriptors for send_with — no claim loop
169//!     .without_worker()
170//!     .build()?;
171//! configure(boson);
172//! // Greet::send_with(...).await?;  // same API as Mode 1
173//! # Ok(())
174//! # }
175//! # }
176//! ```
177//!
178//! API detail: [`BosonBuilder::without_worker`], [`BosonBuilder::auto_registry`], [`configure`].
179//!
180//! ### Worker binary
181//!
182//! A **different** binary owns the drain loop. Link every crate that defines `#[task]` handlers
183//! (`use my_tasks as _;`) so inventory discovery works.
184//!
185//! **Pick the same shared backend** (each link has a Mode 2 worker-binary example):
186//!
187//! | Backend | Feature / crate | Mode 2 worker example |
188//! |---------|-----------------|------------------------|
189//! | `SQLite` | `sqlite` | [`SqliteQueueBackend` — worker](../boson_backend_sqlite/index.html#mode-2--worker-binary) |
190//! | Postgres | `postgres` | [`PostgresQueueBackend` — worker](../boson_backend_postgres/index.html#mode-2--worker-binary) |
191//! | Redis | [`boson-backend-redis`](https://docs.rs/boson-backend-redis) | [Redis — worker](../boson_backend_redis/index.html#mode-2--worker-binary) |
192//! | NATS | [`boson-backend-nats`](https://docs.rs/boson-backend-nats) | [NATS — worker](../boson_backend_nats/index.html#mode-2--worker-binary) |
193//!
194//! `SQLite` sketch (same pattern on every backend page above):
195//!
196//! ```rust,no_run
197//! # #[cfg(feature = "sqlite")]
198//! # {
199//! use std::sync::Arc;
200//!
201//! use boson::{Boson, JsonExecutionContextFactory, SqliteQueueBackend};
202//!
203//! # async fn boot_worker() -> boson_core::Result<()> {
204//! let path = std::env::var("BOSON_SQLITE_PATH").unwrap_or_else(|_| "/tmp/boson-remote.db".into());
205//! let backend = SqliteQueueBackend::new(&path).await?;
206//! let _boson = Boson::builder()
207//!     .queue_backend(Arc::new(backend))
208//!     .execution_context_factory(JsonExecutionContextFactory)
209//!     .worker_id(std::env::var("BOSON_WORKER_ID").unwrap_or_else(|_| "worker-1".into()))
210//!     .lease_ttl_secs(30) // required when multiple processes share the backend
211//!     .auto_registry()
212//!     .build()?; // background claim + dispatch loop
213//! // keep the process alive (await shutdown, serve health, …)
214//! # Ok(())
215//! # }
216//! # }
217//! ```
218//!
219//! API detail: [`BosonBuilder::worker_id`], [`BosonBuilder::lease_ttl_secs`], [`WorkerSettings`],
220//! [`BosonBuilder::auto_registry`].
221//!
222//! ### Run both
223//!
224//! 1. Start the **worker** (so claims are ready).
225//! 2. Start one or more **enqueue** hosts.
226//! 3. Each worker process needs a unique `BOSON_WORKER_ID` (or builder
227//!    [`worker_id`](BosonBuilder::worker_id)) and a positive lease TTL (`BOSON_LEASE_TTL_SECS` or
228//!    [`lease_ttl_secs`](BosonBuilder::lease_ttl_secs)).
229//!
230//! Runnable: `remote_worker`, `remote_enqueue`
231//!
232//! ```bash
233//! export BOSON_SQLITE_PATH=/tmp/boson-remote.db
234//! cargo run -p uf-boson --example remote_worker --features sqlite &
235//! cargo run -p uf-boson --example remote_enqueue --features sqlite
236//! ```
237//!
238//! ## 3. Define tasks
239//!
240//! When the worker is already booted (Mode 1) or the worker binary discovers inventory (Mode 2),
241//! adding a handler is the macro plus an enqueue call:
242//!
243//! ```rust,no_run
244//! use boson::{task, ExecutionContext};
245//!
246//! #[task(name = "notify")]
247//! async fn notify(ctx: Box<dyn ExecutionContext>, message: String) -> boson_core::Result<()> {
248//!     let _ = (ctx, message);
249//!     Ok(())
250//! }
251//!
252//! # async fn enqueue() -> boson_core::Result<()> {
253//! Notify::send_with(
254//!     serde_json::json!({"System": {"operation": "notify"}}),
255//!     NotifyParams { message: "hello".into() },
256//! )
257//! .await?;
258//! # Ok(())
259//! # }
260//! ```
261//!
262//! Policy attributes (`priority`, `pool`, `max_attempts`, …) are documented on [`task`].
263//! Persisted overrides use [`TaskConfig`].
264//!
265//! ## 4. Choose persistence
266//!
267//! Pick from the [Mode 1 backend table](#mode-1--embedded-one-binary). Connect examples live on
268//! each backend type. Fleet Redis/NATS: see those crate docs for `connect_fleet_from_env` and
269//! URL precedence (`BOSON_*_POOL_ROUTING` → `BOSON_*_URLS`).
270//!
271//! Custom adapters implement [`QueueBackend`] (start from [`MemQueueBackend`] or the trait’s
272//! **How to implement** section).
273//!
274//! ## 5. Mount HTTP admin (optional)
275//!
276//! With feature `axum`, nest [`boson_router`] at [`NEST_PATH`] (`/api/boson`) using [`BosonState`]:
277//!
278//! Runnable: `cargo run -p uf-boson --example axum_admin --features mem,axum`
279//! (`BOSON_EXAMPLE_SERVE=1` to listen).
280//!
281//! ## Prerequisites and gotchas
282//!
283//! - Enable the backend feature (or fleet crate) that matches your topology — `mem` is Mode 1 only.
284//! - Mode 2 workers need **`lease_ttl_secs > 0`** and unique [`worker_id`](BosonBuilder::worker_id) values.
285//! - Worker binaries must **link** every crate that submits `#[task]` inventory.
286//! - [`configure`] is required in any process that calls macro `send_with` (including enqueue-only hosts).
287//!
288//! ## Configuration precedence
289//!
290//! | Layer | Resolution order |
291//! |-------|------------------|
292//! | Worker settings | [`BosonBuilder`] field → environment variable → hardcoded default |
293//! | Task config at enqueue | Persisted backend config → macro/descriptor defaults |
294//! | Idempotency mode | Per-task override → [`BosonBuilder::idempotency_mode`] (default lease-backed) |
295//! | Queue backend | Explicit [`BosonBuilder::queue_backend`] → global router |
296//! | Ops log | [`BosonBuilder::ops_log`] → [`NoOpsLog`]; or [`ops_log_from_env`] separately |
297//! | Fleet URLs (Redis/NATS) | `BOSON_*_POOL_ROUTING` → `BOSON_*_URLS` |
298//!
299//! See [`WorkerSettings`] and [`TaskConfig`] for field-level defaults.
300//!
301//! ## Runnable examples
302//!
303//! | Example | Topology | Features |
304//! |---------|----------|----------|
305//! | `task_macro` | Mode 1 (manual drain) | `mem` |
306//! | `minimal_enqueue` | Mode 1 | `mem` |
307//! | `idempotency_and_rate_limit` | Mode 1 | `mem` |
308//! | `axum_admin` | Mode 1 + HTTP | `mem,axum` |
309//! | `remote_worker` | Mode 2 worker | `sqlite` |
310//! | `remote_enqueue` | Mode 2 enqueue | `sqlite` |
311//!
312//! ```bash
313//! cargo run -p uf-boson --example task_macro --features mem
314//! ```
315
316pub mod prelude;
317
318pub use boson_core::{
319    default_backend_from_global, BosonError, ExecutionContext, ExecutionContextFactory,
320    IdentityError, Job, JobEnqueueDisposition, JobStatus, JsonExecutionContextFactory,
321    QueueBackend, QueueRouter, RateLimitPolicy, RetryPolicy, Run, RunStatus, TaskConfig,
322    TaskRunStats,
323};
324/// Background task handler — typed params, `send_with` enqueue, and link-time registration.
325///
326/// # Example
327///
328/// Assumes the worker (or enqueue host) is already booted and [`configure`]d. For topology
329/// choice see [Mode 1](crate#mode-1--embedded-one-binary) and
330/// [Mode 2](crate#mode-2--remote-worker-two-binaries).
331///
332/// ```rust,no_run
333/// use boson::{task, ExecutionContext};
334///
335/// #[task(name = "notify")]
336/// async fn notify(
337///     ctx: Box<dyn ExecutionContext>,
338///     message: String,
339/// ) -> boson_core::Result<()> {
340///     let _ = (ctx, message);
341///     Ok(())
342/// }
343///
344/// # async fn enqueue() -> boson_core::Result<()> {
345/// Notify::send_with(
346///     serde_json::json!({"System": {"operation": "notify"}}),
347///     NotifyParams { message: "hello".into() },
348/// )
349/// .await?;
350/// # Ok(())
351/// # }
352/// ```
353///
354/// # Contract
355///
356/// - Function must be `async`.
357/// - First parameter must be `Box<dyn ExecutionContext>`.
358/// - Return type must be `Result<()>` (typically `boson_core::Result<()>`).
359/// - `name = "..."` is required and must be the first attribute.
360///
361/// # Policy attributes
362///
363/// Optional: `priority`, `pool`, `max_attempts`, `base_delay_ms`, `backoff_multiplier`,
364/// `max_delay_ms`, `max_in_flight`, `max_enqueue_per_second`. Defaults and meanings are documented
365/// on [`boson_macros`](https://docs.rs/boson-macros).
366pub use boson_macros::task;
367pub use boson_runtime::{
368    configure, default, Boson, BosonBuilder, InvokeFn, ManualWorker, TaskDescriptor, TaskRegistry,
369    WorkerSettings,
370};
371pub use boson_telemetry::{install_ops_log, ops_log, ops_log_from_env, ConsoleOpsLog, NoOpsLog, OpsLog};
372
373#[cfg(feature = "mem")]
374pub use boson_backend_mem::{install_default_mem_backend, MemQueueBackend};
375
376#[cfg(feature = "sqlite")]
377pub use boson_backend_sqlite::{install_default_sqlite_backend, SqliteQueueBackend};
378
379#[cfg(feature = "postgres")]
380pub use boson_backend_postgres::{
381    install_default_postgres_backend, install_isolated_postgres_backend, postgres_test_url,
382    PostgresQueueBackend,
383};
384
385#[cfg(feature = "axum")]
386pub use boson_axum::{boson_router, BosonState, NEST_PATH};