pg_embedded_setup_unpriv/cluster/guard.rs
1//! Lifecycle guard for a running `PostgreSQL` cluster.
2//!
3//! [`ClusterGuard`] manages the non-`Send` components of a cluster's lifecycle:
4//! environment variable restoration and cluster shutdown. It is intentionally
5//! `!Send` to ensure environment guards are dropped on the thread that created
6//! them.
7//!
8//! # Architecture
9//!
10//! The guard holds:
11//! - **Environment guards**: `ScopedEnv` instances that restore environment
12//! variables when dropped
13//! - **Shutdown resources**: Runtime, `PostgreSQL` instance, and configuration
14//! needed to cleanly stop the cluster
15//! - **Tracing span**: Keeps the cluster's observability span alive
16//!
17//! # Drop Behaviour
18//!
19//! When dropped, the guard:
20//! 1. Stops the `PostgreSQL` cluster (gracefully if possible)
21//! 2. Restores environment variables to their pre-cluster state
22//!
23//! # Thread Safety
24//!
25//! `ClusterGuard` is intentionally `!Send` because `ScopedEnv` uses thread-local
26//! storage to track environment changes. Dropping on a different thread would
27//! corrupt the environment restoration logic.
28//!
29//! # Testing
30//!
31//! `ClusterGuard`'s shutdown and environment-restoration behaviour is tested
32//! in `tests/cluster_split_constructors.rs`:
33//!
34//! - `new_split_creates_working_handle_and_guard`: Verifies that dropping the
35//! guard stops the `PostgreSQL` cluster (`postmaster.pid` is removed) and
36//! restores environment variables to their pre-cluster state.
37//!
38//! - `start_async_split_creates_working_handle_and_guard`: Tests the async
39//! variant with the same shutdown and restoration assertions.
40
41use super::runtime_mode::ClusterRuntime;
42use super::shutdown;
43use crate::env::ScopedEnv;
44use crate::observability::LOG_TARGET;
45use crate::{CleanupMode, TestBootstrapSettings};
46use postgresql_embedded::PostgreSQL;
47use tracing::{info, warn};
48
49/// Lifecycle guard for a running `PostgreSQL` cluster.
50///
51/// This guard manages cluster shutdown and environment restoration. It is
52/// intentionally `!Send` to ensure thread-local environment state is handled
53/// correctly.
54///
55/// # Obtaining a Guard
56///
57/// Use [`TestCluster::new_split()`](super::TestCluster::new_split) to obtain
58/// a handle and guard pair:
59///
60/// ```no_run
61/// use pg_embedded_setup_unpriv::TestCluster;
62///
63/// let (handle, guard) = TestCluster::new_split()?;
64/// // handle: ClusterHandle (Send + Sync)
65/// // guard: ClusterGuard (!Send, manages lifecycle)
66///
67/// // When guard drops, cluster shuts down and environment is restored
68/// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
69/// ```
70///
71/// # Shared Cluster Pattern
72///
73/// For shared clusters that should run for the entire process lifetime,
74/// the guard must be explicitly forgotten to prevent shutdown on drop.
75/// Use [`std::mem::forget`] to keep the cluster running:
76///
77/// ```no_run
78/// use std::sync::OnceLock;
79/// use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
80///
81/// static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
82///
83/// fn shared_handle() -> &'static ClusterHandle {
84/// SHARED.get_or_init(|| {
85/// let (handle, guard) = TestCluster::new_split()
86/// .expect("cluster bootstrap failed");
87/// // Forget the guard to prevent shutdown - cluster runs for process lifetime
88/// std::mem::forget(guard);
89/// handle
90/// })
91/// }
92/// ```
93///
94/// **Warning**: Dropping the guard shuts down the cluster. Do not use the
95/// handle after the guard has been dropped unless the guard was forgotten.
96#[derive(Debug)]
97pub struct ClusterGuard {
98 /// Runtime mode: either owns a runtime (sync) or runs on caller's runtime (async).
99 pub(super) runtime: ClusterRuntime,
100 /// The `PostgreSQL` instance, taken during shutdown.
101 pub(super) postgres: Option<PostgreSQL>,
102 /// Bootstrap settings needed for shutdown operations.
103 pub(super) bootstrap: TestBootstrapSettings,
104 /// Whether the cluster is managed via the worker subprocess.
105 pub(super) is_managed_via_worker: bool,
106 /// Environment variables applied to the cluster.
107 pub(super) env_vars: Vec<(String, Option<String>)>,
108 /// Optional worker environment guard.
109 pub(super) worker_guard: Option<ScopedEnv>,
110 /// Main environment guard (must drop last among env guards).
111 pub(super) _env_guard: ScopedEnv,
112 /// Keeps the cluster span alive for the lifetime of the guard.
113 pub(super) _cluster_span: tracing::Span,
114}
115
116// Note: ClusterGuard is !Send because it contains ScopedEnv which has
117// PhantomData<Rc<()>>. This is verified by the test in tests/test_cluster.rs
118// which uses a compile_fail doctest to ensure the type cannot be sent across
119// threads.
120
121impl ClusterGuard {
122 /// Extends the guard to cover an additional scoped environment.
123 ///
124 /// Primarily used by fixtures that need to ensure `PG_EMBEDDED_WORKER`
125 /// remains set for the duration of the cluster lifetime.
126 #[must_use]
127 pub fn with_worker_guard(mut self, worker_guard: Option<ScopedEnv>) -> Self {
128 self.worker_guard = worker_guard;
129 self
130 }
131
132 /// Overrides the cleanup mode used when the guard is dropped.
133 ///
134 /// # Examples
135 /// ```no_run
136 /// use pg_embedded_setup_unpriv::{CleanupMode, TestCluster};
137 ///
138 /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
139 /// let (handle, guard) = TestCluster::new_split()?;
140 /// let guard = guard.with_cleanup_mode(CleanupMode::None);
141 /// # drop(handle);
142 /// # drop(guard);
143 /// # Ok(())
144 /// # }
145 /// ```
146 #[must_use]
147 pub const fn with_cleanup_mode(mut self, cleanup_mode: CleanupMode) -> Self {
148 self.bootstrap.cleanup_mode = cleanup_mode;
149 self
150 }
151}
152
153impl Drop for ClusterGuard {
154 fn drop(&mut self) {
155 if self.should_skip_shutdown() {
156 return;
157 }
158 self.perform_shutdown();
159 // Environment guards drop after this block, restoring the process state.
160 }
161}
162
163impl ClusterGuard {
164 /// Returns true if shutdown should be skipped.
165 ///
166 /// Shutdown is skipped if the cluster was already stopped (e.g., via
167 /// `stop_async()`) or if the postgres handle was never initialised.
168 const fn should_skip_shutdown(&self) -> bool {
169 self.postgres.is_none() && !self.is_managed_via_worker
170 }
171
172 /// Performs cluster shutdown, logging and delegating to the appropriate path.
173 fn perform_shutdown(&mut self) {
174 let context = shutdown::stop_context(&self.bootstrap.settings);
175 let is_async = self.runtime.is_async();
176 info!(
177 target: LOG_TARGET,
178 context = %context,
179 worker_managed = self.is_managed_via_worker,
180 async_mode = is_async,
181 "stopping embedded postgres cluster"
182 );
183
184 if is_async {
185 self.drop_async_cluster(&context);
186 } else {
187 self.drop_sync_cluster(&context);
188 }
189 }
190
191 /// Asynchronous drop path: best-effort cleanup for async clusters.
192 fn drop_async_cluster(&mut self, context: &str) {
193 shutdown::drop_async_cluster(shutdown::DropContext {
194 is_managed_via_worker: self.is_managed_via_worker,
195 postgres: &mut self.postgres,
196 bootstrap: &self.bootstrap,
197 env_vars: &self.env_vars,
198 context,
199 });
200 }
201
202 /// Synchronous drop path: stops the cluster using the owned runtime.
203 fn drop_sync_cluster(&mut self, context: &str) {
204 let ClusterRuntime::Sync(ref runtime) = self.runtime else {
205 // Should never happen: drop_sync_cluster is only called for sync mode.
206 warn!(
207 target: LOG_TARGET,
208 "drop_sync_cluster called with non-sync runtime mode; skipping shutdown"
209 );
210 return;
211 };
212
213 shutdown::drop_sync_cluster(
214 runtime,
215 shutdown::DropContext {
216 is_managed_via_worker: self.is_managed_via_worker,
217 postgres: &mut self.postgres,
218 bootstrap: &self.bootstrap,
219 env_vars: &self.env_vars,
220 context,
221 },
222 );
223 }
224}