sz_rust_sdk/core/
environment.rs

1//! Core implementation of SzEnvironment trait
2
3use crate::{
4    error::{SzError, SzResult},
5    ffi_call,
6    traits::*,
7    types::*,
8};
9use std::mem::ManuallyDrop;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Mutex, Once, OnceLock};
12
13/// Core implementation of the SzEnvironment trait.
14///
15/// `SzEnvironmentCore` manages the lifecycle of the Senzing environment and serves
16/// as a factory for obtaining instances of other SDK components. It implements a
17/// singleton pattern required by the Senzing native library.
18///
19/// # Complete Lifecycle Example
20///
21/// This example shows the full lifecycle: creation, usage, and cleanup.
22///
23/// ```no_run
24/// # use sz_rust_sdk::helpers::ExampleEnvironment;
25/// use sz_rust_sdk::prelude::*;
26/// use std::sync::Arc;
27///
28/// # let env = ExampleEnvironment::initialize("doctest_lifecycle")?;
29/// // Get SDK components
30/// let engine = env.get_engine()?;
31/// let product = env.get_product()?;
32///
33/// // Use the SDK
34/// let version = product.get_version()?;
35/// println!("Senzing version: {}", version);
36///
37/// engine.add_record("TEST", "LIFECYCLE_1",
38///     r#"{"NAME_FULL": "John Smith"}"#, None)?;
39///
40/// // Drop components first (they borrow the environment)
41/// drop(engine);
42/// drop(product);
43///
44/// // Destroy releases native resources
45/// env.destroy()?;
46/// # Ok::<(), SzError>(())
47/// ```
48///
49/// # Singleton Pattern
50///
51/// The Senzing native library requires a single global instance per process.
52/// `get_instance()` enforces this by storing one `Arc` reference in a static
53/// variable. This means:
54///
55/// - All `get_instance()` calls with the same parameters return the same instance
56/// - The returned `Arc` always has `strong_count() >= 2` (singleton + caller)
57/// - Calling `destroy()` removes the singleton reference and cleans up native
58///   resources only when you hold the last reference
59///
60/// # Thread Safety
61///
62/// `SzEnvironmentCore` is thread-safe (`Send + Sync`). The `Arc<SzEnvironmentCore>`
63/// can be cloned and shared across threads. Each call to `get_engine()`, etc.,
64/// returns an independent component instance that can be used in its own thread.
65///
66/// ```
67/// # use sz_rust_sdk::helpers::ExampleEnvironment;
68/// use sz_rust_sdk::prelude::*;
69///
70/// # let env = ExampleEnvironment::initialize("doctest_thread_safety")?;
71/// let handles: Vec<_> = (0..4).map(|i| {
72///     let env = env.clone();
73///     std::thread::spawn(move || {
74///         let engine = env.get_engine().unwrap();
75///         engine.add_record("TEST", &format!("THREAD_{}", i),
76///             r#"{"NAME_FULL": "Test User"}"#, None)
77///     })
78/// }).collect();
79///
80/// for h in handles { h.join().unwrap()?; }
81/// # drop(env);
82/// # Ok::<(), SzError>(())
83/// ```
84pub struct SzEnvironmentCore {
85    is_destroyed: Arc<AtomicBool>,
86    /// Guards Sz_init() - ensures it runs exactly once and other threads wait
87    init_once: Arc<Once>,
88    /// Stores any error that occurred during Sz_init
89    init_error: Arc<Mutex<Option<String>>>,
90    /// Guards SzConfigMgr_init() - ensures it runs exactly once and other threads wait
91    config_mgr_init_once: Arc<Once>,
92    /// Stores any error that occurred during SzConfigMgr_init
93    config_mgr_init_error: Arc<Mutex<Option<String>>>,
94    /// Guards SzProduct_init() - ensures it runs exactly once and other threads wait
95    product_init_once: Arc<Once>,
96    /// Stores any error that occurred during SzProduct_init
97    product_init_error: Arc<Mutex<Option<String>>>,
98    module_name: String,
99    ini_params: String,
100    verbose_logging: bool,
101}
102
103// Singleton storage for the global SzEnvironmentCore instance
104// Using ManuallyDrop to prevent static destructor from running at exit,
105// which avoids conflicts with Senzing's internal static mutex destruction order
106static GLOBAL_ENVIRONMENT: OnceLock<ManuallyDrop<Mutex<Option<Arc<SzEnvironmentCore>>>>> =
107    OnceLock::new();
108
109impl SzEnvironmentCore {
110    /// Creates a new SzEnvironment instance
111    ///
112    /// # Arguments
113    ///
114    /// * `module_name` - Name of the module for logging purposes
115    /// * `ini_params` - JSON string containing initialization parameters
116    /// * `verbose_logging` - Whether to enable verbose logging
117    pub fn new(module_name: &str, ini_params: &str, verbose_logging: bool) -> SzResult<Self> {
118        Ok(Self {
119            is_destroyed: Arc::new(AtomicBool::new(false)),
120            init_once: Arc::new(Once::new()),
121            init_error: Arc::new(Mutex::new(None)),
122            config_mgr_init_once: Arc::new(Once::new()),
123            config_mgr_init_error: Arc::new(Mutex::new(None)),
124            product_init_once: Arc::new(Once::new()),
125            product_init_error: Arc::new(Mutex::new(None)),
126            module_name: module_name.to_string(),
127            ini_params: ini_params.to_string(),
128            verbose_logging,
129        })
130    }
131
132    /// Creates a new SzEnvironment instance with default parameters
133    pub fn new_default() -> SzResult<Self> {
134        Self::new("SzRustSDK", "{}", false)
135    }
136
137    /// Gets or creates the global singleton SzEnvironmentCore instance
138    ///
139    /// This method ensures that only one SzEnvironmentCore instance exists
140    /// per process, which is required by the Senzing SDK.
141    ///
142    /// # Arguments
143    ///
144    /// * `module_name` - Name of the module for logging purposes
145    /// * `ini_params` - JSON string containing initialization parameters
146    /// * `verbose_logging` - Whether to enable verbose logging
147    ///
148    /// # Arc Reference Count Behavior
149    ///
150    /// **Important**: The returned `Arc<SzEnvironmentCore>` will always have
151    /// `strong_count() >= 2` due to the singleton pattern:
152    ///
153    /// - **Reference 1**: Stored in `GLOBAL_ENVIRONMENT` (the singleton storage)
154    /// - **Reference 2+**: Returned to the caller (and any clones they make)
155    ///
156    /// This is by design and is NOT a memory leak. The singleton reference ensures
157    /// that subsequent calls to `get_instance()` return the same instance.
158    ///
159    /// ```
160    /// # use sz_rust_sdk::helpers::ExampleEnvironment;
161    /// use sz_rust_sdk::prelude::*;
162    /// use std::sync::Arc;
163    ///
164    /// # let env = ExampleEnvironment::initialize("doctest_get_instance_arc")?;
165    /// assert!(Arc::strong_count(&env) >= 2); // Expected: singleton + caller
166    ///
167    /// let env2 = SzEnvironmentCore::get_existing_instance()?;
168    /// assert!(Arc::ptr_eq(&env, &env2)); // Same instance
169    /// assert!(Arc::strong_count(&env) >= 3); // singleton + 2 callers
170    /// # drop(env2);
171    /// # drop(env);
172    /// # Ok::<(), SzError>(())
173    /// ```
174    ///
175    /// # Cleanup
176    ///
177    /// To properly release resources, use `destroy()` which removes the singleton
178    /// reference and calls native cleanup when you hold the last reference:
179    ///
180    /// ```no_run
181    /// # use sz_rust_sdk::helpers::ExampleEnvironment;
182    /// use sz_rust_sdk::prelude::*;
183    ///
184    /// # let env = ExampleEnvironment::initialize("doctest_get_instance_cleanup")?;
185    /// let env2 = SzEnvironmentCore::get_existing_instance()?;
186    /// // Drop all other references first
187    /// drop(env2);
188    /// // Then destroy - this removes singleton ref and cleans up native resources
189    /// env.destroy()?;
190    /// # Ok::<(), SzError>(())
191    /// ```
192    pub fn get_instance(
193        module_name: &str,
194        ini_params: &str,
195        verbose_logging: bool,
196    ) -> SzResult<Arc<Self>> {
197        let global_env = GLOBAL_ENVIRONMENT.get_or_init(|| ManuallyDrop::new(Mutex::new(None)));
198        let mut env_guard = global_env.lock().unwrap();
199
200        match env_guard.as_ref() {
201            Some(existing_env) => {
202                // Check if the existing environment is still valid
203                if existing_env.is_destroyed() {
204                    let new_env = Arc::new(Self::new(module_name, ini_params, verbose_logging)?);
205                    *env_guard = Some(new_env.clone());
206                    Ok(new_env)
207                } else {
208                    // Validate critical parameters match existing instance (ini_params and verbose_logging)
209                    // Module name can be different as it's only used for logging
210                    if existing_env.ini_params != ini_params
211                        || existing_env.verbose_logging != verbose_logging
212                    {
213                        return Err(SzError::configuration(
214                            "Cannot change critical initialization parameters (ini_params, verbose_logging) after SzEnvironmentCore instance is created",
215                        ));
216                    }
217                    // Return the existing valid environment (module name can be different)
218                    Ok(existing_env.clone())
219                }
220            }
221            None => {
222                // Create the first instance
223                let new_env = Arc::new(Self::new(module_name, ini_params, verbose_logging)?);
224                *env_guard = Some(new_env.clone());
225                Ok(new_env)
226            }
227        }
228    }
229
230    /// Gets the existing global singleton SzEnvironmentCore instance
231    ///
232    /// This method returns the existing singleton instance without creating a new one.
233    /// It will return an error if no instance has been created yet.
234    ///
235    /// # Returns
236    ///
237    /// Returns the existing singleton instance or an error if none exists.
238    pub fn get_existing_instance() -> SzResult<Arc<Self>> {
239        let global_env = GLOBAL_ENVIRONMENT.get_or_init(|| ManuallyDrop::new(Mutex::new(None)));
240        let env_guard = global_env.lock().unwrap();
241
242        match env_guard.as_ref() {
243            Some(existing_env) => {
244                if existing_env.is_destroyed() {
245                    Err(SzError::unrecoverable(
246                        "SzEnvironmentCore instance has been destroyed",
247                    ))
248                } else {
249                    Ok(existing_env.clone())
250                }
251            }
252            None => Err(SzError::unrecoverable(
253                "No SzEnvironmentCore instance has been created yet. Call get_instance() first.",
254            )),
255        }
256    }
257
258    /// Gets the global singleton instance if it exists
259    ///
260    /// Returns None if no instance has been created yet.
261    pub fn try_get_instance() -> Option<Arc<Self>> {
262        GLOBAL_ENVIRONMENT
263            .get()?
264            .lock()
265            .unwrap()
266            .as_ref()
267            .map(|env| env.clone())
268    }
269
270    /// Destroys the environment, consuming the Arc and releasing all native resources.
271    ///
272    /// This method uses Rust's ownership semantics to ensure safe cleanup:
273    /// - Only succeeds if the caller holds the sole reference to the environment
274    /// - If other references exist (e.g., other threads still using the environment),
275    ///   returns an error and the environment remains valid
276    ///
277    /// # Ownership Requirements
278    ///
279    /// The Senzing native library is a global resource. Destroying the environment
280    /// while other code still holds references would cause undefined behavior.
281    /// This method enforces that you can only destroy when you're the sole owner.
282    ///
283    /// # Example
284    ///
285    /// ```no_run
286    /// # use sz_rust_sdk::helpers::ExampleEnvironment;
287    /// use sz_rust_sdk::prelude::*;
288    ///
289    /// # let env = ExampleEnvironment::initialize("doctest_destroy_basic")?;
290    /// // ... use env ...
291    ///
292    /// // When done, destroy (only works if this is the only reference)
293    /// env.destroy()?;
294    /// # Ok::<(), SzError>(())
295    /// ```
296    ///
297    /// # Calling `destroy()` from a `Drop` Implementation
298    ///
299    /// Because `destroy()` takes `Arc<Self>` by value, calling it from a `Drop`
300    /// impl requires moving the Arc out of your struct. **Use `Option<Arc<...>>`
301    /// with `.take()`** — this is exactly what [`SenzingGuard`](super::SenzingGuard) does internally.
302    ///
303    /// ```no_run
304    /// use sz_rust_sdk::prelude::*;
305    /// use std::sync::Arc;
306    ///
307    /// // CORRECT: Use Option<Arc<...>> + take()
308    /// struct MyWrapper {
309    ///     env: Option<Arc<SzEnvironmentCore>>,
310    /// }
311    ///
312    /// impl Drop for MyWrapper {
313    ///     fn drop(&mut self) {
314    ///         if let Some(env) = self.env.take() {
315    ///             let _ = env.destroy();
316    ///         }
317    ///     }
318    /// }
319    ///
320    /// # use sz_rust_sdk::helpers::ExampleEnvironment;
321    /// # let env = ExampleEnvironment::initialize("doctest_destroy_drop_correct")?;
322    /// let wrapper = MyWrapper { env: Some(env) };
323    /// drop(wrapper); // Cleanup happens via Drop
324    /// # Ok::<(), SzError>(())
325    /// ```
326    ///
327    /// ```no_run
328    /// use sz_rust_sdk::prelude::*;
329    /// use std::sync::Arc;
330    ///
331    /// // WRONG: ptr::read causes double-free / heap corruption
332    /// struct MyWrapper {
333    ///     env: Arc<SzEnvironmentCore>,  // Not wrapped in Option!
334    /// }
335    ///
336    /// impl Drop for MyWrapper {
337    ///     fn drop(&mut self) {
338    ///         // BUG: ptr::read creates a second Arc without incrementing the
339    ///         // reference count. After destroy() frees the inner data, the
340    ///         // compiler also drops self.env -> double-free -> heap corruption.
341    ///         let env = unsafe { std::ptr::read(&self.env) };
342    ///         let _ = env.destroy();
343    ///         // <- compiler drops self.env here (use-after-free!)
344    ///     }
345    /// }
346    /// ```
347    ///
348    /// **Note:** Using `ManuallyDrop` instead of `ptr::read` avoids the
349    /// double-free but is unnecessarily complex. Prefer `Option::take()` or
350    /// use [`SenzingGuard`](super::SenzingGuard) directly — it handles all of this correctly.
351    ///
352    /// # Errors
353    ///
354    /// Returns `SzError::Unrecoverable` if:
355    /// - Other references to the environment still exist
356    /// - The environment was already destroyed
357    pub fn destroy(self: Arc<Self>) -> SzResult<()> {
358        // First, remove from global singleton storage
359        // This drops the global reference, leaving only the caller's reference
360        if let Some(global_env) = GLOBAL_ENVIRONMENT.get() {
361            let mut env_guard = match global_env.lock() {
362                Ok(guard) => guard,
363                Err(poisoned) => poisoned.into_inner(),
364            };
365            // Only take if it's the same instance
366            if let Some(stored) = env_guard.as_ref()
367                && Arc::ptr_eq(stored, &self)
368            {
369                env_guard.take();
370            }
371        }
372
373        // Now try to get exclusive ownership
374        match Arc::try_unwrap(self) {
375            Ok(env) => {
376                // We have sole ownership - safe to destroy
377                if env.is_destroyed.load(Ordering::Relaxed) {
378                    return Ok(()); // Already destroyed, nothing to do
379                }
380
381                // Mark as destroyed
382                env.is_destroyed.store(true, Ordering::Relaxed);
383
384                // Cleanup all Senzing modules
385                // Note: SzConfig_destroy() is not needed here - it manages config handles,
386                // not the config system itself. Config handles have their own lifecycle.
387                unsafe {
388                    let _ = crate::ffi::SzDiagnostic_destroy();
389                    let _ = crate::ffi::SzProduct_destroy();
390                    let _ = crate::ffi::SzConfigMgr_destroy(); // CRITICAL: Clears cached config state
391                    let _ = crate::ffi::Sz_destroy();
392
393                    // Clear exception states
394                    crate::ffi::Sz_clearLastException();
395                    crate::ffi::SzDiagnostic_clearLastException();
396                    crate::ffi::SzProduct_clearLastException();
397                    crate::ffi::SzConfigMgr_clearLastException();
398                }
399
400                // Give the native library time to fully clean up internal state
401                std::thread::sleep(std::time::Duration::from_millis(100));
402
403                Ok(())
404            }
405            Err(arc) => {
406                // Other references exist - put it back in global storage and return error
407                // We must restore it since we removed it earlier
408                if let Some(global_env) = GLOBAL_ENVIRONMENT.get()
409                    && let Ok(mut env_guard) = global_env.lock()
410                {
411                    *env_guard = Some(arc);
412                }
413                Err(SzError::unrecoverable(
414                    "Cannot destroy environment: other references still exist. \
415                     Ensure all Arc<SzEnvironmentCore> clones are dropped before calling destroy().",
416                ))
417            }
418        }
419    }
420
421    /// Get the initialization parameters used by this environment
422    pub fn get_ini_params(&self) -> &str {
423        &self.ini_params
424    }
425
426    /// Get the verbose logging setting used by this environment
427    pub fn get_verbose_logging(&self) -> bool {
428        self.verbose_logging
429    }
430
431    /// Ensures Sz_init has been called - should be called before any engine operations
432    ///
433    /// This method is thread-safe: the first thread to call this will run Sz_init(),
434    /// and all other threads will block until initialization is complete.
435    fn ensure_initialized(&self) -> SzResult<()> {
436        // Clone Arcs for use in closure (can't capture &self in call_once)
437        let module_name = self.module_name.clone();
438        let ini_params = self.ini_params.clone();
439        let verbose_logging = self.verbose_logging;
440        let init_error = Arc::clone(&self.init_error);
441
442        // call_once blocks all threads until the closure completes
443        self.init_once.call_once(|| {
444            let result = (|| -> SzResult<()> {
445                let module_name_c = crate::ffi::helpers::str_to_c_string(&module_name)?;
446                let ini_params_c = crate::ffi::helpers::str_to_c_string(&ini_params)?;
447                let verbose = if verbose_logging { 1 } else { 0 };
448
449                ffi_call!(crate::ffi::Sz_init(
450                    module_name_c.as_ptr(),
451                    ini_params_c.as_ptr(),
452                    verbose as i64
453                ));
454                Ok(())
455            })();
456
457            // Store any error for other threads to see
458            if let Err(e) = result
459                && let Ok(mut guard) = init_error.lock()
460            {
461                *guard = Some(e.to_string());
462            }
463        });
464
465        // Check if initialization failed
466        if let Ok(guard) = self.init_error.lock()
467            && let Some(err_msg) = guard.as_ref()
468        {
469            return Err(SzError::unrecoverable(format!("Sz_init failed: {err_msg}")));
470        }
471
472        Ok(())
473    }
474
475    /// Ensures SzConfigMgr_init has been called - should be called before any config manager operations
476    ///
477    /// This method is thread-safe: the first thread to call this will run SzConfigMgr_init(),
478    /// and all other threads will block until initialization is complete.
479    fn ensure_config_mgr_initialized(&self) -> SzResult<()> {
480        // Clone Arcs for use in closure (can't capture &self in call_once)
481        let module_name = self.module_name.clone();
482        let ini_params = self.ini_params.clone();
483        let verbose_logging = self.verbose_logging;
484        let init_error = Arc::clone(&self.config_mgr_init_error);
485
486        // call_once blocks all threads until the closure completes
487        self.config_mgr_init_once.call_once(|| {
488            let result = (|| -> SzResult<()> {
489                let module_name_c = crate::ffi::helpers::str_to_c_string(&module_name)?;
490                let ini_params_c = crate::ffi::helpers::str_to_c_string(&ini_params)?;
491                let verbose = if verbose_logging { 1 } else { 0 };
492
493                // Call the FFI directly and check with the proper config_mgr error handler
494                let return_code = unsafe {
495                    crate::ffi::SzConfigMgr_init(
496                        module_name_c.as_ptr(),
497                        ini_params_c.as_ptr(),
498                        verbose,
499                    )
500                };
501                crate::ffi::helpers::check_config_mgr_return_code(return_code)?;
502                Ok(())
503            })();
504
505            // Store any error for other threads to see
506            if let Err(e) = result
507                && let Ok(mut guard) = init_error.lock()
508            {
509                *guard = Some(e.to_string());
510            }
511        });
512
513        // Check if initialization failed
514        if let Ok(guard) = self.config_mgr_init_error.lock()
515            && let Some(err_msg) = guard.as_ref()
516        {
517            return Err(SzError::unrecoverable(format!(
518                "SzConfigMgr_init failed: {err_msg}"
519            )));
520        }
521
522        Ok(())
523    }
524
525    /// Ensures SzProduct_init has been called - should be called before any product operations
526    ///
527    /// This method is thread-safe: the first thread to call this will run SzProduct_init(),
528    /// and all other threads will block until initialization is complete.
529    fn ensure_product_initialized(&self) -> SzResult<()> {
530        // Clone Arcs for use in closure (can't capture &self in call_once)
531        let module_name = self.module_name.clone();
532        let ini_params = self.ini_params.clone();
533        let verbose_logging = self.verbose_logging;
534        let init_error = Arc::clone(&self.product_init_error);
535
536        // call_once blocks all threads until the closure completes
537        self.product_init_once.call_once(|| {
538            let result = (|| -> SzResult<()> {
539                let module_name_c = crate::ffi::helpers::str_to_c_string(&module_name)?;
540                let ini_params_c = crate::ffi::helpers::str_to_c_string(&ini_params)?;
541                let verbose = if verbose_logging { 1 } else { 0 };
542
543                // Call the FFI directly and check with the proper product error handler
544                let return_code = unsafe {
545                    crate::ffi::SzProduct_init(
546                        module_name_c.as_ptr(),
547                        ini_params_c.as_ptr(),
548                        verbose,
549                    )
550                };
551                crate::ffi::helpers::check_product_return_code(return_code)?;
552                Ok(())
553            })();
554
555            // Store any error for other threads to see
556            if let Err(e) = result
557                && let Ok(mut guard) = init_error.lock()
558            {
559                *guard = Some(e.to_string());
560            }
561        });
562
563        // Check if initialization failed
564        if let Ok(guard) = self.product_init_error.lock()
565            && let Some(err_msg) = guard.as_ref()
566        {
567            return Err(SzError::unrecoverable(format!(
568                "SzProduct_init failed: {err_msg}"
569            )));
570        }
571
572        Ok(())
573    }
574}
575
576impl SzEnvironment for SzEnvironmentCore {
577    fn is_destroyed(&self) -> bool {
578        self.is_destroyed.load(Ordering::Relaxed)
579    }
580
581    fn reinitialize(&self, config_id: ConfigId) -> SzResult<()> {
582        if self.is_destroyed() {
583            return Err(SzError::unrecoverable("Environment has been destroyed"));
584        }
585
586        // Ensure Sz_init has been called before reinitializing
587        self.ensure_initialized()?;
588
589        ffi_call!(crate::ffi::Sz_reinit(config_id));
590        Ok(())
591    }
592
593    fn get_active_config_id(&self) -> SzResult<ConfigId> {
594        if self.is_destroyed() {
595            return Err(SzError::unrecoverable("Environment has been destroyed"));
596        }
597
598        // Ensure Sz_init has been called before getting active config ID
599        self.ensure_initialized()?;
600
601        let mut config_id: i64 = 0;
602        let return_code = unsafe { crate::ffi::Sz_getActiveConfigID(&mut config_id) };
603        crate::ffi::helpers::check_return_code(return_code)?;
604        Ok(config_id)
605    }
606
607    fn get_product(&self) -> SzResult<Box<dyn SzProduct>> {
608        if self.is_destroyed() {
609            return Err(SzError::unrecoverable("Environment has been destroyed"));
610        }
611
612        // Ensure SzProduct_init has been called (thread-safe, all threads wait for completion)
613        self.ensure_product_initialized()?;
614
615        // Create product instance (init already done, so this is safe)
616        let product_core = super::product::SzProductCore::new()?;
617        Ok(Box::new(product_core))
618    }
619
620    fn get_engine(&self) -> SzResult<Box<dyn SzEngine>> {
621        if self.is_destroyed() {
622            return Err(SzError::unrecoverable("Environment has been destroyed"));
623        }
624
625        // Ensure Sz_init has been called before creating engine
626        self.ensure_initialized()?;
627
628        let engine_core = super::engine::SzEngineCore::new()?;
629        Ok(Box::new(engine_core))
630    }
631
632    fn get_config_manager(&self) -> SzResult<Box<dyn SzConfigManager>> {
633        if self.is_destroyed() {
634            return Err(SzError::unrecoverable("Environment has been destroyed"));
635        }
636
637        // Ensure SzConfigMgr_init has been called (thread-safe, all threads wait for completion)
638        // Note: SzConfigMgr does NOT require Sz_init - it initializes independently
639        // This allows config setup before engine initialization
640        self.ensure_config_mgr_initialized()?;
641
642        // Create config manager instance (init already done, so this is safe)
643        let config_mgr_core = super::config_manager::SzConfigManagerCore::new()?;
644        Ok(Box::new(config_mgr_core))
645    }
646
647    fn get_diagnostic(&self) -> SzResult<Box<dyn SzDiagnostic>> {
648        if self.is_destroyed() {
649            return Err(SzError::unrecoverable("Environment has been destroyed"));
650        }
651
652        // Ensure Sz_init has been called before creating diagnostic
653        self.ensure_initialized()?;
654
655        let diagnostic_core = super::diagnostic::SzDiagnosticCore::new_with_params(
656            &self.module_name,
657            &self.ini_params,
658            self.verbose_logging,
659        )?;
660        Ok(Box::new(diagnostic_core))
661    }
662}
663
664/// # Drop Behavior - Intentionally Does NOT Clean Up Native Resources
665///
666/// The `Drop` implementation for `SzEnvironmentCore` is **intentionally a no-op**
667/// that only marks the environment as destroyed without releasing native Senzing
668/// resources. This design is deliberate for several important reasons:
669///
670/// ## Why Drop Doesn't Clean Up
671///
672/// 1. **Singleton Pattern Complexity**: Due to the global singleton pattern,
673///    multiple `Arc` references exist (one in `GLOBAL_ENVIRONMENT`, one or more
674///    held by callers). The `Drop` trait cannot detect if this is the "last"
675///    reference being dropped.
676///
677/// 2. **Native Library Safety**: The Senzing native library has specific
678///    destruction ordering requirements. Calling `Sz_destroy()` while other
679///    threads might still be using the library can cause undefined behavior.
680///
681/// 3. **Controlled Shutdown**: Users need explicit control over when native
682///    resources are released, especially in applications that may reinitialize
683///    with different configurations.
684///
685/// ## Proper Cleanup Pattern
686///
687/// **Recommended:** Use [`SenzingGuard`](super::SenzingGuard) for automatic RAII cleanup:
688///
689/// ```no_run
690/// # use sz_rust_sdk::helpers::ExampleEnvironment;
691/// use sz_rust_sdk::prelude::*;
692///
693/// // Explicit destroy (ownership-based)
694/// # let env = ExampleEnvironment::initialize("doctest_drop_cleanup")?;
695/// let engine = env.get_engine()?;
696/// // ... use engine ...
697/// drop(engine);
698/// env.destroy()?;  // Explicitly release native resources
699/// # Ok::<(), SzError>(())
700/// ```
701///
702/// If you need to call `destroy()` from your own `Drop` implementation,
703/// see the safety notes on [`destroy()`](Self::destroy) — you **must** store
704/// the Arc in an `Option` and use `.take()` to avoid double-free bugs.
705///
706/// ## What This Implementation Does
707///
708/// - Marks the environment as "destroyed" to prevent further API calls
709/// - Does **NOT** call `Sz_destroy()` or other native cleanup functions
710/// - Allows garbage collection of Rust-side resources
711impl Drop for SzEnvironmentCore {
712    fn drop(&mut self) {
713        // Mark as destroyed to prevent further use through any remaining references.
714        // We intentionally do NOT call Sz_destroy() here because:
715        // 1. We can't detect if this is the last Arc reference (singleton has one too)
716        // 2. Calling native cleanup during Drop can race with other threads
717        // 3. Users should use destroy() for explicit cleanup or SenzingGuard for RAII
718        if !self.is_destroyed() {
719            self.is_destroyed
720                .store(true, std::sync::atomic::Ordering::Relaxed);
721        }
722    }
723}