chronon_backend_postgres/lib.rs
1//! `PostgreSQL` [`SchedulerStore`](chronon_core::store::SchedulerStore) for Chronon.
2//!
3//! **When to use:** shared durable storage for **Mode 1** or **Mode 2** coordinator–worker
4//! clusters. For higher claim throughput, wrap with `PostgresRedisSchedulerStore`
5//! (`chronon-backend-redis`).
6//!
7//! Getting started:
8//! [Mode 1](https://docs.rs/uf-chronon/latest/chronon/index.html#mode-1--embedded-one-binary) /
9//! [Mode 2](https://docs.rs/uf-chronon/latest/chronon/index.html#mode-2--coordinator--worker-two-binaries).
10//!
11//! ## Stack position
12//!
13//! ```text
14//! chronon (facade, `postgres` feature) → chronon-backend-postgres → chronon-backend-sql-common → chronon-core
15//! ```
16//!
17//! ## Entry points
18//!
19//! - [`PostgresSchedulerStore::connect`] — open a pool and bootstrap schema
20//! - [`PostgresSchedulerStore::connect_isolated`] — isolated schema for parallel tests
21//! - [`postgres_test_url`] — resolve test URL from `CHRONON_POSTGRES_URL` / `CHRONON_TEST_POSTGRES_URL`
22//!
23//! ## Mode 1 — Embedded
24//!
25//! ```ignore
26//! use std::sync::Arc;
27//! use chronon::prelude::*;
28//! use chronon::PostgresSchedulerStore;
29//!
30//! let url = std::env::var("CHRONON_POSTGRES_URL")?;
31//! let store = PostgresSchedulerStore::connect(&url).await?;
32//! let chronon = ChrononBuilder::new()
33//! .scheduler_store(Arc::new(store))
34//! .context_factory(Arc::new(JsonScriptContextFactory))
35//! .embedded()
36//! .auto_registry()
37//! .build()?;
38//! ```
39//!
40//! Runnable: `cargo run -p uf-chronon --example postgres_boot --features postgres`
41//!
42//! ## Mode 2 — Coordinator binary
43//!
44//! Shared `CHRONON_POSTGRES_URL` with workers. Tick only:
45//!
46//! ```ignore
47//! use std::sync::Arc;
48//! use chronon::prelude::*;
49//! use chronon::PostgresSchedulerStore;
50//!
51//! let url = std::env::var("CHRONON_POSTGRES_URL")?;
52//! let store = PostgresSchedulerStore::connect(&url).await?;
53//! let mut chronon = ChrononBuilder::new()
54//! .scheduler_store(Arc::new(store))
55//! .context_factory(Arc::new(JsonScriptContextFactory))
56//! .instance_id("coordinator-0")
57//! .coordinator_only()
58//! .build()?;
59//! chronon.scheduler.init_partitions().await;
60//! chronon.run().await?;
61//! ```
62//!
63//! ## Mode 2 — Worker binary
64//!
65//! Same Postgres URL, unique `CHRONON_INSTANCE_ID`, scripts via `.auto_registry()`:
66//!
67//! ```ignore
68//! use std::sync::Arc;
69//! use chronon::prelude::*;
70//! use chronon::PostgresSchedulerStore;
71//!
72//! let url = std::env::var("CHRONON_POSTGRES_URL")?;
73//! let store = PostgresSchedulerStore::connect(&url).await?;
74//! let mut chronon = ChrononBuilder::new()
75//! .scheduler_store(Arc::new(store))
76//! .context_factory(Arc::new(JsonScriptContextFactory))
77//! .instance_id(std::env::var("CHRONON_INSTANCE_ID").unwrap_or_else(|_| "worker-1".into()))
78//! .auto_registry()
79//! .worker("general")
80//! .build()?;
81//! chronon.run().await?;
82//! ```
83//!
84//! Production claim path: [Postgres + Redis](../chronon_backend_redis/index.html#mode-2--coordinator-binary).
85
86mod bootstrap;
87
88use chronon_backend_sql_common::SqlSchedulerStore;
89use chronon_core::Result;
90use sqlx::PgPool;
91
92pub use bootstrap::{postgres_store_from_env, postgres_test_url};
93
94/// PostgreSQL-backed scheduler store.
95///
96/// Shared durable storage for Mode 2 coordinator–worker clusters (and Mode 1 when you already
97/// run Postgres). Pass a connection URL to [`Self::connect`]; daemons often use
98/// `CHRONON_POSTGRES_URL` / [`postgres_test_url`].
99///
100/// Mode 2 examples: [coordinator](index.html#mode-2--coordinator-binary) /
101/// [worker](index.html#mode-2--worker-binary).
102///
103/// For higher claim throughput, wrap with `PostgresRedisSchedulerStore` from
104/// `chronon-backend-redis` (`postgres` + `redis` features). Enable the facade `postgres`
105/// feature to re-export this type.
106///
107/// # Examples
108///
109/// ```rust,no_run
110/// use chronon_backend_postgres::PostgresSchedulerStore;
111///
112/// # async fn example() -> chronon_core::Result<()> {
113/// let store = PostgresSchedulerStore::connect(
114/// "postgres://user:pass@localhost/chronon",
115/// )
116/// .await?;
117/// # let _ = store;
118/// # Ok(())
119/// # }
120/// ```
121///
122/// Runnable: `cargo run -p uf-chronon --example postgres_boot --features postgres`.
123pub struct PostgresSchedulerStore {
124 inner: SqlSchedulerStore,
125}
126
127impl PostgresSchedulerStore {
128 /// Connect using a `PostgreSQL` connection URL.
129 ///
130 /// # Examples
131 ///
132 /// ```rust,no_run
133 /// use chronon_backend_postgres::PostgresSchedulerStore;
134 ///
135 /// # async fn example() -> chronon_core::Result<()> {
136 /// let store = PostgresSchedulerStore::connect(
137 /// "postgres://user:pass@localhost/chronon",
138 /// )
139 /// .await?;
140 /// # Ok(())
141 /// # }
142 /// ```
143 ///
144 /// # Errors
145 ///
146 /// Returns a storage error when the pool cannot connect or schema bootstrap fails.
147 pub async fn connect(url: &str) -> Result<Self> {
148 let inner = SqlSchedulerStore::connect_postgres(url).await?;
149 Ok(Self { inner })
150 }
151
152 /// Connect with an isolated schema (for parallel tests).
153 ///
154 /// # Examples
155 ///
156 /// ```rust,no_run
157 /// use chronon_backend_postgres::PostgresSchedulerStore;
158 ///
159 /// # async fn example() -> chronon_core::Result<()> {
160 /// let store = PostgresSchedulerStore::connect_isolated(
161 /// "postgres://user:pass@localhost/chronon",
162 /// "chronon_test_schema",
163 /// )
164 /// .await?;
165 /// # Ok(())
166 /// # }
167 /// ```
168 ///
169 /// # Errors
170 ///
171 /// Returns a storage error when schema creation, pool connect, or bootstrap fails.
172 pub async fn connect_isolated(url: &str, schema: &str) -> Result<Self> {
173 let inner = SqlSchedulerStore::connect_postgres_isolated(url, schema).await?;
174 Ok(Self { inner })
175 }
176
177 /// Attach to an existing isolated schema (no DDL bootstrap; for multi-process workers).
178 ///
179 /// # Errors
180 ///
181 /// Returns a storage error when the pool cannot be opened.
182 pub async fn attach_isolated(url: &str, schema: &str) -> Result<Self> {
183 let inner = SqlSchedulerStore::attach_postgres_isolated(url, schema).await?;
184 Ok(Self { inner })
185 }
186
187 /// Drop an isolated bench/test schema (multibench cell reset).
188 ///
189 /// # Errors
190 ///
191 /// Returns a storage error when the admin connection or DDL fails.
192 pub async fn drop_isolated_schema(url: &str, schema: &str) -> Result<()> {
193 SqlSchedulerStore::drop_postgres_schema(url, schema).await
194 }
195
196 /// Wrap an existing pool (schema bootstrap runs).
197 ///
198 /// # Examples
199 ///
200 /// ```rust,no_run
201 /// use chronon_backend_postgres::PostgresSchedulerStore;
202 /// use sqlx::postgres::PgPoolOptions;
203 ///
204 /// # async fn example() -> chronon_core::Result<()> {
205 /// let pool = PgPoolOptions::new()
206 /// .connect("postgres://localhost/chronon")
207 /// .await
208 /// .expect("pool");
209 /// let store = PostgresSchedulerStore::from_pool(pool).await?;
210 /// # Ok(())
211 /// # }
212 /// ```
213 ///
214 /// # Errors
215 ///
216 /// Returns a storage error when schema bootstrap fails.
217 pub async fn from_pool(pool: PgPool) -> Result<Self> {
218 let inner = SqlSchedulerStore::from_postgres_pool(pool).await?;
219 Ok(Self { inner })
220 }
221
222 /// Underlying connection pool.
223 ///
224 /// # Panics
225 ///
226 /// Panics if the inner pool is not `PostgreSQL` (internal invariant violation).
227 #[must_use]
228 pub fn pool(&self) -> &PgPool {
229 match self.inner.pool() {
230 chronon_backend_sql_common::SqlPool::Postgres(pool) => pool,
231 chronon_backend_sql_common::SqlPool::Sqlite(_) => {
232 panic!("postgres backend has non-postgres pool")
233 }
234 }
235 }
236}
237
238impl std::fmt::Debug for PostgresSchedulerStore {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 f.debug_struct("PostgresSchedulerStore")
241 .finish_non_exhaustive()
242 }
243}
244
245chronon_backend_sql_common::delegate_scheduler_store!(PostgresSchedulerStore, inner);