cja/lib.rs
1#![doc(html_favicon_url = "https://cja.app/docs/favicon.svg")]
2//! # CJA — Cron, Jobs and Axum
3//!
4//! > **ALPHA**: CJA is under active development. APIs may change between releases.
5//!
6//! A Rust web framework that combines background job processing, cron scheduling,
7//! and an HTTP server built on [Axum](https://docs.rs/axum).
8//!
9//! ## Overview
10//!
11//! CJA provides three integrated subsystems:
12//!
13//! - **[`jobs`]** — PostgreSQL-backed background job queue with automatic retries,
14//! exponential backoff, dead letter queue, and graceful shutdown
15//! - **[`cron`]** — Scheduled task execution with both interval-based and cron expression
16//! scheduling, timezone support
17//! - **[`server`]** — HTTP server on Axum with encrypted cookie management, database-backed
18//! sessions, and zero-downtime reload via systemfd
19//!
20//! All three share a single [`app_state::AppState`] and coordinate shutdown via
21//! [`tokio_util::sync::CancellationToken`].
22//!
23//! ## Getting Started
24//!
25//! Add CJA to your project:
26//!
27//! ```bash
28//! cargo add cja
29//! ```
30//!
31//! ### 1. Implement `AppState`
32//!
33//! ```rust
34//! use cja::app_state::AppState;
35//! use cja::server::cookies::CookieKey;
36//!
37//! #[derive(Clone)]
38//! struct MyAppState {
39//! db: sqlx::PgPool,
40//! cookie_key: CookieKey,
41//! }
42//!
43//! impl AppState for MyAppState {
44//! fn version(&self) -> &str { "1.0.0" }
45//! fn db(&self) -> &sqlx::PgPool { &self.db }
46//! fn cookie_key(&self) -> &CookieKey { &self.cookie_key }
47//! }
48//! ```
49//!
50//! [`app_state::AppState`] is cloned per request — wrap expensive resources in `Arc`.
51//!
52//! ### 2. Set Up the Database
53//!
54//! CJA requires `PostgreSQL`. Set `DATABASE_URL` and run migrations on startup:
55//!
56//! ```rust,ignore
57//! let pool = sqlx::postgres::PgPoolOptions::new()
58//! .max_connections(5)
59//! .connect(&std::env::var("DATABASE_URL")?)
60//! .await?;
61//!
62//! // Run CJA's built-in migrations (jobs, crons, sessions, dead_letter_jobs)
63//! cja::db::run_migrations(&pool).await?;
64//! ```
65//!
66//! ### 3. Wire Up `main()`
67//!
68//! ```rust,ignore
69//! use cja::setup::{setup_sentry, setup_tracing};
70//! use cja::tasks::{ShutdownBudget, Supervisor};
71//!
72//! fn main() -> cja::Result<()> {
73//! let _sentry_guard = setup_sentry();
74//! tokio::runtime::Builder::new_multi_thread()
75//! .enable_all()
76//! .build()?
77//! .block_on(run())
78//! }
79//!
80//! async fn run() -> cja::Result<()> {
81//! // Keep the Eyes handle alive for graceful shutdown
82//! let _eyes_handle = setup_tracing("my-app")?;
83//!
84//! let app_state = MyAppState { /* ... */ };
85//!
86//! // Registers SIGTERM/SIGINT and owns the shutdown sequence
87//! let mut supervisor = Supervisor::new(ShutdownBudget::from_env())?;
88//! let shutdown = supervisor.shutdown_token();
89//!
90//! // HTTP server: stops accepting connections when shutdown starts
91//! supervisor.spawn("server",
92//! cja::server::run_server_until(
93//! routes(app_state.clone()),
94//! shutdown.clone().cancelled_owned(),
95//! )
96//! );
97//!
98//! // Job worker: stops claiming jobs, drains the in-flight one, then
99//! // releases its lock so another instance picks it up immediately
100//! supervisor.spawn("jobs",
101//! cja::jobs::worker::job_worker(
102//! app_state.clone(),
103//! Jobs, // your job registry (see jobs module docs)
104//! std::time::Duration::from_secs(60),
105//! cja::jobs::DEFAULT_MAX_RETRIES,
106//! shutdown.clone(),
107//! Default::default(),
108//! )
109//! );
110//!
111//! // Returns Ok after a signal, Err if a task died on its own
112//! supervisor.run().await
113//! }
114//! ```
115//!
116//! See `crates/cja.app/` in the repository for a complete working example.
117//!
118//! ## Configuration
119//!
120//! | Variable | Required | Default | Description |
121//! |----------|----------|---------|-------------|
122//! | `DATABASE_URL` | Yes | — | `PostgreSQL` connection string |
123//! | `PORT` | No | `3000` | HTTP server port |
124//! | `COOKIE_KEY` | No | Auto-generated | Base64-encoded 64-byte cookie encryption key. **Must be set in production** for session persistence across restarts. |
125//! | `SERVER_DISABLED` | No | `false` | Set to `"true"` to skip starting the HTTP server |
126//! | `JOBS_DISABLED` | No | `false` | Set to `"true"` to skip starting the job worker |
127//! | `CRON_DISABLED` | No | `false` | Set to `"true"` to skip starting the cron worker |
128//! | `RUST_LOG` | No | `info` | Tracing filter directive |
129//! | `CJA_SHUTDOWN_JOB_DRAIN_SECS` | No | `2` | How long an in-flight job may run after a shutdown signal before it is dropped and unlocked |
130//! | `CJA_SHUTDOWN_EXIT_GRACE_SECS` | No | `2` | Extra time for tasks to exit after the job drain. Drain + grace must fit the platform's kill window (Cloud Run 10s fixed, Fly 5s default) |
131//! | `JSON_LOGS` | No | — | If set, outputs JSON logs instead of tree format |
132//! | `SENTRY_DSN` | No | — | Enables Sentry error tracking |
133//! | `HONEYCOMB_API_KEY` | No | — | Enables OpenTelemetry export to Honeycomb |
134//! | `EYES_ORG_ID` | No | — | Eyes tracing org ID (both `ORG_ID` and `APP_ID` required) |
135//! | `EYES_APP_ID` | No | — | Eyes tracing app ID |
136//! | `EYES_URL` | No | `https://eyes.coreyja.com` | Eyes server URL |
137//!
138//! ## Feature Flags
139//!
140//! ```toml
141//! [features]
142//! default = ["cron", "jobs"]
143//! cron = ["jobs"] # cron depends on jobs
144//! jobs = []
145//! testing = [] # enables mock OAuth for tests
146//! ```
147//!
148//! Disabling `jobs` at compile time also disables `cron`.
149//!
150//! ## Re-exports
151//!
152//! CJA re-exports key dependencies so you don't need to add them directly:
153//!
154//! ```rust
155//! use cja::eyes_subscriber;
156//! use cja::sqlx;
157//! use cja::uuid;
158//! use cja::color_eyre;
159//! use cja::maud;
160//! use cja::chrono;
161//! use cja::chrono_tz;
162//! ```
163
164pub use eyes_subscriber;
165pub mod assets;
166pub use sqlx;
167pub use uuid;
168
169#[cfg(feature = "cron")]
170pub mod cron;
171#[cfg(feature = "jobs")]
172pub mod eyes_manifest;
173#[cfg(feature = "jobs")]
174pub mod jobs;
175pub mod server;
176#[cfg(feature = "testing")]
177pub mod testing;
178
179pub mod app_state;
180pub mod setup;
181
182pub use color_eyre;
183pub use color_eyre::Result;
184
185pub use maud;
186
187pub mod db;
188pub mod tasks;
189
190pub use chrono;
191pub use chrono_tz;