Skip to main content

sabi/
lib.rs

1// Copyright (C) 2024-2026 Takayuki Sato. All Rights Reserved.
2// This program is free software under MIT License.
3// See the file LICENSE in this distribution for more details.
4
5//! This crate provides a small framework for Rust, designed to separate application logic
6//! from data access.
7//!
8//! In this framework, the logic exclusively takes a data access trait as its argument,
9//! and all necessary data access is defined by a single data access trait.
10//! Conversely, the concrete implementations of data access methods are provided as default methods
11//! of `DataAcc` derived traits, allowing for flexible grouping, often by data service.
12//!
13//! The `DataHub` bridges these two parts.
14//! It attaches all `DataAcc` derived traits, and then, using the
15//! [override_macro](https://github.com/sttk/override_macro-rust) crate, it overrides
16//! the methods of the data access trait used by the logic to point to the implementations
17//! found in the `DataAcc` derived traits.
18//! This clever use of this macro compensates for Rust's lack of native method overriding,
19//! allowing the logic to interact with data through an abstract interface.
20//!
21//! Furthermore, the `DataHub` provides transaction control for data operations performed
22//! within the logic.
23//! You can execute logic functions with transaction control using its [`DataHub::txn`] method,
24//! or without transaction control using its [`DataHub::run`] method.
25//!
26//! This framework brings clear separation and robustness to Rust application design.
27
28#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
29#![cfg_attr(docsrs, feature(doc_cfg))]
30#![allow(unused_features)]
31
32mod async_group;
33mod data_acc;
34mod data_conn;
35mod data_hub;
36mod data_src;
37mod non_null;
38mod txn_failure;
39
40use std::collections::HashMap;
41use std::sync::Arc;
42use std::{any, cell, marker, ptr, thread};
43
44pub use async_group::AsyncGroupError;
45pub use data_conn::DataConnError;
46pub use data_hub::DataHubError;
47pub use data_src::{create_static_data_src_container, setup, setup_with_order, uses, DataSrcError};
48
49#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
50#[cfg(feature = "tokio")]
51pub mod tokio;
52
53/// Represents an entry containing an error, along with its context.
54///
55/// This structure is used to aggregate errors that occur during parallel
56/// or asynchronous operations, providing the index of the operation,
57/// a descriptive name, and the error itself.
58#[derive(Debug)]
59pub struct ErrEntry {
60    /// The index of the operation or handler that generated the error.
61    pub index: usize,
62    /// A descriptive name for the operation or context.
63    pub name: Arc<str>,
64    /// The actual error that occurred.
65    pub err: errs::Err,
66}
67
68/// The structure that allows for the concurrent execution of multiple functions
69/// using `std::thread` and waits for all of them to complete.
70///
71/// Functions are added using the `add` method and are then run concurrently in separate threads.
72/// The `AsyncGroup` ensures that all tasks finish before proceeding,
73/// and can collect any errors that occur.
74pub struct AsyncGroup {
75    handlers: Vec<(usize, Arc<str>, thread::JoinHandle<errs::Result<()>>)>,
76    _index: usize,
77    _name: Arc<str>,
78}
79
80/// The trait that abstracts a connection per session to an external data service,
81/// such as a database, file system, or messaging service.
82///
83/// Its primary purpose is to enable cohesive transaction operations across multiple
84/// external data services within a single transaction context. Implementations of this
85/// trait provide the concrete input/output operations for their respective data services.
86///
87/// Methods declared within this trait are designed to handle transactional logic.
88/// The [`AsyncGroup`] parameter in various methods allows for concurrent processing
89/// when commit or rollback operations are time-consuming.
90#[allow(unused_variables)] // rustdoc
91pub trait DataConn {
92    /// Attempts to commit the changes made within this data connection's transaction.
93    ///
94    /// This method should encapsulate the logic required to finalize the transaction
95    /// for the specific external data service.
96    ///
97    /// # Parameters
98    ///
99    /// * `ag`: A mutable reference to an [`AsyncGroup`] for potentially offloading
100    ///   time-consuming commit operations to a separate thread.
101    ///
102    /// # Returns
103    ///
104    /// * `errs::Result<()>`: `Ok(())` if the commit is successful, or an [`errs::Err`]
105    ///   if the commit fails.
106    fn commit(&mut self, ag: &mut AsyncGroup) -> errs::Result<()>;
107
108    /// This method is executed before the transaction commit process for all [`DataConn`] instances
109    /// involved in the transaction.
110    ///
111    /// This method provides a timing to execute unusual commit processes or update operations not
112    /// supported by transactions beforehand.
113    /// This allows other update operations to be rolled back if the operations in this method
114    /// fail.
115    ///
116    /// # Parameters
117    ///
118    /// * `ag`: A mutable reference to an [`AsyncGroup`]. This can be used to run the pre-commit
119    ///   asynchronously in a separate thread.
120    ///
121    /// # Returns
122    ///
123    /// * `errs::Result<()>`: `Ok(())` if pre-commit is successful, or an [`errs::Err`] if it fails.
124    fn pre_commit(&mut self, ag: &mut AsyncGroup) -> errs::Result<()> {
125        Ok(())
126    }
127
128    /// This method is executed after the transaction commit process has successfully completed
129    /// for all [`DataConn`] instances involved in the transaction.
130    ///
131    /// It provides a moment to perform follow-up actions that depend on a successful commit.
132    /// For example, after a database commit, a messaging service's [`DataConn`] might use this
133    /// method to send a "transaction completed" message.
134    ///
135    /// # Parameters
136    ///
137    /// * `ag`: A mutable reference to an [`AsyncGroup`] for potentially offloading
138    ///   concurrent post-commit operations.
139    ///
140    /// # Returns
141    ///
142    /// * `errs::Result<()>`: `Ok(())` if post-commit tasks succeed, or an [`errs::Err`] if they
143    ///   fail.
144    fn post_commit(&mut self, ag: &mut AsyncGroup) -> errs::Result<()> {
145        Ok(())
146    }
147
148    /// Returns whether the transaction on this connection has been successfully committed.
149    fn is_committed(&self) -> bool;
150
151    /// Rolls back any changes made within this data connection's transaction.
152    ///
153    /// This method undoes all operations performed since the beginning of the transaction,
154    /// restoring the data service to its state before the transaction began.
155    ///
156    /// # Parameters
157    ///
158    /// * `ag`: A mutable reference to an [`AsyncGroup`]. This can be used to run the rollback
159    ///   asynchronously in a separate thread.
160    ///
161    /// # Returns
162    ///
163    /// * `errs::Result<()>`: `Ok(())` if the rollback is successful, or an [`errs::Err`] if it
164    ///   fails.
165    fn rollback(&mut self, ag: &mut AsyncGroup) -> errs::Result<()>;
166
167    /// A lifecycle callback invoked when a transaction fails and a rollback is executed.
168    ///
169    /// This allows the data connection to handle post-failure tasks or custom logic
170    /// based on the provided transaction failure reports.
171    ///
172    /// # Parameters
173    ///
174    /// * `ag`: A mutable reference to an [`AsyncGroup`] for asynchronous task execution.
175    /// * `reports`: A slice of [`TxnFailureReport`] containing failure details for all connections.
176    #[cfg_attr(coverage_nightly, coverage(off))]
177    fn on_txn_failure(&mut self, ag: &mut AsyncGroup, reports: &[TxnFailureReport]) {}
178
179    /// Closes the connection to the external data service.
180    ///
181    /// This method should release any resources held by the data connection, ensuring
182    /// a graceful shutdown of the connection.
183    fn close(&mut self);
184}
185
186struct NoopDataConn {}
187
188#[cfg_attr(coverage_nightly, coverage(off))]
189impl DataConn for NoopDataConn {
190    fn commit(&mut self, _ag: &mut AsyncGroup) -> errs::Result<()> {
191        Ok(())
192    }
193    fn is_committed(&self) -> bool {
194        false
195    }
196    fn rollback(&mut self, _ag: &mut AsyncGroup) -> errs::Result<()> {
197        Ok(())
198    }
199    fn close(&mut self) {}
200}
201
202#[repr(C)]
203struct DataConnContainer<C = NoopDataConn>
204where
205    C: DataConn + 'static,
206{
207    drop_fn: fn(*const DataConnContainer),
208    is_fn: fn(any::TypeId) -> bool,
209    type_fn: fn() -> &'static str,
210    commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
211    pre_commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
212    post_commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
213    is_committed_fn: fn(*const DataConnContainer) -> bool,
214    rollback_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
215    on_txn_failure_fn: fn(*const DataConnContainer, &mut AsyncGroup, &[TxnFailureReport]),
216    close_fn: fn(*const DataConnContainer),
217
218    name: Arc<str>,
219    data_conn: Box<C>,
220}
221
222struct DataConnManager {
223    vec: Vec<Option<SendSyncNonNull<DataConnContainer>>>,
224    index_map: HashMap<Arc<str>, usize>,
225    committed: bool,
226}
227
228/// The trait that abstracts a data source responsible for managing connections
229/// to external data services, such as databases, file systems, or messaging services.
230///
231/// It receives configuration for connecting to an external data service and then
232/// creates and supplies [`DataConn`] instance, representing a single session connection.
233#[allow(unused_variables)] // for rustdoc
234pub trait DataSrc<C>
235where
236    C: DataConn + 'static,
237{
238    /// Performs the setup process for the data source.
239    ///
240    /// This method is responsible for establishing global connections, configuring
241    /// connection pools, or performing any necessary initializations required
242    /// before [`DataConn`] instances can be created.
243    ///
244    /// # Parameters
245    ///
246    /// * `ag`: A mutable reference to an [`AsyncGroup`]. This is used if the setup
247    ///   process is potentially time-consuming and can benefit from concurrent
248    ///   execution in a separate thread.
249    ///
250    /// # Returns
251    ///
252    /// * `errs::Result<()>`: `Ok(())` if the setup is successful, or an [`errs::Err`]
253    ///   if any part of the setup fails.
254    fn setup(&mut self, ag: &mut AsyncGroup) -> errs::Result<()>;
255
256    /// Closes the data source and releases any globally held resources.
257    ///
258    /// This method should perform cleanup operations, such as closing global connections
259    /// or shutting down connection pools, that were established during the setup process.
260    fn close(&mut self);
261
262    /// Creates a new [`DataConn`] instance which is a connection per session.
263    ///
264    /// Each call to this method should yield a distinct [`DataConn`] object tailored
265    /// for a single session's operations.
266    ///
267    /// # Returns
268    ///
269    /// * `errs::Result<Box<C>>`: `Ok(Box<C>)` containing the newly created [`DataConn`]
270    ///   if successful, or an [`errs::Err`] if the connection could not be created.
271    fn create_data_conn(&mut self) -> errs::Result<Box<C>>;
272}
273
274struct NoopDataSrc {}
275
276#[cfg_attr(coverage_nightly, coverage(off))]
277impl DataSrc<NoopDataConn> for NoopDataSrc {
278    fn setup(&mut self, _ag: &mut AsyncGroup) -> errs::Result<()> {
279        Ok(())
280    }
281    fn close(&mut self) {}
282    fn create_data_conn(&mut self) -> errs::Result<Box<NoopDataConn>> {
283        Ok(Box::new(NoopDataConn {}))
284    }
285}
286
287#[repr(C)]
288struct DataSrcContainer<S = NoopDataSrc, C = NoopDataConn>
289where
290    S: DataSrc<C>,
291    C: DataConn + 'static,
292{
293    drop_fn: fn(*const DataSrcContainer),
294    setup_fn: fn(*const DataSrcContainer, &mut AsyncGroup) -> errs::Result<()>,
295    close_fn: fn(*const DataSrcContainer),
296    create_data_conn_fn: fn(*const DataSrcContainer) -> errs::Result<Box<DataConnContainer<C>>>,
297    is_data_conn_fn: fn(any::TypeId) -> bool,
298
299    local: bool,
300    name: Arc<str>,
301    data_src: S,
302}
303
304struct DataSrcManager {
305    vec_unready: Vec<SendSyncNonNull<DataSrcContainer>>,
306    vec_ready: Vec<SendSyncNonNull<DataSrcContainer>>,
307    local: bool,
308}
309
310/// A utility struct that ensures to close and drop global data sources when it goes out of scope.
311///
312/// This struct implements the `Drop` trait, and its `drop` method handles the closing and
313/// dropping of registered global data sources.
314/// Therefore, this ensures that these operations are automatically executed at the end of
315/// the scope.
316///
317/// **NOTE:** Do not receive an instance of this struct into an anonymous variable
318/// (`let _ = ...`), because an anonymous variable is dropped immediately at that point.
319pub struct AutoShutdown {}
320
321/// The struct that acts as a central hub for data input/output operations, integrating
322/// multiple *Data* traits (which are passed to business logic functions as their arguments) with
323/// [`DataAcc`] traits (which implement default data I/O methods for external services).
324///
325/// It facilitates data access by providing [`DataConn`] objects, created from
326/// both global data sources (registered via the global [`uses!`] macro) and
327/// session-local data sources (registered via [`DataHub::uses`] method).
328///
329/// The [`DataHub`] is capable of performing aggregated transactional operations
330/// on all [`DataConn`] objects created from its registered [`DataSrc`] instances.
331pub struct DataHub {
332    local_data_src_manager: DataSrcManager,
333    data_src_map: HashMap<Arc<str>, (bool, usize)>,
334    data_conn_manager: DataConnManager,
335    fixed: bool,
336}
337
338/// This trait provides a mechanism to retrieve a mutable reference to a [`DataConn`] object
339/// by name, creating it if necessary.
340///
341/// It is typically implemented as a derived trait with default methods (using
342/// the `override_macro` crate) on [`DataHub`], allowing application logic to
343/// interact with data services through an abstract interface.
344pub trait DataAcc {
345    /// Retrieves a mutable reference to a [`DataConn`] object by name, creating it if necessary.
346    ///
347    /// This is the core method used by [`DataAcc`] implementations to obtain connections
348    /// to external data services. It first checks if a [`DataConn`] with the given name
349    /// already exists in the current session. If not, it attempts to find a
350    /// corresponding [`DataSrc`] and create a new [`DataConn`] from it.
351    ///
352    /// # Type Parameters
353    ///
354    /// * `C`: The concrete type of [`DataConn`] expected.
355    ///
356    /// # Parameters
357    ///
358    /// * `name`: The name of the data source/connection to retrieve.
359    ///
360    /// # Returns
361    ///
362    /// * `errs::Result<&mut C>`: A mutable reference to the [`DataConn`] instance if successful,
363    ///   or an [`errs::Err`] if the data source is not found, or if the retrieved/created
364    ///   [`DataConn`] cannot be cast to the specified type `C`.
365    fn get_data_conn<C: DataConn + 'static>(&mut self, name: &str) -> errs::Result<&mut C>;
366}
367
368#[doc(hidden)]
369pub struct StaticDataSrcContainer {
370    ssnnptr: SendSyncNonNull<DataSrcContainer>,
371}
372
373#[doc(hidden)]
374pub struct StaticDataSrcRegistration {
375    factory: fn() -> StaticDataSrcContainer,
376}
377
378struct SendSyncNonNull<T: Send + Sync> {
379    non_null_ptr: ptr::NonNull<T>,
380    _phantom: marker::PhantomData<cell::Cell<T>>,
381}
382
383/// Represents the cause of a transaction failure for a specific data connection.
384#[derive(Debug)]
385pub enum TxnFailureCause {
386    /// No failure occurred, and the transaction was successfully committed.
387    NoneByCommitted,
388    /// No failure occurred, but the transaction was not committed (e.g., because
389    /// another connection in the transaction failed before this one could commit).
390    NoneByUncommitted,
391    /// The logic execution or pre-commit phase of the data connection failed.
392    LogicFailure(errs::Err),
393    /// The commit phase of the data connection failed.
394    CommitFailure(errs::Err),
395    /// The post-commit phase of the data connection failed.
396    PostCommitFailure(errs::Err),
397}
398
399/// Represents the rollback status of a data connection in a failed transaction.
400#[derive(Debug)]
401pub enum TxnFailureRollback {
402    /// The rollback was executed and succeeded.
403    NoneByRolledBack,
404    /// Rollback was not executed or not applicable (e.g., because the connection
405    /// was already committed).
406    NoneByNotRolledBack,
407    /// The rollback was executed but failed.
408    RollbackFailure(errs::Err),
409}
410
411/// Represents the suggested recovery action for a data connection after a transaction failure.
412#[derive(Debug, PartialEq)]
413pub enum TxnFailureRecovery {
414    /// No recovery action is required.
415    NoActionRequired,
416    /// The transaction was successfully rolled back.
417    /// The transaction can be rerun a logic and committed again.
418    RerunLogicAndCommit,
419    /// The transaction failed to run a logic or pre commit.
420    /// After resolving the cause, the transaction can be rerun a logic and commit again.
421    ResolveCauseThenRerunLogicAndCommit,
422    /// The transaction failed to run post commit.
423    /// After resolving the cause, the transaction can be rerun post commit again.
424    ResolveCauseThenRerunPostCommit,
425    /// The rollback failed and it may be in an inconsistent state.
426    /// Resolve the cause and inconsistent state.
427    ResolveCauseAndInconsistency,
428    /// It is in impossile case under normal conditions.
429    /// Investigation of the cause is required.
430    InvestigateBecauseImpossible,
431    /// The transaction was successfully committed.
432    /// It is required to rollback manually.
433    ManualRollbackRequired,
434}
435
436/// A report detailing the transaction failure cause and rollback status
437/// for a specific data connection.
438#[derive(Debug)]
439pub struct TxnFailureReport {
440    /// The name of the data connection.
441    pub data_conn_name: Arc<str>,
442    /// The type name of the data connection.
443    pub data_conn_type: &'static str,
444    /// The cause of the transaction failure.
445    pub cause: TxnFailureCause,
446    /// The rollback status of the data connection.
447    pub rollback: TxnFailureRollback,
448}