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