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}