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 crate::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    pub fn builder() -> ExecutorBuilder {
165        ExecutorBuilder::default()
166    }
167}
168
169impl<L, T> DoubleCheckedLockExecutor<L, T>
170where
171    L: Lock<T>,
172{
173    /// Assembles an executor from the ready builder state.
174    ///
175    /// # Parameters
176    ///
177    /// * `builder` - Ready builder carrying the lock, tester, logger, and
178    ///   prepare lifecycle callbacks.
179    ///
180    /// # Returns
181    ///
182    /// A reusable executor containing the supplied builder state.
183    #[inline]
184    pub fn new(builder: ExecutorReadyBuilder<L, T>) -> Self {
185        Self {
186            lock: builder.lock,
187            tester: builder.tester,
188            logger: builder.logger,
189            prepare_action: builder.prepare_action,
190            rollback_prepare_action: builder.rollback_prepare_action,
191            commit_prepare_action: builder.commit_prepare_action,
192            catch_panics: builder.catch_panics,
193            _phantom: builder._phantom,
194        }
195    }
196
197    /// Executes a zero-argument callable while holding the write lock.
198    ///
199    /// Use [`Self::call_with`] when the task needs direct mutable access to the
200    /// protected data.
201    ///
202    /// # Parameters
203    ///
204    /// * `task` - The callable task to execute after both condition checks pass.
205    ///
206    /// # Returns
207    ///
208    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
209    /// information.
210    #[inline]
211    pub fn call<C, R, E>(&self, task: C) -> ExecutionContext<R, E>
212    where
213        C: Callable<R, E>,
214        E: Display,
215    {
216        let mut task = task;
217        let result = self.execute_with_write_lock(move |_data| task.call());
218        ExecutionContext::new(result)
219    }
220
221    /// Executes a zero-argument runnable while holding the write lock.
222    ///
223    /// # Parameters
224    ///
225    /// * `task` - The runnable task to execute after both condition checks pass.
226    ///
227    /// # Returns
228    ///
229    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
230    /// information.
231    #[inline]
232    pub fn execute<Rn, E>(&self, task: Rn) -> ExecutionContext<(), E>
233    where
234        Rn: Runnable<E>,
235        E: Display,
236    {
237        let mut task = task;
238        let result = self.execute_with_write_lock(move |_data| task.run());
239        ExecutionContext::new(result)
240    }
241
242    /// Executes a callable with mutable access to the protected data.
243    ///
244    /// # Parameters
245    ///
246    /// * `task` - The callable receiving `&mut T` after both condition checks
247    ///   pass.
248    ///
249    /// # Returns
250    ///
251    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
252    /// information.
253    #[inline]
254    pub fn call_with<C, R, E>(&self, task: C) -> ExecutionContext<R, E>
255    where
256        C: CallableWith<T, R, E>,
257        E: Display,
258    {
259        let mut task = task;
260        let result = self.execute_with_write_lock(move |data| task.call_with(data));
261        ExecutionContext::new(result)
262    }
263
264    /// Executes a runnable with mutable access to the protected data.
265    ///
266    /// # Parameters
267    ///
268    /// * `task` - The runnable receiving `&mut T` after both condition checks
269    ///   pass.
270    ///
271    /// # Returns
272    ///
273    /// An [`ExecutionContext`] containing success, unmet-condition, or failure
274    /// information.
275    #[inline]
276    pub fn execute_with<Rn, E>(&self, task: Rn) -> ExecutionContext<(), E>
277    where
278        Rn: RunnableWith<T, E>,
279        E: Display,
280    {
281        let mut task = task;
282        let result = self.execute_with_write_lock(move |data| task.run_with(data));
283        ExecutionContext::new(result)
284    }
285
286    /// Derives an executor with panic capture enabled or disabled for tester,
287    /// callbacks, and task execution.
288    ///
289    /// # Parameters
290    ///
291    /// * `catch_panics` - `true` to convert panic payloads into execution
292    ///   errors, or `false` to let panics unwind normally.
293    ///
294    /// # Returns
295    ///
296    /// A reconfigured executor with the updated panic-capture setting.
297    #[inline]
298    #[must_use = "assign or chain the returned executor"]
299    pub fn with_panic_capture(mut self, catch_panics: bool) -> Self {
300        self.catch_panics = catch_panics;
301        self
302    }
303
304    /// Returns whether panic capture is enabled.
305    ///
306    /// # Returns
307    ///
308    /// `true` when tester, prepare callback, and task panics are converted into
309    /// execution errors instead of unwinding.
310    #[inline]
311    pub fn catch_panics(&self) -> bool {
312        self.catch_panics
313    }
314
315    /// Runs the configured double-checked sequence under a write lock.
316    ///
317    /// # Parameters
318    ///
319    /// * `task` - The task to run with mutable access after both condition
320    ///   checks pass.
321    ///
322    /// # Returns
323    ///
324    /// The final execution result, including prepare finalization.
325    ///
326    /// # Errors
327    ///
328    /// Task errors are captured as [`ExecutionResult::Failed`] with
329    /// [`super::ExecutorError::TaskFailed`]. Prepare, commit, and rollback
330    /// failures are also captured in the returned [`ExecutionResult`] rather
331    /// than returned as a separate `Result`.
332    fn execute_with_write_lock<R, E, F>(&self, task: F) -> ExecutionResult<R, E>
333    where
334        E: Display,
335        F: FnOnce(&mut T) -> Result<R, E>,
336    {
337        let first_check = match self.try_run("tester", || self.tester.test()) {
338            Ok(v) => v,
339            Err(error) => {
340                return ExecutionResult::from_executor_error(ExecutorError::Panic(error));
341            }
342        };
343
344        if !first_check {
345            self.log_unmet_condition();
346            return ExecutionResult::unmet();
347        }
348
349        let prepare_completed = match self.run_prepare_action() {
350            Ok(completed) => completed,
351            Err(error) => {
352                return ExecutionResult::from_executor_error(ExecutorError::PrepareFailed(error));
353            }
354        };
355
356        let result = self.lock.write(|data| {
357            let passed = match self.try_run("tester", || self.tester.test()) {
358                Ok(v) => v,
359                Err(error) => {
360                    return ExecutionResult::from_executor_error(ExecutorError::Panic(error));
361                }
362            };
363            if !passed {
364                return ExecutionResult::unmet();
365            }
366
367            match self.try_run("task", || task(data)) {
368                Ok(Ok(value)) => ExecutionResult::success(value),
369                Ok(Err(error)) => ExecutionResult::task_failed(error),
370                Err(error) => ExecutionResult::from_executor_error(ExecutorError::Panic(error)),
371            }
372        });
373
374        if result.is_unmet() {
375            self.log_unmet_condition();
376        }
377
378        if prepare_completed {
379            self.finalize_prepare(result)
380        } else {
381            result
382        }
383    }
384
385    /// Executes the optional prepare action.
386    ///
387    /// # Returns
388    ///
389    /// `Ok(true)` if prepare exists and succeeds, `Ok(false)` if no prepare
390    /// action is configured, or `Err(message)` if prepare fails.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`CallbackError`] when the prepare action returns an error or
395    /// panics while panic capture is enabled.
396    fn run_prepare_action(&self) -> Result<bool, CallbackError> {
397        let Some(mut prepare_action) = self.prepare_action.clone() else {
398            return Ok(false);
399        };
400
401        match self.try_run("prepare", move || prepare_action.run()) {
402            Ok(Ok(_)) => Ok(true),
403            Ok(Err(error)) => {
404                self.logger.log_prepare_failed(&error);
405                Err(error)
406            }
407            Err(error) => {
408                self.logger.log_prepare_failed(&error);
409                Err(error)
410            }
411        }
412    }
413
414    /// Commits or rolls back a successfully completed prepare action.
415    ///
416    /// This method runs after the write lock has been released.
417    ///
418    /// # Parameters
419    ///
420    /// * `result` - Result produced by the condition check and task execution.
421    ///
422    /// # Returns
423    ///
424    /// `result` unchanged when no finalization action fails. Returns a failed
425    /// result when prepare commit or prepare rollback fails.
426    fn finalize_prepare<R, E>(&self, mut result: ExecutionResult<R, E>) -> ExecutionResult<R, E>
427    where
428        E: Display,
429    {
430        if result.is_success() {
431            if let Some(mut commit_prepare_action) = self.commit_prepare_action.clone() {
432                match self.try_run("prepare_commit", move || commit_prepare_action.run()) {
433                    Ok(Ok(_)) => {}
434                    Ok(Err(error)) => {
435                        self.logger.log_prepare_commit_failed(&error);
436                        result = ExecutionResult::from_executor_error(
437                            ExecutorError::PrepareCommitFailed(error),
438                        );
439                    }
440                    Err(error) => {
441                        self.logger.log_prepare_commit_failed(&error);
442                        result = ExecutionResult::from_executor_error(
443                            ExecutorError::PrepareCommitFailed(error),
444                        );
445                    }
446                }
447            }
448            return result;
449        }
450
451        let original = if let ExecutionResult::Failed(error) = &result {
452            original_failure_to_callback_error(error)
453        } else {
454            CallbackError::from_display("Condition not met")
455        };
456
457        if let Some(mut rollback_prepare_action) = self.rollback_prepare_action.clone() {
458            match self.try_run("prepare_rollback", move || rollback_prepare_action.run()) {
459                Ok(Ok(_)) => {}
460                Ok(Err(error)) => {
461                    self.logger.log_prepare_rollback_failed(&error);
462                    result = ExecutionResult::from_executor_error(
463                        ExecutorError::PrepareRollbackFailed {
464                            original,
465                            rollback: error,
466                        },
467                    );
468                }
469                Err(error) => {
470                    self.logger.log_prepare_rollback_failed(&error);
471                    result = ExecutionResult::from_executor_error(
472                        ExecutorError::PrepareRollbackFailed {
473                            original,
474                            rollback: error,
475                        },
476                    );
477                }
478            }
479        }
480        result
481    }
482
483    /// Runs a callback with optional panic capture.
484    ///
485    /// # Parameters
486    ///
487    /// * `callback_type` - Semantic label used if a captured panic is converted
488    ///   into [`CallbackError`].
489    /// * `callback` - Callback to execute.
490    ///
491    /// # Returns
492    ///
493    /// `Ok(value)` when the callback returns normally, or `Err(error)` when
494    /// panic capture is enabled and the callback panics.
495    ///
496    /// # Errors
497    ///
498    /// Returns [`CallbackError`] only when `catch_panics` is enabled and
499    /// `callback` panics.
500    fn try_run<R>(
501        &self,
502        callback_type: &'static str,
503        callback: impl FnOnce() -> R,
504    ) -> Result<R, CallbackError> {
505        if !self.catch_panics {
506            return Ok(callback());
507        }
508
509        match panic::catch_unwind(AssertUnwindSafe(callback)) {
510            Ok(result) => Ok(result),
511            Err(payload) => {
512                let message = panic_payload_to_message(&*payload);
513                Err(CallbackError::with_callback_type(callback_type, message))
514            }
515        }
516    }
517
518    /// Logs that the double-checked condition was not met.
519    ///
520    /// This method has no return value.
521    fn log_unmet_condition(&self) {
522        self.logger.log_unmet_condition();
523    }
524}
525
526type CallbackError = super::callback_error::CallbackError;
527
528/// Converts the original execution failure into rollback metadata.
529///
530/// # Parameters
531///
532/// * `error` - Failure that caused prepare rollback to run.
533///
534/// # Returns
535///
536/// Callback error metadata suitable for the `original` field of
537/// [`ExecutorError::PrepareRollbackFailed`].
538fn original_failure_to_callback_error<E>(error: &ExecutorError<E>) -> CallbackError
539where
540    E: Display,
541{
542    match error {
543        ExecutorError::Panic(error) => error.clone(),
544        _ => CallbackError::from_display(error),
545    }
546}
547
548/// Converts a panic payload into a stable diagnostic message.
549///
550/// # Parameters
551///
552/// * `payload` - Panic payload captured from `catch_unwind`.
553///
554/// # Returns
555///
556/// The string payload when available, or a stable generic message for
557/// non-string panic payloads.
558fn panic_payload_to_message(payload: &(dyn Any + Send)) -> String {
559    if let Some(message) = payload.downcast_ref::<&str>() {
560        (*message).to_string()
561    } else if let Some(message) = payload.downcast_ref::<String>() {
562        message.to_string()
563    } else {
564        "non-string panic payload".to_string()
565    }
566}