Skip to main content

pg_embedded_setup_unpriv/cluster/
handle.rs

1//! Send-safe handle for accessing a running `PostgreSQL` cluster.
2//!
3//! [`ClusterHandle`] provides thread-safe access to cluster metadata and
4//! connection helpers. Unlike [`TestCluster`](super::TestCluster), handles
5//! implement [`Send`] and [`Sync`], enabling patterns such as:
6//!
7//! - Shared cluster fixtures using [`OnceLock`](std::sync::OnceLock)
8//! - rstest fixtures with timeouts (which require `Send + 'static`)
9//! - Cross-thread sharing in async test patterns
10//!
11//! # Architecture
12//!
13//! The handle/guard split separates concerns:
14//!
15//! - **`ClusterHandle`**: Read-only access to cluster metadata. `Send + Sync`.
16//! - **`ClusterGuard`**: Manages environment and shutdown. `!Send`.
17//!
18//! This separation preserves the safety of thread-local environment management
19//! whilst enabling the most common shared cluster use cases.
20//!
21//! # Examples
22//!
23//! ```no_run
24//! use std::sync::OnceLock;
25//!
26//! use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
27//!
28//! static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
29//!
30//! fn shared_handle() -> &'static ClusterHandle {
31//!     SHARED.get_or_init(|| {
32//!         let (handle, guard) = TestCluster::new_split().expect("cluster bootstrap failed");
33//!         handle
34//!             .register_shutdown_on_exit()
35//!             .expect("shutdown hook registration failed");
36//!         std::mem::forget(guard);
37//!         handle
38//!     })
39//! }
40//! ```
41
42use postgresql_embedded::Settings;
43
44use super::{
45    connection::TestClusterConnection,
46    lifecycle::DatabaseName,
47    temporary_database::TemporaryDatabase,
48};
49use crate::{TestBootstrapEnvironment, TestBootstrapSettings, error::BootstrapResult};
50
51/// Send-safe handle providing read-only access to a running `PostgreSQL` cluster.
52///
53/// Handles are lightweight and cloneable. They contain only the bootstrap
54/// metadata needed to construct connections and query cluster state.
55///
56/// # Thread Safety
57///
58/// `ClusterHandle` implements [`Send`] and [`Sync`], making it safe to share
59/// across threads. The underlying `PostgreSQL` process is an external OS process
60/// that handles concurrent connections safely.
61///
62/// # Obtaining a Handle
63///
64/// Use [`TestCluster::new_split()`](super::TestCluster::new_split) to obtain
65/// a handle and guard pair:
66///
67/// ```no_run
68/// use pg_embedded_setup_unpriv::TestCluster;
69///
70/// let (handle, guard) = TestCluster::new_split()?;
71/// // handle: ClusterHandle (Send + Sync)
72/// // guard: ClusterGuard (!Send, manages lifecycle)
73/// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
74/// ```
75#[derive(Debug, Clone)]
76pub struct ClusterHandle {
77    bootstrap: TestBootstrapSettings,
78}
79
80// Compile-time assertions that ClusterHandle is Send + Sync.
81const _: () = {
82    const fn assert_send<T: Send>() {}
83    const fn assert_sync<T: Sync>() {}
84    assert_send::<ClusterHandle>();
85    assert_sync::<ClusterHandle>();
86};
87
88impl From<TestBootstrapSettings> for ClusterHandle {
89    fn from(bootstrap: TestBootstrapSettings) -> Self { Self { bootstrap } }
90}
91
92impl ClusterHandle {
93    /// Creates a new handle from bootstrap settings.
94    pub(super) const fn new(bootstrap: TestBootstrapSettings) -> Self { Self { bootstrap } }
95
96    /// Returns the prepared `PostgreSQL` settings for the running cluster.
97    ///
98    /// # Examples
99    ///
100    /// ```no_run
101    /// use pg_embedded_setup_unpriv::TestCluster;
102    ///
103    /// let (handle, _guard) = TestCluster::new_split()?;
104    /// let url = handle.settings().url("my_database");
105    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
106    /// ```
107    #[must_use]
108    pub const fn settings(&self) -> &Settings { &self.bootstrap.settings }
109
110    /// Returns the environment required for clients to interact with the cluster.
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// use pg_embedded_setup_unpriv::TestCluster;
116    ///
117    /// let (handle, _guard) = TestCluster::new_split()?;
118    /// let env = handle.environment();
119    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
120    /// ```
121    #[must_use]
122    pub const fn environment(&self) -> &TestBootstrapEnvironment { &self.bootstrap.environment }
123
124    /// Returns the bootstrap metadata captured when the cluster was started.
125    ///
126    /// # Examples
127    ///
128    /// ```no_run
129    /// use pg_embedded_setup_unpriv::TestCluster;
130    ///
131    /// let (handle, _guard) = TestCluster::new_split()?;
132    /// let bootstrap = handle.bootstrap();
133    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
134    /// ```
135    #[must_use]
136    pub const fn bootstrap(&self) -> &TestBootstrapSettings { &self.bootstrap }
137
138    /// Returns helper methods for constructing connection artefacts.
139    ///
140    /// # Examples
141    ///
142    /// ```no_run
143    /// use pg_embedded_setup_unpriv::TestCluster;
144    ///
145    /// let (handle, _guard) = TestCluster::new_split()?;
146    /// let metadata = handle.connection().metadata();
147    /// println!(
148    ///     "postgresql://{}:***@{}:{}/postgres",
149    ///     metadata.superuser(),
150    ///     metadata.host(),
151    ///     metadata.port(),
152    /// );
153    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
154    /// ```
155    #[must_use]
156    pub fn connection(&self) -> TestClusterConnection {
157        TestClusterConnection::new(&self.bootstrap)
158    }
159}
160
161// Delegation methods that forward to TestClusterConnection.
162impl ClusterHandle {
163    /// Creates a new database with the given name.
164    ///
165    /// See [`TestClusterConnection::create_database`] for details.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if the database already exists or if the connection fails.
170    pub fn create_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
171        self.connection().create_database(name)
172    }
173
174    /// Creates a new database by cloning an existing template.
175    ///
176    /// See [`TestClusterConnection::create_database_from_template`] for details.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the target database already exists, the template does
181    /// not exist, or the connection fails.
182    pub fn create_database_from_template(
183        &self,
184        name: impl Into<DatabaseName>,
185        template: impl Into<DatabaseName>,
186    ) -> BootstrapResult<()> {
187        self.connection()
188            .create_database_from_template(name, template)
189    }
190
191    /// Drops an existing database.
192    ///
193    /// See [`TestClusterConnection::drop_database`] for details.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if the database does not exist or the connection fails.
198    pub fn drop_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
199        self.connection().drop_database(name)
200    }
201
202    /// Checks whether a database with the given name exists.
203    ///
204    /// See [`TestClusterConnection::database_exists`] for details.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if the connection fails.
209    pub fn database_exists(&self, name: impl Into<DatabaseName>) -> BootstrapResult<bool> {
210        self.connection().database_exists(name)
211    }
212
213    /// Ensures a template database exists, creating it if necessary.
214    ///
215    /// See [`TestClusterConnection::ensure_template_exists`] for details.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if database creation fails or if `setup_fn` returns an error.
220    pub fn ensure_template_exists<F>(
221        &self,
222        name: impl Into<DatabaseName>,
223        setup_fn: F,
224    ) -> BootstrapResult<()>
225    where
226        F: FnOnce(&str) -> BootstrapResult<()>,
227    {
228        self.connection().ensure_template_exists(name, setup_fn)
229    }
230
231    /// Creates a temporary database that is dropped when the guard is dropped.
232    ///
233    /// See [`TestClusterConnection::temporary_database`] for details.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the database already exists or the connection fails.
238    pub fn temporary_database(
239        &self,
240        name: impl Into<DatabaseName>,
241    ) -> BootstrapResult<TemporaryDatabase> {
242        self.connection().temporary_database(name)
243    }
244
245    /// Creates a temporary database from a template.
246    ///
247    /// See [`TestClusterConnection::temporary_database_from_template`] for details.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the target database already exists, the template does
252    /// not exist, or the connection fails.
253    pub fn temporary_database_from_template(
254        &self,
255        name: impl Into<DatabaseName>,
256        template: impl Into<DatabaseName>,
257    ) -> BootstrapResult<TemporaryDatabase> {
258        self.connection()
259            .temporary_database_from_template(name, template)
260    }
261}
262
263// Process-exit shutdown hook registration.
264impl ClusterHandle {
265    /// Registers a process-exit hook that stops the `PostgreSQL` postmaster
266    /// when the process terminates.
267    ///
268    /// Intended for shared clusters where the [`ClusterGuard`](super::ClusterGuard)
269    /// is intentionally forgotten. The hook requests platform shutdown and
270    /// waits up to the configured shutdown timeout before escalating to the
271    /// platform's forceful termination mechanism.
272    ///
273    /// The method is idempotent: subsequent calls after the first
274    /// successful registration are no-ops. Only one cluster can be
275    /// tracked per process, matching the one-shared-cluster pattern.
276    ///
277    /// # Platform Support
278    ///
279    /// Supported on Unix (Linux, macOS) and Windows. Windows shutdown requests
280    /// terminate the postmaster process tree immediately because the process
281    /// exit hook cannot safely issue a graceful `PostgreSQL` shutdown command. On
282    /// other platforms this method is a silent no-op that returns `Ok(())`, so
283    /// callers need not gate on platform cfgs.
284    ///
285    /// # Errors
286    ///
287    /// Returns an error if `libc::atexit` registration fails on a supported
288    /// platform.
289    ///
290    /// # Examples
291    ///
292    /// ```no_run
293    /// use std::sync::OnceLock;
294    ///
295    /// use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
296    ///
297    /// static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
298    ///
299    /// fn shared_handle() -> &'static ClusterHandle {
300    ///     SHARED.get_or_init(|| {
301    ///         let (handle, guard) = TestCluster::new_split().expect("cluster bootstrap failed");
302    ///         handle
303    ///             .register_shutdown_on_exit()
304    ///             .expect("shutdown hook registration failed");
305    ///         std::mem::forget(guard);
306    ///         handle
307    ///     })
308    /// }
309    /// ```
310    pub fn register_shutdown_on_exit(&self) -> BootstrapResult<()> {
311        self.register_shutdown_on_exit_impl()
312    }
313
314    #[cfg(any(unix, windows))]
315    fn register_shutdown_on_exit_impl(&self) -> BootstrapResult<()> {
316        super::shutdown_hook::register_shutdown_hook(
317            self.bootstrap.settings.clone(),
318            self.bootstrap.shutdown_timeout,
319            self.bootstrap.cleanup_mode,
320        )
321    }
322
323    #[cfg(not(any(unix, windows)))]
324    fn register_shutdown_on_exit_impl(&self) -> BootstrapResult<()> {
325        // No-op on unsupported platforms. Unix and Windows both have concrete
326        // process-exit reapers; other targets can still use normal Drop-based
327        // cleanup.
328        Ok(())
329    }
330}