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}