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}