Skip to main content

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}