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}