Skip to main content

chronon_backend_postgres/
lib.rs

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