Skip to main content

qubit_dcl/double_checked/
double_checked_lock_executor.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! # Double-Checked Lock Executor
11//!
12//! Provides a reusable executor for double-checked locking workflows.
13//!
14
15use std::{
16    any::Any,
17    fmt::Display,
18    marker::PhantomData,
19    panic::{
20        self,
21        AssertUnwindSafe,
22    },
23};
24
25use qubit_function::{
26    ArcRunnable,
27    ArcTester,
28    Callable,
29    CallableWith,
30    Runnable,
31    RunnableWith,
32    Tester,
33};
34
35use super::{
36    ExecutionContext,
37    ExecutionLogger,
38    ExecutionResult,
39    ExecutorError,
40    executor_builder::ExecutorBuilder,
41    executor_ready_builder::ExecutorReadyBuilder,
42};
43use qubit_lock::Lock;
44
45/// Reusable double-checked lock executor.
46///
47/// The executor owns the lock handle, condition tester, execution logger, and
48/// optional prepare lifecycle callbacks. Each execution performs:
49///
50/// 1. A first condition check outside the lock.
51/// 2. Optional prepare action.
52/// 3. Lock acquisition.
53/// 4. A second condition check inside the lock.
54/// 5. The submitted task.
55/// 6. Optional prepare commit or rollback after the lock is released.
56///
57/// The tester is intentionally run both outside and inside the lock. Any state
58/// read by the first check must therefore use atomics or another synchronization
59/// mechanism that is safe without this executor's lock.
60///
61/// # Type Parameters
62///
63/// * `L` - The lock type implementing [`Lock<T>`].
64/// * `T` - The data type protected by the lock.
65///
66/// # Examples
67///
68/// Use [`DoubleCheckedLockExecutor::builder`] to attach a lock (for example
69/// [`crate::ArcMutex`]), set a [`Tester`] with
70/// [`ExecutorLockBuilder::when`](super::ExecutorLockBuilder::when), then call
71/// [`Self::call`], [`Self::execute`], [`Self::call_with`], or
72/// [`Self::execute_with`] on the built executor.
73///
74/// Panics from the tester, prepare callbacks, or task can be captured by
75/// configuring the builder or by deriving a reconfigured executor with
76/// [`with_panic_capture`](Self::with_panic_capture). Tester and task panics
77/// are reported as [`super::ExecutorError::Panic`]. Prepare lifecycle panics
78/// are reported through the corresponding prepare, commit, or rollback error
79/// variants, so rollback can still be executed after captured task or second
80/// condition-check panics.
81///
82/// Cloned executors share their configured prepare callbacks. Concurrent calls
83/// may therefore complete prepare in several threads before one call wins the
84/// second condition check; calls that lose the second check run prepare rollback
85/// if it is configured.
86///
87/// ```rust
88/// use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
89///
90/// use qubit_dcl::{ArcMutex, DoubleCheckedLockExecutor, Lock};
91/// use qubit_dcl::double_checked::ExecutionResult;
92///
93/// fn main() {
94///     let data = ArcMutex::new(10);
95///     let skip = Arc::new(AtomicBool::new(false));
96///
97///     let executor = DoubleCheckedLockExecutor::builder()
98///         .on(data.clone())
99///         .when({
100///             let skip = skip.clone();
101///             move || !skip.load(Ordering::Acquire)
102///         })
103///         .build();
104///
105///     let updated = executor
106///         .call_with(|value: &mut i32| {
107///             *value += 5;
108///             Ok::<i32, std::io::Error>(*value)
109///         })
110///         .get_result();
111///
112///     assert!(matches!(updated, ExecutionResult::Success(15)));
113///     assert_eq!(data.read(|value| *value), 15);
114///
115///     skip.store(true, Ordering::Release);
116///     let skipped = executor
117///         .call_with(|value: &mut i32| {
118///             *value += 1;
119///             Ok::<i32, std::io::Error>(*value)
120///         })
121///         .get_result();
122///
123///     assert!(matches!(skipped, ExecutionResult::ConditionNotMet));
124///     assert_eq!(data.read(|value| *value), 15);
125/// }
126/// ```
127///
128#[derive(Clone)]
129pub struct DoubleCheckedLockExecutor<L = (), T = ()> {
130    /// The lock protecting the target data.
131    lock: L,
132
133    /// Condition checked before and after acquiring the lock.
134    tester: ArcTester,
135
136    /// Logger for unmet conditions and prepare lifecycle failures.
137    logger: ExecutionLogger,
138
139    /// Optional action executed after the first check and before locking.
140    prepare_action: Option<ArcRunnable<CallbackError>>,
141
142    /// Optional action executed when prepare must be rolled back.
143    rollback_prepare_action: Option<ArcRunnable<CallbackError>>,
144
145    /// Optional action executed when prepare should be committed.
146    commit_prepare_action: Option<ArcRunnable<CallbackError>>,
147
148    /// Whether panics from tester, callbacks, and task are captured as errors.
149    catch_panics: bool,
150
151    /// Carries the protected data type.
152    _phantom: PhantomData<fn() -> T>,
153}
154
155impl DoubleCheckedLockExecutor<(), ()> {
156    /// Creates a builder for a reusable double-checked lock executor.
157    ///
158    /// # Returns
159    ///
160    /// A builder in the initial state. Attach a lock with
161    /// [`ExecutorBuilder::on`], then configure a tester with
162    /// [`ExecutorLockBuilder::when`](super::ExecutorLockBuilder::when).
163    #[inline]
164    #[must_use = "assign or chain the returned builder"]
165    pub fn builder() -> ExecutorBuilder {
166        ExecutorBuilder::default()
167    }
168}
169
170impl<L, T> DoubleCheckedLockExecutor<L, T>
171where
172    L: Lock<T>,
173{
174    /// Assembles an executor from the ready builder state.
175    ///
176    /// # Parameters
177    ///
178    /// * `builder` - Ready builder carrying the lock, tester, logger, and
179    ///   prepare lifecycle callbacks.
180    ///
181    /// # Returns
182    ///
183    /// A reusable executor containing the supplied builder state.
184    #[inline]
185    #[must_use = "use the returned executor"]
186    pub fn new(builder: ExecutorReadyBuilder<L, T>) -> Self {
187        Self {
188            lock: builder.lock,
189            tester: builder.tester,
190            logger: builder.logger,
191            prepare_action: builder.prepare_action,
192            rollback_prepare_action: builder.rollback_prepare_action,
193            commit_prepare_action: builder.commit_prepare_action,
194            catch_panics: builder.catch_panics,
195            _phantom: builder._phantom,
196        }
197    }
198
199    /// Executes a zero-argument callable while holding the write lock.
200    ///
201    /// Use [`Self::call_with`] when the task needs direct mutable access to the
202    /// protected data.
203    ///
204    /// # Parameters
205    ///
206    /// * `task` - The callable task to execute after both condition checks pass.
207    ///
208    /// # Returns
209    ///
210    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
211    /// information.
212    #[inline]
213    pub fn call<C, R, E>(&self, task: C) -> ExecutionContext<R, E>
214    where
215        C: Callable<R, E>,
216        E: Display,
217    {
218        let mut task = task;
219        let result = self.execute_with_write_lock(move |_data| task.call());
220        ExecutionContext::new(result)
221    }
222
223    /// Executes a zero-argument runnable while holding the write lock.
224    ///
225    /// # Parameters
226    ///
227    /// * `task` - The runnable task to execute after both condition checks pass.
228    ///
229    /// # Returns
230    ///
231    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
232    /// information.
233    #[inline]
234    pub fn execute<Rn, E>(&self, task: Rn) -> ExecutionContext<(), E>
235    where
236        Rn: Runnable<E>,
237        E: Display,
238    {
239        let mut task = task;
240        let result = self.execute_with_write_lock(move |_data| task.run());
241        ExecutionContext::new(result)
242    }
243
244    /// Executes a callable with mutable access to the protected data.
245    ///
246    /// # Parameters
247    ///
248    /// * `task` - The callable receiving `&mut T` after both condition checks
249    ///   pass.
250    ///
251    /// # Returns
252    ///
253    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
254    /// information.
255    #[inline]
256    pub fn call_with<C, R, E>(&self, task: C) -> ExecutionContext<R, E>
257    where
258        C: CallableWith<T, R, E>,
259        E: Display,
260    {
261        let mut task = task;
262        let result = self.execute_with_write_lock(move |data| task.call_with(data));
263        ExecutionContext::new(result)
264    }
265
266    /// Executes a runnable with mutable access to the protected data.
267    ///
268    /// # Parameters
269    ///
270    /// * `task` - The runnable receiving `&mut T` after both condition checks
271    ///   pass.
272    ///
273    /// # Returns
274    ///
275    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
276    /// information.
277    #[inline]
278    pub fn execute_with<Rn, E>(&self, task: Rn) -> ExecutionContext<(), E>
279    where
280        Rn: RunnableWith<T, E>,
281        E: Display,
282    {
283        let mut task = task;
284        let result = self.execute_with_write_lock(move |data| task.run_with(data));
285        ExecutionContext::new(result)
286    }
287
288    /// Derives an executor with panic capture enabled or disabled for tester,
289    /// callbacks, and task execution.
290    ///
291    /// # Parameters
292    ///
293    /// * `catch_panics` - `true` to convert panic payloads into execution
294    ///   errors, or `false` to let panics unwind normally.
295    ///
296    /// # Returns
297    ///
298    /// A reconfigured executor with the updated panic-capture setting.
299    #[inline]
300    #[must_use = "assign or chain the returned executor"]
301    pub fn with_panic_capture(mut self, catch_panics: bool) -> Self {
302        self.catch_panics = catch_panics;
303        self
304    }
305
306    /// Returns whether panic capture is enabled.
307    ///
308    /// # Returns
309    ///
310    /// `true` when tester, prepare callback, and task panics are converted into
311    /// execution errors instead of unwinding.
312    #[inline]
313    pub fn catch_panics(&self) -> bool {
314        self.catch_panics
315    }
316
317    /// Runs the configured double-checked sequence under a write lock.
318    ///
319    /// # Parameters
320    ///
321    /// * `task` - The task to run with mutable access after both condition
322    ///   checks pass.
323    ///
324    /// # Returns
325    ///
326    /// The final execution result, including prepare finalization.
327    ///
328    /// # Errors
329    ///
330    /// Task errors are captured as [`ExecutionResult::Failed`] with
331    /// [`super::ExecutorError::TaskFailed`]. Prepare, commit, and rollback
332    /// failures are also captured in the returned [`ExecutionResult`] rather
333    /// than returned as a separate `Result`.
334    fn execute_with_write_lock<R, E, F>(&self, task: F) -> ExecutionResult<R, E>
335    where
336        E: Display,
337        F: FnOnce(&mut T) -> Result<R, E>,
338    {
339        let first_check = match self.try_run("tester", || self.tester.test()) {
340            Ok(v) => v,
341            Err(error) => {
342                return ExecutionResult::from_executor_error(ExecutorError::Panic(error));
343            }
344        };
345
346        if !first_check {
347            self.log_unmet_condition();
348            return ExecutionResult::unmet();
349        }
350
351        let prepare_completed = match self.run_prepare_action() {
352            Ok(completed) => completed,
353            Err(error) => {
354                return ExecutionResult::from_executor_error(ExecutorError::PrepareFailed(error));
355            }
356        };
357
358        let result = self.lock.write(|data| {
359            let passed = match self.try_run("tester", || self.tester.test()) {
360                Ok(v) => v,
361                Err(error) => {
362                    return ExecutionResult::from_executor_error(ExecutorError::Panic(error));
363                }
364            };
365            if !passed {
366                return ExecutionResult::unmet();
367            }
368
369            match self.try_run("task", || task(data)) {
370                Ok(Ok(value)) => ExecutionResult::success(value),
371                Ok(Err(error)) => ExecutionResult::task_failed(error),
372                Err(error) => ExecutionResult::from_executor_error(ExecutorError::Panic(error)),
373            }
374        });
375
376        if result.is_unmet() {
377            self.log_unmet_condition();
378        }
379
380        if prepare_completed {
381            self.finalize_prepare(result)
382        } else {
383            result
384        }
385    }
386
387    /// Executes the optional prepare action.
388    ///
389    /// # Returns
390    ///
391    /// `Ok(true)` if prepare exists and succeeds, `Ok(false)` if no prepare
392    /// action is configured, or `Err(message)` if prepare fails.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`CallbackError`] when the prepare action returns an error or
397    /// panics while panic capture is enabled.
398    fn run_prepare_action(&self) -> Result<bool, CallbackError> {
399        let Some(mut prepare_action) = self.prepare_action.clone() else {
400            return Ok(false);
401        };
402
403        match self.try_run("prepare", move || prepare_action.run()) {
404            Ok(Ok(_)) => Ok(true),
405            Ok(Err(error)) => {
406                self.logger.log_prepare_failed(&error);
407                Err(error)
408            }
409            Err(error) => {
410                self.logger.log_prepare_failed(&error);
411                Err(error)
412            }
413        }
414    }
415
416    /// Commits or rolls back a successfully completed prepare action.
417    ///
418    /// This method runs after the write lock has been released.
419    ///
420    /// # Parameters
421    ///
422    /// * `result` - Result produced by the condition check and task execution.
423    ///
424    /// # Returns
425    ///
426    /// `result` unchanged when no finalization action fails. Returns a failed
427    /// result when prepare commit or prepare rollback fails.
428    fn finalize_prepare<R, E>(&self, mut result: ExecutionResult<R, E>) -> ExecutionResult<R, E>
429    where
430        E: Display,
431    {
432        if result.is_success() {
433            if let Some(mut commit_prepare_action) = self.commit_prepare_action.clone() {
434                match self.try_run("prepare_commit", move || commit_prepare_action.run()) {
435                    Ok(Ok(_)) => {}
436                    Ok(Err(error)) => {
437                        self.logger.log_prepare_commit_failed(&error);
438                        result = ExecutionResult::from_executor_error(ExecutorError::PrepareCommitFailed(error));
439                    }
440                    Err(error) => {
441                        self.logger.log_prepare_commit_failed(&error);
442                        result = ExecutionResult::from_executor_error(ExecutorError::PrepareCommitFailed(error));
443                    }
444                }
445            }
446            return result;
447        }
448
449        let original = if let ExecutionResult::Failed(error) = &result {
450            original_failure_to_callback_error(error)
451        } else {
452            CallbackError::from_display("Condition not met")
453        };
454
455        if let Some(mut rollback_prepare_action) = self.rollback_prepare_action.clone() {
456            match self.try_run("prepare_rollback", move || rollback_prepare_action.run()) {
457                Ok(Ok(_)) => {}
458                Ok(Err(error)) => {
459                    self.logger.log_prepare_rollback_failed(&error);
460                    result = ExecutionResult::from_executor_error(ExecutorError::PrepareRollbackFailed {
461                        original,
462                        rollback: error,
463                    });
464                }
465                Err(error) => {
466                    self.logger.log_prepare_rollback_failed(&error);
467                    result = ExecutionResult::from_executor_error(ExecutorError::PrepareRollbackFailed {
468                        original,
469                        rollback: error,
470                    });
471                }
472            }
473        }
474        result
475    }
476
477    /// Runs a callback with optional panic capture.
478    ///
479    /// # Parameters
480    ///
481    /// * `callback_type` - Semantic label used if a captured panic is converted
482    ///   into [`CallbackError`].
483    /// * `callback` - Callback to execute.
484    ///
485    /// # Returns
486    ///
487    /// `Ok(value)` when the callback returns normally, or `Err(error)` when
488    /// panic capture is enabled and the callback panics.
489    ///
490    /// # Errors
491    ///
492    /// Returns [`CallbackError`] only when `catch_panics` is enabled and
493    /// `callback` panics.
494    fn try_run<R>(&self, callback_type: &'static str, callback: impl FnOnce() -> R) -> Result<R, CallbackError> {
495        if !self.catch_panics {
496            return Ok(callback());
497        }
498
499        match panic::catch_unwind(AssertUnwindSafe(callback)) {
500            Ok(result) => Ok(result),
501            Err(payload) => {
502                let message = panic_payload_to_message(&*payload);
503                Err(CallbackError::with_callback_type(callback_type, message))
504            }
505        }
506    }
507
508    /// Logs that the double-checked condition was not met.
509    ///
510    /// This method has no return value.
511    fn log_unmet_condition(&self) {
512        self.logger.log_unmet_condition();
513    }
514}
515
516type CallbackError = super::callback_error::CallbackError;
517
518/// Converts the original execution failure into rollback metadata.
519///
520/// # Parameters
521///
522/// * `error` - Failure that caused prepare rollback to run.
523///
524/// # Returns
525///
526/// Callback error metadata suitable for the `original` field of
527/// [`ExecutorError::PrepareRollbackFailed`].
528fn original_failure_to_callback_error<E>(error: &ExecutorError<E>) -> CallbackError
529where
530    E: Display,
531{
532    match error {
533        ExecutorError::Panic(error) => error.clone(),
534        _ => CallbackError::from_display(error),
535    }
536}
537
538/// Converts a panic payload into a stable diagnostic message.
539///
540/// # Parameters
541///
542/// * `payload` - Panic payload captured from `catch_unwind`.
543///
544/// # Returns
545///
546/// The string payload when available, or a stable generic message for
547/// non-string panic payloads.
548fn panic_payload_to_message(payload: &(dyn Any + Send)) -> String {
549    if let Some(message) = payload.downcast_ref::<&str>() {
550        (*message).to_string()
551    } else if let Some(message) = payload.downcast_ref::<String>() {
552        message.to_string()
553    } else {
554        "non-string panic payload".to_string()
555    }
556}