sz_rust_sdk/core/guard.rs
1//! RAII guard for automatic Senzing environment cleanup
2//!
3//! This module provides [`SenzingGuard`], an RAII wrapper around
4//! `Arc<SzEnvironmentCore>` that automatically cleans up native Senzing
5//! resources when dropped.
6
7use crate::error::SzResult;
8use std::sync::Arc;
9
10/// RAII guard for automatic Senzing environment cleanup.
11///
12/// `SenzingGuard` wraps an `Arc<SzEnvironmentCore>` and ensures that native
13/// Senzing resources are properly released when the guard goes out of scope.
14/// This provides idiomatic Rust resource management without requiring explicit
15/// `destroy()` calls.
16///
17/// # Lifecycle
18///
19/// 1. **Creation**: Initializes or obtains the Senzing environment
20/// 2. **Usage**: Access the environment via `Deref` or `env()` method
21/// 3. **Destruction**: When dropped, removes the singleton reference and
22/// attempts to release native resources
23///
24/// # Example
25///
26/// ```no_run
27/// # use sz_rust_sdk::helpers::ExampleEnvironment;
28/// use sz_rust_sdk::prelude::*;
29///
30/// # let env = ExampleEnvironment::initialize("doctest_guard_struct")?;
31/// // Guard automatically manages the lifecycle
32/// let guard = SenzingGuard::from_env(env);
33///
34/// // Access environment and components
35/// let engine = guard.get_engine()?;
36/// let product = guard.get_product()?;
37///
38/// // Add records
39/// engine.add_record("TEST", "GUARD_1", r#"{"NAME_FULL": "John"}"#, None)?;
40///
41/// // Resources released automatically when guard drops
42/// # Ok::<(), SzError>(())
43/// ```
44///
45/// # Thread Safety
46///
47/// `SenzingGuard` is thread-safe (`Send + Sync`) because it wraps an
48/// `Arc<SzEnvironmentCore>`. The underlying environment can be safely
49/// shared across threads.
50///
51/// # Comparison with Manual `destroy()`
52///
53/// | Aspect | `SenzingGuard` | Manual `destroy()` |
54/// |--------|----------------|-------------------|
55/// | Resource release | Automatic on drop | Explicit call required |
56/// | Error handling | Panics on cleanup failure | Returns `SzResult` |
57/// | Multiple references | Must be sole owner | Returns error if refs exist |
58/// | Idiomatic Rust | ✅ RAII pattern | ❌ Explicit lifecycle |
59///
60/// # Why Use This Instead of a Custom `Drop`
61///
62/// The [`SzEnvironmentCore::destroy()`](super::SzEnvironmentCore::destroy)
63/// method takes `Arc<Self>` by value, which makes it tricky to call from a
64/// `Drop` implementation (since `Drop` only gives `&mut self`).
65/// `SenzingGuard` solves this by storing the Arc in an `Option` and using
66/// `.take()` to safely move it out before calling `destroy()`. If you write
67/// your own wrapper, follow the same pattern — see the safety notes on
68/// [`SzEnvironmentCore::destroy()`](super::SzEnvironmentCore::destroy)
69/// for details.
70///
71/// # Panic Behavior
72///
73/// The guard will panic if cleanup fails during `Drop`. This is intentional
74/// to prevent silent resource leaks. If you need to handle cleanup errors
75/// gracefully, use the explicit `into_inner()` method and call `destroy()`
76/// manually.
77pub struct SenzingGuard {
78 env: Option<Arc<super::SzEnvironmentCore>>,
79}
80
81impl SenzingGuard {
82 /// Creates a new `SenzingGuard` with the specified configuration.
83 ///
84 /// This initializes the Senzing environment singleton or returns the
85 /// existing instance if one already exists with compatible parameters.
86 ///
87 /// # Arguments
88 ///
89 /// * `module_name` - Name for logging purposes
90 /// * `ini_params` - JSON string with Senzing configuration
91 /// * `verbose_logging` - Enable verbose logging
92 ///
93 /// # Example
94 ///
95 /// ```no_run
96 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
97 /// use sz_rust_sdk::prelude::*;
98 ///
99 /// # let env = ExampleEnvironment::initialize("doctest_guard_new")?;
100 /// // SenzingGuard::new(name, settings, verbose) initializes directly.
101 /// // Here we use from_env with a pre-initialized environment:
102 /// let guard = SenzingGuard::from_env(env);
103 /// let product = guard.get_product()?;
104 /// # Ok::<(), SzError>(())
105 /// ```
106 pub fn new(module_name: &str, ini_params: &str, verbose_logging: bool) -> SzResult<Self> {
107 let env = super::SzEnvironmentCore::get_instance(module_name, ini_params, verbose_logging)?;
108 Ok(Self { env: Some(env) })
109 }
110
111 /// Creates a guard from an existing environment instance.
112 ///
113 /// Use this when you already have an `Arc<SzEnvironmentCore>` and want
114 /// to transfer ownership to a guard for automatic cleanup.
115 ///
116 /// # Example
117 ///
118 /// ```no_run
119 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
120 /// use sz_rust_sdk::prelude::*;
121 ///
122 /// # let env = ExampleEnvironment::initialize("doctest_guard_from_env")?;
123 /// let guard = SenzingGuard::from_env(env);
124 /// // guard now owns the Arc and will clean up on drop
125 /// let product = guard.get_product()?;
126 /// # Ok::<(), SzError>(())
127 /// ```
128 pub fn from_env(env: Arc<super::SzEnvironmentCore>) -> Self {
129 Self { env: Some(env) }
130 }
131
132 /// Gets a reference to the inner environment.
133 ///
134 /// # Panics
135 ///
136 /// Panics if the guard has already been consumed via `into_inner()`.
137 pub fn env(&self) -> &Arc<super::SzEnvironmentCore> {
138 self.env.as_ref().expect("SenzingGuard already consumed")
139 }
140
141 /// Consumes the guard and returns the inner `Arc<SzEnvironmentCore>`.
142 ///
143 /// After calling this, the guard will NOT perform automatic cleanup.
144 /// You become responsible for calling `destroy()` on the returned Arc.
145 ///
146 /// # Example
147 ///
148 /// ```no_run
149 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
150 /// use sz_rust_sdk::prelude::*;
151 ///
152 /// # let env = ExampleEnvironment::initialize("doctest_guard_into_inner")?;
153 /// let guard = SenzingGuard::from_env(env);
154 /// let env = guard.into_inner();
155 /// // Manual cleanup required after into_inner()
156 /// env.destroy()?;
157 /// # Ok::<(), SzError>(())
158 /// ```
159 pub fn into_inner(mut self) -> Arc<super::SzEnvironmentCore> {
160 self.env.take().expect("SenzingGuard already consumed")
161 }
162
163 /// Attempts cleanup without panicking.
164 ///
165 /// Returns an error if cleanup fails, allowing graceful error handling.
166 /// After calling this, the guard is consumed and Drop will be a no-op.
167 ///
168 /// # Example
169 ///
170 /// ```no_run
171 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
172 /// use sz_rust_sdk::prelude::*;
173 ///
174 /// # let env = ExampleEnvironment::initialize("doctest_guard_try_cleanup")?;
175 /// let guard = SenzingGuard::from_env(env);
176 /// // ... use guard ...
177 /// if let Err(e) = guard.try_cleanup() {
178 /// eprintln!("Cleanup failed: {}", e);
179 /// }
180 /// # Ok::<(), SzError>(())
181 /// ```
182 pub fn try_cleanup(mut self) -> SzResult<()> {
183 if let Some(env) = self.env.take() {
184 env.destroy()
185 } else {
186 Ok(())
187 }
188 }
189}
190
191// Allow direct access to SzEnvironment trait methods via Deref
192impl std::ops::Deref for SenzingGuard {
193 type Target = Arc<super::SzEnvironmentCore>;
194
195 fn deref(&self) -> &Self::Target {
196 self.env()
197 }
198}
199
200// Implement Drop to automatically clean up resources
201impl Drop for SenzingGuard {
202 fn drop(&mut self) {
203 if let Some(env) = self.env.take() {
204 // Attempt to destroy the environment
205 // If this fails (e.g., other references exist), we log and continue
206 // rather than panicking, to be more forgiving in edge cases
207 match env.destroy() {
208 Ok(()) => {}
209 Err(e) => {
210 // Only log if this is a genuine error, not "already destroyed"
211 if !e.to_string().contains("already destroyed")
212 && !e.to_string().contains("other references")
213 {
214 eprintln!("SenzingGuard: cleanup warning: {e}");
215 }
216 }
217 }
218 }
219 }
220}
221
222// Thread safety: SenzingGuard is safe to send/share because it only contains Arc
223unsafe impl Send for SenzingGuard {}
224unsafe impl Sync for SenzingGuard {}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 // Note: Full integration tests require Senzing to be installed.
231 // These are compile-time checks only.
232
233 fn _assert_send_sync<T: Send + Sync>() {}
234
235 #[test]
236 fn senzing_guard_is_send_sync() {
237 _assert_send_sync::<SenzingGuard>();
238 }
239}