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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! Sidekiq-style job queue with embedded `SQLite` and pluggable Postgres.
//!
//! Domain crate — host-agnostic. Consumers register handlers, enqueue jobs,
//! and the runtime claims/runs/finalizes them across N worker tasks. The
//! same code path runs single-process on `SQLite` (local desktop) or
//! multi-replica on Postgres (deployed service).
//!
//! ## What it gives you
//!
//! - Backend-agnostic [`Storage`] traits (`JobQueue`, `ProcessRegistry`,
//! `QueueConfig`, `CronStorage`, `RateLimitStorage`) — one row of
//! indirection between the runtime and the database
//! - Per-queue worker pools, cooperative shutdown, stale-heartbeat reaper
//! - Cron schedules with lease-elected leader (only one replica fires
//! each tick); same lease gates the retention sweep and metrics roller
//! - Per-queue exponential backoff with a configurable on/off toggle; the
//! `Failed` and `Throttled` arms both respect it
//! - Cancellation that survives across replicas: [`QueueHandle::request_cancel`]
//! short-circuits in-process; cross-pod cancels flow through a DB flag
//! the heartbeat tick observes
//! - Cluster-wide rate-limit budget — handlers `acquire("slack")` /
//! `acquire("gh")` against a server-side token bucket; one upstream
//! 429 drains the bucket so sibling pods don't each fire their own
//!
//! ## Minimal consumer
//!
//! ```ignore
//! use std::sync::Arc;
//! use forge_jobs::storage::{DatabaseConfig, PathsError, QueuePaths};
//! use forge_jobs::{
//! DefaultRouter, EnqueueRequest, HandlerRegistry, NoopEcho, QueueRuntime,
//! };
//!
//! #[derive(Debug)]
//! struct EnvPaths;
//! impl QueuePaths for EnvPaths {
//! fn config_dir(&self) -> Result<std::path::PathBuf, PathsError> {
//! Ok("./jobs/config".into())
//! }
//! fn data_dir(&self) -> Result<std::path::PathBuf, PathsError> {
//! Ok("./jobs/data".into())
//! }
//! }
//!
//! # async fn run() -> forge_jobs::storage::Result<()> {
//! let paths = EnvPaths;
//! let storage = DatabaseConfig::load(&paths)?.open_storage(&paths).await?;
//! let mut handlers = HandlerRegistry::new();
//! handlers.register(NoopEcho);
//! let runtime = QueueRuntime::new(storage, handlers, Arc::new(DefaultRouter))
//! .with_queues(["default".to_owned()]); // required: which queues this worker runs
//! runtime.ensure_queue("default", 2).await?;
//! runtime
//! .enqueue(
//! EnqueueRequest::new(forge_jobs::NOOP_ECHO_KIND, serde_json::json!({}))
//! .on_queue("default"),
//! )
//! .await?;
//! let handle = runtime.start().await?;
//! // ... handle.shutdown_graceful(...) at exit
//! # let _ = handle;
//! # Ok(())
//! # }
//! ```
//!
//! See `examples/minimal.rs` in the crate root for the runnable version.
//!
//! ## Handler cancellation contract
//!
//! Handlers that take longer than ~1s should periodically check
//! `ctx.cancel.is_cancelled()` between `.await` points. A user click on
//! the Mission Control "delete" button fires [`QueueHandle::request_cancel`];
//! the worker's heartbeat picks up the cross-pod variant via the DB
//! `cancel_requested_at` flag within `HEARTBEAT_INTERVAL` (10s). User-
//! cancelled jobs route straight to `Dead` without burning the retry budget.
//!
//! ## Optional features
//!
//! - **`postgres`** — enables the Postgres storage adapter. Off by
//! default; service / k8s deploys flip this on.
//! - **`legacy-scheduler`** — re-exports a smaller cooperative
//! recurring-job `Scheduler` (with `Job`, `JobStore`, `Clock`,
//! `parse_cron`, etc.) that predates the queue subsystem. Used by
//! the originating project's LLM idempotency cache pruning and a
//! few similar internal cron tasks. New code should use
//! [`QueueRuntime`] with a cron schedule instead.
pub use parse_cron;
// Legacy cron `Scheduler` modules. Only compiled when the
// `legacy-scheduler` feature is on. The shared `parse_cron` helper
// the queue's `runtime::cron` needs lives in `cron_expr` (always
// compiled).
pub use ;
pub use ;
// The legacy `JobCtx` is re-exported as `SchedulerJobCtx` so the
// queue's `JobCtx` (the one handler authors actually use) can keep
// the short name.
pub use ;
pub use Scheduler;
pub use ;
// Queue subsystem — runtime + storage trait surface.
//
// Consumers (handlers, host setup, IPC) get everything they need
// from these two re-export groups. Swapping the storage backend
// later (e.g. Redis) only changes which `Storage`-implementing type
// the host constructs in `setup()`; nothing in `runtime::*` or in
// downstream handler crates moves.
pub use ;
pub use PostgresStorage;
pub use ;
/// Format an error with its full `Error::source()` chain as
/// `"top: middle: root"`.
///
/// `Display` only shows the outermost variant — for thiserror enums with
/// `#[from]` that drops the cause entirely (e.g. a `GhError::Octocrab`
/// reads as `"github: <generic>"` instead of `"github: <generic>: 422
/// Validation Failed: field X required"`). Use this when building the
/// `JobOutcome::Failed` / `Dead` message and when logging handler
/// failures so the cause survives into `last_error` and into the CLI.