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 **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);