Skip to main content

boson_backend_postgres/
lib.rs

1//! `PostgreSQL` [`QueueBackend`](boson_core::QueueBackend) for Boson.
2//!
3//! **When to use:** shared durable state for embedded or remote-worker topologies (enqueue hosts +
4//! worker binaries against the same database). Enable via the `boson` crate `postgres` feature.
5//!
6//! Remote workers should set a unique `worker_id` and `lease_ttl_secs > 0`. See the
7//! [`boson`](https://docs.rs/uf-boson) crate
8//! [Remote worker](https://docs.rs/uf-boson/latest/boson/index.html#remote-worker-two-binaries).
9//!
10//! ## Entry points
11//!
12//! - [`PostgresQueueBackend::connect`] — open a pool and bootstrap schema
13//! - [`install_default_postgres_backend`] — register on the global [`QueueRouter`](boson_core::QueueRouter)
14//!
15//! ## Remote worker — Enqueue binary
16//!
17//! Shared `DATABASE_URL` with the worker. No claim loop in this process:
18//!
19//! ```rust,ignore
20//! use std::sync::Arc;
21//!
22//! use boson_backend_postgres::PostgresQueueBackend;
23//! use boson_core::JsonExecutionContextFactory;
24//! use boson::{configure, Boson};
25//!
26//! # async fn boot_enqueue() -> boson_core::Result<()> {
27//! let url = std::env::var("DATABASE_URL")
28//!     .unwrap_or_else(|_| "postgres://localhost/boson".into());
29//! let backend = PostgresQueueBackend::connect(&url).await?;
30//! let boson = Boson::builder()
31//!     .queue_backend(Arc::new(backend))
32//!     .execution_context_factory(JsonExecutionContextFactory)
33//!     .auto_registry()
34//!     .without_worker()
35//!     .build()?;
36//! configure(boson);
37//! // MyTask::send_with(...).await?;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! Runnable: `cargo run -p uf-boson --example postgres_enqueue --features postgres`
43//!
44//! ## Remote worker — Worker binary
45//!
46//! Same database URL, unique `worker_id`, and `lease_ttl_secs > 0`:
47//!
48//! ```rust,ignore
49//! use std::sync::Arc;
50//!
51//! use boson_backend_postgres::PostgresQueueBackend;
52//! use boson_core::JsonExecutionContextFactory;
53//! use boson::Boson;
54//!
55//! # async fn boot_worker() -> boson_core::Result<()> {
56//! let url = std::env::var("DATABASE_URL")
57//!     .unwrap_or_else(|_| "postgres://localhost/boson".into());
58//! let backend = PostgresQueueBackend::connect(&url).await?;
59//! let _boson = Boson::builder()
60//!     .queue_backend(Arc::new(backend))
61//!     .execution_context_factory(JsonExecutionContextFactory)
62//!     .worker_id(std::env::var("BOSON_WORKER_ID").unwrap_or_else(|_| "worker-1".into()))
63//!     .lease_ttl_secs(30)
64//!     .auto_registry()
65//!     .build()?;
66//! # Ok(())
67//! # }
68//! ```
69//!
70//! Runnable: `cargo run -p uf-boson --example postgres_worker --features postgres`
71//!
72//! Other remote-worker backends:
73//! [`SQLite`](../boson_backend_sqlite/index.html#remote-worker--enqueue-binary),
74//! [Redis](../boson_backend_redis/index.html#remote-worker--enqueue-binary),
75//! [NATS](../boson_backend_nats/index.html#remote-worker--enqueue-binary).
76
77mod bootstrap;
78
79use boson_backend_sql_common::SqlQueueBackend;
80use boson_core::{BosonError, Result};
81use sqlx::PgPool;
82
83pub use bootstrap::{
84    install_default_postgres_backend, install_isolated_postgres_backend, postgres_test_url,
85};
86
87/// PostgreSQL-backed queue backend.
88///
89/// Remote-worker examples: [enqueue](index.html#remote-worker--enqueue-binary) /
90/// [worker](index.html#remote-worker--worker-binary).
91pub struct PostgresQueueBackend {
92    inner: SqlQueueBackend,
93}
94
95impl PostgresQueueBackend {
96    /// Connect to `PostgreSQL` at `url`.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error when the pool cannot connect or schema bootstrap fails.
101    pub async fn new(url: &str) -> Result<Self> {
102        Self::connect(url).await
103    }
104
105    /// Connect using a `PostgreSQL` connection URL and wire into [`Boson`](https://docs.rs/boson-runtime).
106    ///
107    /// See crate-level [Remote worker — Enqueue binary](index.html#remote-worker--enqueue-binary) and
108    /// [Remote worker — Worker binary](index.html#remote-worker--worker-binary).
109    ///
110    /// # Examples
111    ///
112    /// ```rust,ignore
113    /// use std::sync::Arc;
114    ///
115    /// use boson_backend_postgres::PostgresQueueBackend;
116    /// use boson_core::JsonExecutionContextFactory;
117    /// use boson::Boson;
118    ///
119    /// # async fn connect() -> boson_core::Result<()> {
120    /// let url = std::env::var("DATABASE_URL")
121    ///     .unwrap_or_else(|_| "postgres://localhost/boson".into());
122    /// let backend = PostgresQueueBackend::connect(&url).await?;
123    /// let _boson = Boson::builder()
124    ///     .queue_backend(Arc::new(backend))
125    ///     .execution_context_factory(JsonExecutionContextFactory)
126    ///     .worker_id("worker-1")
127    ///     .lease_ttl_secs(30) // remote-worker multi-process
128    ///     .auto_registry()
129    ///     .build()?;
130    /// # Ok(())
131    /// # }
132    /// ```
133    ///
134    /// # Errors
135    ///
136    /// Returns an error when the pool cannot connect or schema bootstrap fails.
137    pub async fn connect(url: &str) -> Result<Self> {
138        let inner = SqlQueueBackend::connect_postgres(url).await?;
139        Ok(Self { inner })
140    }
141
142    /// Connect with an isolated schema (for parallel tests).
143    ///
144    /// # Errors
145    ///
146    /// Returns an error when schema creation, pool connect, or bootstrap fails.
147    pub async fn connect_isolated(url: &str, schema: &str) -> Result<Self> {
148        let inner = SqlQueueBackend::connect_postgres_isolated(url, schema).await?;
149        Ok(Self { inner })
150    }
151
152    /// Wrap an existing pool (schema bootstrap runs).
153    ///
154    /// # Errors
155    ///
156    /// Returns an error when schema bootstrap fails.
157    pub async fn from_pool(pool: PgPool) -> Result<Self> {
158        let inner = SqlQueueBackend::from_postgres_pool(pool).await?;
159        Ok(Self { inner })
160    }
161
162    /// Underlying connection pool.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`BosonError::Internal`] if the inner pool is not `PostgreSQL`
167    /// (internal invariant violation).
168    pub fn pool(&self) -> Result<&PgPool> {
169        match self.inner.pool() {
170            boson_backend_sql_common::SqlPool::Postgres(pool) => Ok(pool),
171            boson_backend_sql_common::SqlPool::Sqlite(_) => Err(BosonError::internal(
172                "postgres backend has non-postgres pool",
173            )),
174        }
175    }
176}
177
178impl std::fmt::Debug for PostgresQueueBackend {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        f.debug_struct("PostgresQueueBackend")
181            .finish_non_exhaustive()
182    }
183}
184
185boson_backend_sql_common::delegate_queue_backend!(PostgresQueueBackend, inner);