1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! Durable background jobs on PostgreSQL, SQLite, or MySQL.
//!
//! One queue over the application's existing pool -- no second pool, no
//! separate broker. The queue is at-least-once; claims are fenced by a
//! per-claim UUID token so a stale worker (lease expired, sweep requeued)
//! cannot commit its result over another worker's claim.
//!
//! # Which dialect
//!
//! The build speaks exactly one dialect, chosen by the `db-postgres`,
//! `db-sqlite`, and `db-mysql` features. Everything dialect-specific -- the
//! statement text, the placeholder style, how a timestamp is stored, and the
//! shape of the claim -- is confined to the private `dialect` module. The
//! claim comes in two shapes because the databases genuinely differ:
//!
//! - **PostgreSQL** claims in one statement: `UPDATE ... RETURNING` over a
//! `FOR UPDATE SKIP LOCKED` subquery.
//! - **MySQL 8** has `SKIP LOCKED` but no `RETURNING`, so it picks with a
//! locking `SELECT` and marks each picked row inside one transaction.
//! - **SQLite** has neither, and needs neither: `BEGIN IMMEDIATE` takes the
//! database write lock, so a claim is exclusive by construction and
//! competing claimers wait (bounded by `busy_timeout`) rather than skip.
//! The cost is that SQLite claimers serialise, which is the right trade for
//! the single-node use SQLite is chosen for.
//!
//! Lease arithmetic is done by the database in every dialect, so recovering a
//! crashed worker's jobs does not depend on worker clocks agreeing.
//!
//! # Architecture
//!
//! - [`Jobs`] is the enqueue facade (one pool, no second connection).
//! - [`Worker`] claims and runs jobs. Handlers are closures registered via
//! [`Registry::add`].
//! - [`Scheduler`] enqueues recurring jobs on a cadence; the worker runs them.
//! - [`RetryPolicy`] is exponential backoff with jitter and a cap.
//! - [`Observer`] is the observability seam (default: no-op).
//!
//! # Example
//!
//! ```no_run
//! use arcature::jobs::{JobError, JobModel, JobPool, JobRequest, Jobs, Registry, Worker};
//!
//! #[derive(serde::Serialize, serde::Deserialize)]
//! struct SendWelcome {
//! email: String,
//! }
//!
//! // What `#[job]` generates: one const descriptor, kind and version pinned
//! // at compile time rather than looked up in a registry at run time.
//! const SEND_WELCOME: JobModel<SendWelcome> = JobModel::new("send_welcome", 1, 3);
//!
//! async fn boot(pool: JobPool) -> Result<(), Box<dyn std::error::Error>> {
//! let mut registry = Registry::new();
//! registry.add(&SEND_WELCOME, |job: SendWelcome| async move {
//! println!("welcome {}", job.email);
//! Ok::<(), JobError>(())
//! })?;
//!
//! // One pool for both sides: enqueueing opens no second connection.
//! let worker = Worker::new(pool.clone(), registry);
//! let jobs = Jobs::new(pool);
//!
//! let payload = SendWelcome {
//! email: "a@b.com".into(),
//! };
//! jobs.enqueue(&JobRequest::new(&SEND_WELCOME, &payload)?).await?;
//!
//! let _ = worker;
//! Ok(())
//! }
//! ```
// The fixture the live-database tests share. Gated on `test-kit` because it
// reuses that module's safety check rather than keeping a second copy of it;
// see the module comment.
pub use ClaimedJob;
pub use ;
pub use JobPool;
pub use ;
pub use ;
pub use Jobs;
pub use ;
pub use Registry;
pub use ;
pub use ;
// Re-export the certified sqlx so downstream code targets the pinned version
// through Arcature (e.g. `arcature::jobs::sqlx`).
pub use sqlx;
use Serialize;
// ---------------------------------------------------------------------------
// Job trait — the marker trait for typed jobs.
// ---------------------------------------------------------------------------
/// The marker trait for typed Arcature jobs.
///
/// A job type must have a static [`NAME`](crate::DxComponent::NAME) and must
/// be `Serialize + DeserializeOwned` (the user adds
/// `#[derive(Serialize, Deserialize)]`). The `#[job]` macro generates
/// `impl DxComponent` (with `NAME = stringify!(StructName)`) and the empty
/// `impl Job`.