Skip to main content

fedimint_client/sm/
executor.rs

1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::convert::Infallible;
3use std::fmt::{Debug, Formatter};
4use std::io::{Error, Write};
5use std::mem;
6use std::sync::Arc;
7use std::time::Duration;
8
9use fedimint_client_module::sm::executor::{
10    ActiveStateKey, ContextGen, IExecutor, InactiveStateKey,
11};
12use fedimint_client_module::sm::{
13    ActiveStateMeta, ClientSMDatabaseTransaction, DynContext, DynState, InactiveStateMeta, State,
14    StateTransition, StateTransitionFunction,
15};
16use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, OperationId};
17use fedimint_core::db::{
18    AutocommitError, Database, DatabaseKeyWithNotify, DatabaseTransaction,
19    IDatabaseTransactionOpsCoreTyped,
20};
21use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
22use fedimint_core::fmt_utils::AbbreviateJson;
23use fedimint_core::module::registry::ModuleDecoderRegistry;
24use fedimint_core::task::TaskGroup;
25use fedimint_core::util::{BoxFuture, FmtCompactAnyhow as _};
26use fedimint_core::{apply, async_trait_maybe_send};
27use fedimint_eventlog::{DBTransactionEventLogExt as _, Event, EventKind, EventPersistence};
28use fedimint_logging::LOG_CLIENT_REACTOR;
29use futures::future::{self, select_all};
30use futures::stream::{FuturesUnordered, StreamExt};
31use serde::{Deserialize, Serialize};
32use tokio::select;
33use tokio::sync::{mpsc, oneshot, watch};
34use tracing::{Instrument, debug, error, info, trace, warn};
35
36use crate::sm::notifier::Notifier;
37use crate::{AddStateMachinesError, AddStateMachinesResult, DynGlobalClientContext};
38
39/// After how many attempts a DB transaction is aborted with an error
40const MAX_DB_ATTEMPTS: Option<usize> = Some(100);
41
42/// Prefixes for executor DB entries
43pub(crate) enum ExecutorDbPrefixes {
44    /// See [`ActiveStateKey`]
45    ActiveStates = 0xa1,
46    /// See [`InactiveStateKey`]
47    InactiveStates = 0xa2,
48}
49
50#[derive(Serialize, Deserialize)]
51pub struct StateMachineUpdated {
52    operation_id: OperationId,
53    started: bool,
54    terminal: bool,
55    module_id: ModuleInstanceId,
56}
57
58impl Event for StateMachineUpdated {
59    const MODULE: Option<fedimint_core::core::ModuleKind> = None;
60    const KIND: EventKind = EventKind::from_static("sm-updated");
61    const PERSISTENCE: EventPersistence = EventPersistence::Trimable;
62}
63
64/// Executor that drives forward state machines under its management.
65///
66/// Each state transition is atomic and supposed to be idempotent such that a
67/// stop/crash of the executor at any point can be recovered from on restart.
68/// The executor is aware of the concept of Fedimint modules and can give state
69/// machines a different [execution context](crate::module::sm::Context)
70/// depending on the owning module, making it very flexible.
71#[derive(Clone, Debug)]
72pub struct Executor {
73    inner: Arc<ExecutorInner>,
74}
75
76struct ExecutorInner {
77    db: Database,
78    state: std::sync::RwLock<ExecutorState>,
79    module_contexts: BTreeMap<ModuleInstanceId, DynContext>,
80    valid_module_ids: BTreeSet<ModuleInstanceId>,
81    notifier: Notifier,
82    /// Any time executor should notice state machine update (e.g. because it
83    /// was created), it's must be sent through this channel for it to notice.
84    sm_update_tx: mpsc::UnboundedSender<DynState>,
85    client_task_group: TaskGroup,
86    log_ordering_wakeup_tx: watch::Sender<()>,
87}
88
89enum ExecutorState {
90    Unstarted {
91        sm_update_rx: mpsc::UnboundedReceiver<DynState>,
92    },
93    Running {
94        context_gen: ContextGen,
95        shutdown_sender: oneshot::Sender<()>,
96    },
97    Stopped,
98}
99
100impl ExecutorState {
101    /// Starts the executor, returning a receiver that will be signalled when
102    /// the executor is stopped and a receiver for state machine updates.
103    /// Returns `None` if the executor has already been started and/or stopped.
104    fn start(
105        &mut self,
106        context: ContextGen,
107    ) -> Option<(oneshot::Receiver<()>, mpsc::UnboundedReceiver<DynState>)> {
108        let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::<()>();
109
110        let previous_state = mem::replace(
111            self,
112            ExecutorState::Running {
113                context_gen: context,
114                shutdown_sender,
115            },
116        );
117
118        match previous_state {
119            ExecutorState::Unstarted { sm_update_rx } => Some((shutdown_receiver, sm_update_rx)),
120            _ => {
121                // Replace the previous state, undoing the `mem::replace` above.
122                *self = previous_state;
123
124                debug!(target: LOG_CLIENT_REACTOR, "Executor already started, ignoring start request");
125                None
126            }
127        }
128    }
129
130    /// Stops the executor, returning `Some(())` if the executor was running and
131    /// `None` if it was in any other state.
132    fn stop(&mut self) -> Option<()> {
133        let previous_state = mem::replace(self, ExecutorState::Stopped);
134
135        match previous_state {
136            ExecutorState::Running {
137                shutdown_sender, ..
138            } => {
139                if shutdown_sender.send(()).is_err() {
140                    warn!(target: LOG_CLIENT_REACTOR, "Failed to send shutdown signal to executor, already dead?");
141                }
142                Some(())
143            }
144            _ => {
145                // Replace the previous state, undoing the `mem::replace` above.
146                *self = previous_state;
147
148                debug!(target: LOG_CLIENT_REACTOR, "Executor not running, ignoring stop request");
149                None
150            }
151        }
152    }
153
154    fn gen_context(&self, state: &DynState) -> Option<DynGlobalClientContext> {
155        let ExecutorState::Running { context_gen, .. } = self else {
156            return None;
157        };
158        Some(context_gen(
159            state.module_instance_id(),
160            state.operation_id(),
161        ))
162    }
163}
164
165/// Builder to which module clients can be attached and used to build an
166/// [`Executor`] supporting these.
167#[derive(Debug, Default)]
168pub struct ExecutorBuilder {
169    module_contexts: BTreeMap<ModuleInstanceId, DynContext>,
170    valid_module_ids: BTreeSet<ModuleInstanceId>,
171}
172
173impl Executor {
174    /// Creates an [`ExecutorBuilder`]
175    pub fn builder() -> ExecutorBuilder {
176        ExecutorBuilder::default()
177    }
178
179    pub async fn get_active_states(&self) -> Vec<(DynState, ActiveStateMeta)> {
180        self.inner.get_active_states().await
181    }
182
183    /// Adds a number of state machines to the executor atomically. They will be
184    /// driven to completion automatically in the background.
185    ///
186    /// **Attention**: do not use before background task is started!
187    // TODO: remove warning once finality is an inherent state attribute
188    pub async fn add_state_machines(
189        &self,
190        states: Vec<DynState>,
191    ) -> Result<(), AddStateMachinesError> {
192        self.inner
193            .db
194            .autocommit(
195                |dbtx, _| Box::pin(self.add_state_machines_dbtx(dbtx, states.clone())),
196                MAX_DB_ATTEMPTS,
197            )
198            .await
199            .map_err(|e| match e {
200                AutocommitError::CommitFailed { last_error, .. } => {
201                    AddStateMachinesError::Database(last_error)
202                }
203                AutocommitError::ClosureError { error, .. } => error,
204            })?;
205
206        // TODO: notify subscribers to state changes?
207
208        Ok(())
209    }
210
211    /// Adds a number of state machines to the executor atomically with other DB
212    /// changes is `dbtx`. See [`Executor::add_state_machines`] for more
213    /// details.
214    ///
215    /// ## Panics
216    /// If called before background task is started using
217    /// [`Executor::start_executor`]!
218    // TODO: remove warning once finality is an inherent state attribute
219    pub async fn add_state_machines_dbtx(
220        &self,
221        dbtx: &mut DatabaseTransaction<'_>,
222        states: Vec<DynState>,
223    ) -> AddStateMachinesResult {
224        for state in states {
225            if !self
226                .inner
227                .valid_module_ids
228                .contains(&state.module_instance_id())
229            {
230                return Err(AddStateMachinesError::UnknownModule {
231                    module_instance_id: state.module_instance_id(),
232                });
233            }
234
235            let is_active_state = dbtx
236                .get_value(&ActiveStateKeyDb(ActiveStateKey::from_state(state.clone())))
237                .await
238                .is_some();
239            let is_inactive_state = dbtx
240                .get_value(&InactiveStateKeyDb(InactiveStateKey::from_state(
241                    state.clone(),
242                )))
243                .await
244                .is_some();
245
246            if is_active_state || is_inactive_state {
247                return Err(AddStateMachinesError::StateAlreadyExists);
248            }
249
250            // In case of recovery functions, the module itself is not yet initialized,
251            // so we can't check if the state is terminal. However the
252            // [`Self::get_transitions_for`] function will double check and
253            // deactivate any terminal states that would slip past this check.
254            if let Some(module_context) =
255                self.inner.module_contexts.get(&state.module_instance_id())
256            {
257                match self
258                    .inner
259                    .state
260                    .read()
261                    .expect("locking failed")
262                    .gen_context(&state)
263                {
264                    Some(context) => {
265                        if state.is_terminal(module_context, &context) {
266                            return Err(AddStateMachinesError::StateAlreadyTerminal);
267                        }
268                    }
269                    _ => {
270                        warn!(target: LOG_CLIENT_REACTOR, "Executor should be running at this point");
271                    }
272                }
273            }
274
275            dbtx.insert_new_entry(
276                &ActiveStateKeyDb(ActiveStateKey::from_state(state.clone())),
277                &ActiveStateMeta::default(),
278            )
279            .await;
280
281            let operation_id = state.operation_id();
282            self.inner
283                .log_event_dbtx(
284                    dbtx,
285                    StateMachineUpdated {
286                        operation_id,
287                        started: true,
288                        terminal: false,
289                        module_id: state.module_instance_id(),
290                    },
291                )
292                .await;
293
294            let notify_sender = self.inner.notifier.sender();
295            let sm_updates_tx = self.inner.sm_update_tx.clone();
296            dbtx.on_commit(move || {
297                notify_sender.notify(state.clone());
298                let _ = sm_updates_tx.send(state);
299            });
300        }
301
302        Ok(())
303    }
304
305    /// **Mostly used for testing**
306    ///
307    /// Check if state exists in the database as part of an actively running
308    /// state machine.
309    pub async fn contains_active_state<S: State>(
310        &self,
311        instance: ModuleInstanceId,
312        state: S,
313    ) -> bool {
314        let state = DynState::from_typed(instance, state);
315        self.inner
316            .get_active_states()
317            .await
318            .into_iter()
319            .any(|(s, _)| s == state)
320    }
321
322    // TODO: unify querying fns
323    /// **Mostly used for testing**
324    ///
325    /// Check if state exists in the database as inactive. If the state is
326    /// terminal it means the corresponding state machine finished its
327    /// execution. If the state is non-terminal it means the state machine was
328    /// in that state at some point but moved on since then.
329    pub async fn contains_inactive_state<S: State>(
330        &self,
331        instance: ModuleInstanceId,
332        state: S,
333    ) -> bool {
334        let state = DynState::from_typed(instance, state);
335        self.inner
336            .get_inactive_states()
337            .await
338            .into_iter()
339            .any(|(s, _)| s == state)
340    }
341
342    pub async fn await_inactive_state(&self, state: DynState) -> InactiveStateMeta {
343        self.inner
344            .db
345            .wait_key_exists(&InactiveStateKeyDb(InactiveStateKey::from_state(state)))
346            .await
347    }
348
349    pub async fn await_active_state(&self, state: DynState) -> ActiveStateMeta {
350        self.inner
351            .db
352            .wait_key_exists(&ActiveStateKeyDb(ActiveStateKey::from_state(state)))
353            .await
354    }
355
356    /// Only meant for debug tooling
357    pub async fn get_operation_states(
358        &self,
359        operation_id: OperationId,
360    ) -> (
361        Vec<(DynState, ActiveStateMeta)>,
362        Vec<(DynState, InactiveStateMeta)>,
363    ) {
364        let mut dbtx = self.inner.db.begin_transaction_nc().await;
365        let active_states: Vec<_> = dbtx
366            .find_by_prefix(&ActiveOperationStateKeyPrefix { operation_id })
367            .await
368            .map(|(active_key, active_meta)| (active_key.0.state, active_meta))
369            .collect()
370            .await;
371        let inactive_states: Vec<_> = dbtx
372            .find_by_prefix(&InactiveOperationStateKeyPrefix { operation_id })
373            .await
374            .map(|(active_key, inactive_meta)| (active_key.0.state, inactive_meta))
375            .collect()
376            .await;
377
378        (active_states, inactive_states)
379    }
380
381    /// Starts the background thread that runs the state machines. This cannot
382    /// be done when building the executor since some global contexts in turn
383    /// may depend on the executor, forming a cyclic dependency.
384    ///
385    /// ## Panics
386    /// If called more than once.
387    pub fn start_executor(&self, context_gen: ContextGen, client_span: tracing::Span) {
388        let Some((shutdown_receiver, sm_update_rx)) = self
389            .inner
390            .state
391            .write()
392            .expect("locking can't fail")
393            .start(context_gen.clone())
394        else {
395            panic!("start_executor was called previously");
396        };
397
398        let task_runner_inner = self.inner.clone();
399        let _handle = self.inner.client_task_group.spawn_with_span(
400            client_span,
401            "sm-executor",
402            |task_handle| async move {
403                let executor_runner = task_runner_inner.run(context_gen, sm_update_rx);
404                let task_group_shutdown_rx = task_handle.make_shutdown_rx();
405                select! {
406                    () = task_group_shutdown_rx => {
407                        debug!(
408                            target: LOG_CLIENT_REACTOR,
409                            "Shutting down state machine executor runner due to task group shutdown signal"
410                        );
411                    },
412                    shutdown_happened_sender = shutdown_receiver => {
413                        match shutdown_happened_sender {
414                            Ok(()) => {
415                                debug!(
416                                    target: LOG_CLIENT_REACTOR,
417                                    "Shutting down state machine executor runner due to explicit shutdown signal"
418                                );
419                            },
420                            Err(_) => {
421                                warn!(
422                                    target: LOG_CLIENT_REACTOR,
423                                    "Shutting down state machine executor runner because the shutdown signal channel was closed (the executor object was dropped)"
424                                );
425                            }
426                        }
427                    },
428                    () = executor_runner => {
429                        error!(target: LOG_CLIENT_REACTOR, "State machine executor runner exited unexpectedly!");
430                    },
431                };
432            },
433        );
434    }
435
436    /// Stops the background task that runs the state machines.
437    ///
438    /// If a shutdown signal was sent it returns a [`oneshot::Receiver`] that
439    /// will be signalled when the main loop of the background task has
440    /// exited. This can be useful to block until the executor has stopped
441    /// to avoid errors due to the async runtime shutting down while the
442    /// task is still running.
443    ///
444    /// If no shutdown signal was sent it returns `None`. This can happen if
445    /// `stop_executor` is called multiple times.
446    ///
447    /// ## Panics
448    /// If called in parallel with [`start_executor`](Self::start_executor).
449    pub fn stop_executor(&self) -> Option<()> {
450        self.inner.stop_executor()
451    }
452
453    /// Returns a reference to the [`Notifier`] that can be used to subscribe to
454    /// state transitions
455    pub fn notifier(&self) -> &Notifier {
456        &self.inner.notifier
457    }
458}
459
460impl Drop for ExecutorInner {
461    fn drop(&mut self) {
462        self.stop_executor();
463    }
464}
465
466struct TransitionForActiveState {
467    outcome: serde_json::Value,
468    state: DynState,
469    meta: ActiveStateMeta,
470    transition_fn: StateTransitionFunction<DynState>,
471}
472
473impl ExecutorInner {
474    async fn run(
475        &self,
476        global_context_gen: ContextGen,
477        sm_update_rx: tokio::sync::mpsc::UnboundedReceiver<DynState>,
478    ) {
479        debug!(target: LOG_CLIENT_REACTOR, "Starting state machine executor task");
480        if let Err(err) = self
481            .run_state_machines_executor_inner(global_context_gen, sm_update_rx)
482            .await
483        {
484            warn!(
485                target: LOG_CLIENT_REACTOR,
486                err = %err.fmt_compact_anyhow(),
487                "An unexpected error occurred during a state transition"
488            );
489        }
490    }
491
492    async fn get_transition_for(
493        &self,
494        state: &DynState,
495        meta: ActiveStateMeta,
496        global_context_gen: &ContextGen,
497    ) -> Vec<BoxFuture<'static, TransitionForActiveState>> {
498        let module_instance = state.module_instance_id();
499        let context = &self
500            .module_contexts
501            .get(&module_instance)
502            .expect("Unknown module");
503        let transitions = state
504            .transitions(
505                context,
506                &global_context_gen(module_instance, state.operation_id()),
507            )
508            .into_iter()
509            .map(|transition| {
510                let state = state.clone();
511                let f: BoxFuture<TransitionForActiveState> = Box::pin(async move {
512                    let StateTransition {
513                        trigger,
514                        transition,
515                    } = transition;
516                    TransitionForActiveState {
517                        outcome: trigger.await,
518                        state,
519                        transition_fn: transition,
520                        meta,
521                    }
522                });
523                f
524            })
525            .collect::<Vec<_>>();
526        if transitions.is_empty() {
527            // In certain cases a terminal (no transitions) state could get here due to
528            // module bug. Inactivate it to prevent accumulation of such states.
529            // See [`Self::add_state_machines_dbtx`].
530            warn!(
531                target: LOG_CLIENT_REACTOR,
532                module_id = module_instance, "A terminal state where only active states are expected. Please report this bug upstream."
533            );
534            self.db
535                .autocommit::<_, _, anyhow::Error>(
536                    |dbtx, _| {
537                        Box::pin(async {
538                            let k = InactiveStateKey::from_state(state.clone());
539                            let v = ActiveStateMeta::default().into_inactive();
540                            dbtx.remove_entry(&ActiveStateKeyDb(ActiveStateKey::from_state(
541                                state.clone(),
542                            )))
543                            .await;
544                            dbtx.insert_entry(&InactiveStateKeyDb(k), &v).await;
545                            Ok(())
546                        })
547                    },
548                    None,
549                )
550                .await
551                .expect("Autocommit here can't fail");
552        }
553
554        transitions
555    }
556
557    async fn run_state_machines_executor_inner(
558        &self,
559        global_context_gen: ContextGen,
560        mut sm_update_rx: tokio::sync::mpsc::UnboundedReceiver<DynState>,
561    ) -> anyhow::Result<()> {
562        /// All futures in the executor resolve to this type, so the handling
563        /// code can tell them apart.
564        enum ExecutorLoopEvent {
565            /// Notification about `DynState` arrived and should be handled,
566            /// usually added to the list of pending futures.
567            New { state: DynState },
568            /// One of trigger futures of a state machine finished and
569            /// returned transition function to run
570            Triggered(TransitionForActiveState),
571            /// The state machine did not need to run, so it was canceled
572            Invalid { state: DynState },
573            /// Transition function and all the accounting around it are done
574            Completed {
575                state: DynState,
576                outcome: ActiveOrInactiveState,
577            },
578            /// New job receiver disconnected, that can only mean termination
579            Disconnected,
580        }
581
582        let active_states = self.get_active_states().await;
583        debug!(
584            target: LOG_CLIENT_REACTOR,
585            total = active_states.len(),
586            "Starting active state machines",
587        );
588        for (state, meta) in active_states {
589            trace!(target: LOG_CLIENT_REACTOR, ?state, ?meta, "Starting active state");
590
591            let age = fedimint_core::time::now()
592                .duration_since(meta.created_at)
593                .unwrap_or_default();
594            if age > Duration::from_secs(7 * 24 * 3600) {
595                warn!(
596                    target: LOG_CLIENT_REACTOR,
597                    operation_id = %state.operation_id().fmt_short(),
598                    module_instance = %state.module_instance_id(),
599                    age_days = age.as_secs() / 86400,
600                    "Active state machine has been running for over a week, possibly stuck",
601                );
602            }
603            self.sm_update_tx
604                .send(state)
605                .expect("Must be able to send state machine to own opened channel");
606        }
607
608        // Keeps track of things already running, so we can deduplicate, just
609        // in case.
610        let mut currently_running_sms = HashSet::<DynState>::new();
611        // All things happening in parallel go into here
612        // NOTE: `FuturesUnordered` is a footgun: when it's not being polled
613        // (e.g. we picked an event and are awaiting on something to process it),
614        // nothing inside `futures` will be making progress, which in extreme cases
615        // could lead to hangs. For this reason we try really hard in the code here,
616        // to pick an event from `futures` and spawn a new task, avoiding any `await`,
617        // just so we can get back to `futures.next()` ASAP.
618        let mut futures: FuturesUnordered<BoxFuture<'_, ExecutorLoopEvent>> =
619            FuturesUnordered::new();
620
621        loop {
622            let event = tokio::select! {
623                new = sm_update_rx.recv() => {
624                    match new { Some(new) => {
625                        ExecutorLoopEvent::New {
626                            state: new,
627                        }
628                    } _ => {
629                        ExecutorLoopEvent::Disconnected
630                    }}
631                },
632
633                event = futures.next(), if !futures.is_empty() => event.expect("we only .next() if there are pending futures"),
634            };
635
636            // main reactor loop: wait for next thing that completed, react (possibly adding
637            // more things to `futures`)
638            match event {
639                ExecutorLoopEvent::New { state } => {
640                    if currently_running_sms.contains(&state) {
641                        warn!(target: LOG_CLIENT_REACTOR, operation_id = %state.operation_id().fmt_short(), "Received a state machine that is already running. Ignoring");
642                        continue;
643                    }
644                    currently_running_sms.insert(state.clone());
645                    let futures_len = futures.len();
646                    let global_context_gen = &global_context_gen;
647                    trace!(target: LOG_CLIENT_REACTOR, state = ?state, "Started new active state machine, details.");
648                    futures.push(Box::pin(async move {
649                        let Some(meta) = self.get_active_state(&state).await else {
650                            warn!(target: LOG_CLIENT_REACTOR, operation_id = %state.operation_id().fmt_short(), "Couldn't look up received state machine. Ignoring.");
651                            return ExecutorLoopEvent::Invalid { state: state.clone() };
652                        };
653
654                        let transitions = self
655                            .get_transition_for(&state, meta, global_context_gen)
656                            .await;
657                        if transitions.is_empty() {
658                            warn!(target: LOG_CLIENT_REACTOR, operation_id = %state.operation_id().fmt_short(), "Received an active state that doesn't produce any transitions. Ignoring.");
659                            return ExecutorLoopEvent::Invalid { state: state.clone() };
660                        }
661                        let transitions_num = transitions.len();
662
663                        debug!(target: LOG_CLIENT_REACTOR, operation_id = %state.operation_id().fmt_short(), total = futures_len + 1, transitions_num, "New active state machine.");
664
665                        let (first_completed_result, _index, _unused_transitions) =
666                            select_all(transitions).await;
667                        ExecutorLoopEvent::Triggered(first_completed_result)
668                    }));
669                }
670                ExecutorLoopEvent::Triggered(TransitionForActiveState {
671                    outcome,
672                    state,
673                    meta,
674                    transition_fn,
675                }) => {
676                    debug!(
677                        target: LOG_CLIENT_REACTOR,
678                        operation_id = %state.operation_id().fmt_short(),
679                        "Triggered state transition",
680                    );
681                    let span = tracing::debug_span!(
682                        target: LOG_CLIENT_REACTOR,
683                        "sm_transition",
684                        operation_id = %state.operation_id().fmt_short()
685                    );
686                    // Perform the transition as another future, so transitions can happen in
687                    // parallel.
688                    // Database write conflicts might be happening quite often here,
689                    // but transaction functions are supposed to be idempotent anyway,
690                    // so it seems like a good stress-test in the worst case.
691                    futures.push({
692                        let sm_update_tx = self.sm_update_tx.clone();
693                        let db = self.db.clone();
694                        let notifier = self.notifier.clone();
695                        let module_contexts = self.module_contexts.clone();
696                        let global_context_gen = global_context_gen.clone();
697                        Box::pin(
698                            async move {
699                                debug!(
700                                    target: LOG_CLIENT_REACTOR,
701                                    "Executing state transition",
702                                );
703                                trace!(
704                                    target: LOG_CLIENT_REACTOR,
705                                    ?state,
706                                    outcome = ?AbbreviateJson(&outcome),
707                                    "Executing state transition (details)",
708                                );
709
710                                let module_contexts = &module_contexts;
711                                let global_context_gen = &global_context_gen;
712
713                                let outcome = db
714                                    .autocommit::<'_, '_, _, _, Infallible>(
715                                        |dbtx, _| {
716                                            let state = state.clone();
717                                            let state_module_instance_id = state.module_instance_id();
718                                            let transition_fn = transition_fn.clone();
719                                            let transition_outcome = outcome.clone();
720                                            Box::pin(async move {
721                                                let new_state = transition_fn(
722                                                    &mut ClientSMDatabaseTransaction::new(
723                                                        &mut dbtx.to_ref(),
724                                                        state.module_instance_id(),
725                                                    ),
726                                                    transition_outcome.clone(),
727                                                    state.clone(),
728                                                )
729                                                .await;
730                                                dbtx.remove_entry(&ActiveStateKeyDb(ActiveStateKey::from_state(
731                                                    state.clone(),
732                                                )))
733                                                .await;
734                                                dbtx.insert_entry(
735                                                    &InactiveStateKeyDb(InactiveStateKey::from_state(state.clone())),
736                                                    &meta.into_inactive(),
737                                                )
738                                                .await;
739
740                                                let context = &module_contexts
741                                                    .get(&state.module_instance_id())
742                                                    .expect("Unknown module");
743
744                                                let operation_id = state.operation_id();
745                                                let global_context = global_context_gen(
746                                                    state.module_instance_id(),
747                                                    operation_id,
748                                                );
749
750                                                let is_terminal = new_state.is_terminal(context, &global_context);
751
752                                                self.log_event_dbtx(dbtx,
753                                                    StateMachineUpdated{
754                                                        started: false,
755                                                        operation_id,
756                                                        module_id: state_module_instance_id,
757                                                        terminal: is_terminal,
758                                                    }
759                                                ).await;
760
761                                                if is_terminal {
762                                                    let k = InactiveStateKey::from_state(
763                                                        new_state.clone(),
764                                                    );
765                                                    let v = ActiveStateMeta::default().into_inactive();
766                                                    dbtx.insert_entry(&InactiveStateKeyDb(k), &v).await;
767                                                    Ok(ActiveOrInactiveState::Inactive {
768                                                        dyn_state: new_state,
769                                                    })
770                                                } else {
771                                                    let k = ActiveStateKey::from_state(
772                                                        new_state.clone(),
773                                                    );
774                                                    let v = ActiveStateMeta::default();
775                                                    dbtx.insert_entry(&ActiveStateKeyDb(k), &v).await;
776                                                    Ok(ActiveOrInactiveState::Active {
777                                                        dyn_state: new_state,
778                                                        meta: v,
779                                                    })
780                                                }
781                                            })
782                                        },
783                                        None,
784                                    )
785                                    .await
786                                    .expect("autocommit should keep trying to commit (max_attempt: None) and body doesn't return errors");
787
788                                debug!(
789                                    target: LOG_CLIENT_REACTOR,
790                                    terminal = !outcome.is_active(),
791                                    ?outcome,
792                                    "State transition complete",
793                                );
794
795                                match &outcome {
796                                    ActiveOrInactiveState::Active { dyn_state, meta: _ } => {
797                                        sm_update_tx
798                                            .send(dyn_state.clone())
799                                            .expect("can't fail: we are the receiving end");
800                                        notifier.notify(dyn_state.clone());
801                                    }
802                                    ActiveOrInactiveState::Inactive { dyn_state } => {
803                                        notifier.notify(dyn_state.clone());
804                                    }
805                                }
806                                ExecutorLoopEvent::Completed { state, outcome }
807                            }
808                            .instrument(span),
809                        )
810                    });
811                }
812                ExecutorLoopEvent::Invalid { state } => {
813                    trace!(
814                        target: LOG_CLIENT_REACTOR,
815                        operation_id = %state.operation_id().fmt_short(), total = futures.len(),
816                        "State invalid"
817                    );
818                    assert!(
819                        currently_running_sms.remove(&state),
820                        "State must have been recorded"
821                    );
822                }
823
824                ExecutorLoopEvent::Completed { state, outcome } => {
825                    assert!(
826                        currently_running_sms.remove(&state),
827                        "State must have been recorded"
828                    );
829                    debug!(
830                        target: LOG_CLIENT_REACTOR,
831                        operation_id = %state.operation_id().fmt_short(),
832                        outcome_active = outcome.is_active(),
833                        total = futures.len(),
834                        "State transition complete"
835                    );
836                    trace!(
837                        target: LOG_CLIENT_REACTOR,
838                        ?outcome,
839                        operation_id = %state.operation_id().fmt_short(), total = futures.len(),
840                        "State transition complete"
841                    );
842                }
843                ExecutorLoopEvent::Disconnected => {
844                    break;
845                }
846            }
847        }
848
849        info!(target: LOG_CLIENT_REACTOR, "Terminated.");
850        Ok(())
851    }
852
853    async fn get_active_states(&self) -> Vec<(DynState, ActiveStateMeta)> {
854        self.db
855            .begin_transaction_nc()
856            .await
857            .find_by_prefix(&ActiveStateKeyPrefix)
858            .await
859            // ignore states from modules that are not initialized yet
860            .filter(|(state, _)| {
861                future::ready(
862                    self.module_contexts
863                        .contains_key(&state.0.state.module_instance_id()),
864                )
865            })
866            .map(|(state, meta)| (state.0.state, meta))
867            .collect::<Vec<_>>()
868            .await
869    }
870
871    async fn get_active_state(&self, state: &DynState) -> Option<ActiveStateMeta> {
872        // ignore states from modules that are not initialized yet
873        if !self
874            .module_contexts
875            .contains_key(&state.module_instance_id())
876        {
877            return None;
878        }
879        self.db
880            .begin_transaction_nc()
881            .await
882            .get_value(&ActiveStateKeyDb(ActiveStateKey::from_state(state.clone())))
883            .await
884    }
885
886    async fn get_inactive_states(&self) -> Vec<(DynState, InactiveStateMeta)> {
887        self.db
888            .begin_transaction_nc()
889            .await
890            .find_by_prefix(&InactiveStateKeyPrefix)
891            .await
892            // ignore states from modules that are not initialized yet
893            .filter(|(state, _)| {
894                future::ready(
895                    self.module_contexts
896                        .contains_key(&state.0.state.module_instance_id()),
897                )
898            })
899            .map(|(state, meta)| (state.0.state, meta))
900            .collect::<Vec<_>>()
901            .await
902    }
903
904    pub async fn log_event_dbtx<E, Cap>(&self, dbtx: &mut DatabaseTransaction<'_, Cap>, event: E)
905    where
906        E: Event + Send,
907        Cap: Send,
908    {
909        dbtx.log_event(self.log_ordering_wakeup_tx.clone(), None, event)
910            .await;
911    }
912}
913
914impl ExecutorInner {
915    /// See [`Executor::stop_executor`].
916    fn stop_executor(&self) -> Option<()> {
917        // This runs from destructors, which must never panic, so recover from a lock
918        // poisoned by a panic elsewhere. `ExecutorState` is always left in a coherent
919        // variant, so the poison can be ignored safely.
920        let mut state = self
921            .state
922            .write()
923            .unwrap_or_else(std::sync::PoisonError::into_inner);
924
925        state.stop()
926    }
927}
928
929impl Debug for ExecutorInner {
930    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
931        writeln!(f, "ExecutorInner {{}}")
932    }
933}
934
935impl ExecutorBuilder {
936    /// Allow executor being built to run state machines associated with the
937    /// supplied module
938    pub fn with_module<C>(&mut self, instance_id: ModuleInstanceId, context: C)
939    where
940        C: IntoDynInstance<DynType = DynContext>,
941    {
942        self.with_module_dyn(context.into_dyn(instance_id));
943    }
944
945    /// Allow executor being built to run state machines associated with the
946    /// supplied module
947    pub fn with_module_dyn(&mut self, context: DynContext) {
948        self.valid_module_ids.insert(context.module_instance_id());
949
950        if self
951            .module_contexts
952            .insert(context.module_instance_id(), context)
953            .is_some()
954        {
955            panic!("Tried to add two modules with the same instance id!");
956        }
957    }
958
959    /// Allow executor to build state machines associated with the module id,
960    /// for which the module itself might not be available yet (otherwise it
961    /// would be registered with `[Self::with_module_dyn]`).
962    pub fn with_valid_module_id(&mut self, module_id: ModuleInstanceId) {
963        self.valid_module_ids.insert(module_id);
964    }
965
966    /// Build [`Executor`] and spawn background task in `tasks` executing active
967    /// state machines. The supplied database `db` must support isolation, so
968    /// cannot be an isolated DB instance itself.
969    pub fn build(
970        self,
971        db: Database,
972        notifier: Notifier,
973        client_task_group: TaskGroup,
974        log_ordering_wakeup_tx: watch::Sender<()>,
975    ) -> Executor {
976        let (sm_update_tx, sm_update_rx) = tokio::sync::mpsc::unbounded_channel();
977
978        let inner = Arc::new(ExecutorInner {
979            db,
980            log_ordering_wakeup_tx,
981            state: std::sync::RwLock::new(ExecutorState::Unstarted { sm_update_rx }),
982            module_contexts: self.module_contexts,
983            valid_module_ids: self.valid_module_ids,
984            notifier,
985            sm_update_tx,
986            client_task_group,
987        });
988
989        debug!(
990            target: LOG_CLIENT_REACTOR,
991            instances = ?inner.module_contexts.keys().copied().collect::<Vec<_>>(),
992            "Initialized state machine executor with module instances"
993        );
994        Executor { inner }
995    }
996}
997#[derive(Debug)]
998pub struct ActiveOperationStateKeyPrefix {
999    pub operation_id: OperationId,
1000}
1001
1002impl Encodable for ActiveOperationStateKeyPrefix {
1003    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
1004        self.operation_id.consensus_encode(writer)
1005    }
1006}
1007
1008impl ::fedimint_core::db::DatabaseLookup for ActiveOperationStateKeyPrefix {
1009    type Record = ActiveStateKeyDb;
1010}
1011
1012#[derive(Debug)]
1013pub(crate) struct ActiveModuleOperationStateKeyPrefix {
1014    pub operation_id: OperationId,
1015    pub module_instance: ModuleInstanceId,
1016}
1017
1018impl Encodable for ActiveModuleOperationStateKeyPrefix {
1019    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
1020        self.operation_id.consensus_encode(writer)?;
1021        self.module_instance.consensus_encode(writer)?;
1022        Ok(())
1023    }
1024}
1025
1026impl ::fedimint_core::db::DatabaseLookup for ActiveModuleOperationStateKeyPrefix {
1027    type Record = ActiveStateKeyDb;
1028}
1029
1030#[derive(Debug)]
1031pub struct ActiveStateKeyPrefix;
1032
1033impl Encodable for ActiveStateKeyPrefix {
1034    fn consensus_encode<W: Write>(&self, _writer: &mut W) -> Result<(), Error> {
1035        Ok(())
1036    }
1037}
1038
1039#[derive(Encodable, Decodable, Debug)]
1040pub struct ActiveStateKeyDb(pub fedimint_client_module::sm::executor::ActiveStateKey);
1041
1042impl ::fedimint_core::db::DatabaseRecord for ActiveStateKeyDb {
1043    const DB_PREFIX: u8 = ExecutorDbPrefixes::ActiveStates as u8;
1044    const NOTIFY_ON_MODIFY: bool = true;
1045    type Key = Self;
1046    type Value = ActiveStateMeta;
1047}
1048
1049impl DatabaseKeyWithNotify for ActiveStateKeyDb {}
1050
1051impl ::fedimint_core::db::DatabaseLookup for ActiveStateKeyPrefix {
1052    type Record = ActiveStateKeyDb;
1053}
1054
1055#[derive(Debug, Encodable, Decodable)]
1056pub struct ActiveStateKeyPrefixBytes;
1057
1058impl ::fedimint_core::db::DatabaseRecord for ActiveStateKeyBytes {
1059    const DB_PREFIX: u8 = ExecutorDbPrefixes::ActiveStates as u8;
1060    const NOTIFY_ON_MODIFY: bool = false;
1061    type Key = Self;
1062    type Value = ActiveStateMeta;
1063}
1064
1065impl ::fedimint_core::db::DatabaseLookup for ActiveStateKeyPrefixBytes {
1066    type Record = ActiveStateKeyBytes;
1067}
1068
1069#[derive(Encodable, Decodable, Debug)]
1070pub struct InactiveStateKeyDb(pub fedimint_client_module::sm::executor::InactiveStateKey);
1071
1072#[derive(Debug)]
1073pub struct InactiveStateKeyBytes {
1074    pub operation_id: OperationId,
1075    pub module_instance_id: ModuleInstanceId,
1076    pub state: Vec<u8>,
1077}
1078
1079impl Encodable for InactiveStateKeyBytes {
1080    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
1081        self.operation_id.consensus_encode(writer)?;
1082        writer.write_all(self.state.as_slice())?;
1083        Ok(())
1084    }
1085}
1086
1087impl Decodable for InactiveStateKeyBytes {
1088    fn consensus_decode_partial<R: std::io::Read>(
1089        reader: &mut R,
1090        modules: &ModuleDecoderRegistry,
1091    ) -> Result<Self, DecodeError> {
1092        let operation_id = OperationId::consensus_decode_partial(reader, modules)?;
1093        let module_instance_id = ModuleInstanceId::consensus_decode_partial(reader, modules)?;
1094        let mut bytes = Vec::new();
1095        reader.read_to_end(&mut bytes)?;
1096
1097        let mut instance_bytes = ModuleInstanceId::consensus_encode_to_vec(&module_instance_id);
1098        instance_bytes.append(&mut bytes);
1099
1100        Ok(InactiveStateKeyBytes {
1101            operation_id,
1102            module_instance_id,
1103            state: instance_bytes,
1104        })
1105    }
1106}
1107
1108#[derive(Debug)]
1109pub struct InactiveOperationStateKeyPrefix {
1110    pub operation_id: OperationId,
1111}
1112
1113impl Encodable for InactiveOperationStateKeyPrefix {
1114    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
1115        self.operation_id.consensus_encode(writer)
1116    }
1117}
1118
1119impl ::fedimint_core::db::DatabaseLookup for InactiveOperationStateKeyPrefix {
1120    type Record = InactiveStateKeyDb;
1121}
1122
1123#[derive(Debug)]
1124pub(crate) struct InactiveModuleOperationStateKeyPrefix {
1125    pub operation_id: OperationId,
1126    pub module_instance: ModuleInstanceId,
1127}
1128
1129impl Encodable for InactiveModuleOperationStateKeyPrefix {
1130    fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
1131        self.operation_id.consensus_encode(writer)?;
1132        self.module_instance.consensus_encode(writer)?;
1133        Ok(())
1134    }
1135}
1136
1137impl ::fedimint_core::db::DatabaseLookup for InactiveModuleOperationStateKeyPrefix {
1138    type Record = InactiveStateKeyDb;
1139}
1140
1141#[derive(Debug, Clone)]
1142pub struct InactiveStateKeyPrefix;
1143
1144impl Encodable for InactiveStateKeyPrefix {
1145    fn consensus_encode<W: Write>(&self, _writer: &mut W) -> Result<(), Error> {
1146        Ok(())
1147    }
1148}
1149
1150#[derive(Debug, Encodable, Decodable)]
1151pub struct InactiveStateKeyPrefixBytes;
1152
1153impl ::fedimint_core::db::DatabaseRecord for InactiveStateKeyBytes {
1154    const DB_PREFIX: u8 = ExecutorDbPrefixes::InactiveStates as u8;
1155    const NOTIFY_ON_MODIFY: bool = false;
1156    type Key = Self;
1157    type Value = InactiveStateMeta;
1158}
1159
1160impl ::fedimint_core::db::DatabaseLookup for InactiveStateKeyPrefixBytes {
1161    type Record = InactiveStateKeyBytes;
1162}
1163
1164#[derive(Debug)]
1165pub struct ActiveStateKeyBytes {
1166    pub operation_id: OperationId,
1167    pub module_instance_id: ModuleInstanceId,
1168    pub state: Vec<u8>,
1169}
1170
1171impl Encodable for ActiveStateKeyBytes {
1172    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
1173        self.operation_id.consensus_encode(writer)?;
1174        writer.write_all(self.state.as_slice())?;
1175        Ok(())
1176    }
1177}
1178
1179impl Decodable for ActiveStateKeyBytes {
1180    fn consensus_decode_partial<R: std::io::Read>(
1181        reader: &mut R,
1182        modules: &ModuleDecoderRegistry,
1183    ) -> Result<Self, DecodeError> {
1184        let operation_id = OperationId::consensus_decode_partial(reader, modules)?;
1185        let module_instance_id = ModuleInstanceId::consensus_decode_partial(reader, modules)?;
1186        let mut bytes = Vec::new();
1187        reader.read_to_end(&mut bytes)?;
1188
1189        let mut instance_bytes = ModuleInstanceId::consensus_encode_to_vec(&module_instance_id);
1190        instance_bytes.append(&mut bytes);
1191
1192        Ok(ActiveStateKeyBytes {
1193            operation_id,
1194            module_instance_id,
1195            state: instance_bytes,
1196        })
1197    }
1198}
1199impl ::fedimint_core::db::DatabaseRecord for InactiveStateKeyDb {
1200    const DB_PREFIX: u8 = ExecutorDbPrefixes::InactiveStates as u8;
1201    const NOTIFY_ON_MODIFY: bool = true;
1202    type Key = Self;
1203    type Value = InactiveStateMeta;
1204}
1205
1206impl DatabaseKeyWithNotify for InactiveStateKeyDb {}
1207
1208impl ::fedimint_core::db::DatabaseLookup for InactiveStateKeyPrefix {
1209    type Record = InactiveStateKeyDb;
1210}
1211
1212#[derive(Debug)]
1213enum ActiveOrInactiveState {
1214    Active {
1215        dyn_state: DynState,
1216        #[allow(dead_code)] // currently not printed anywhere, but useful in the db
1217        meta: ActiveStateMeta,
1218    },
1219    Inactive {
1220        dyn_state: DynState,
1221    },
1222}
1223
1224impl ActiveOrInactiveState {
1225    fn is_active(&self) -> bool {
1226        match self {
1227            ActiveOrInactiveState::Active { .. } => true,
1228            ActiveOrInactiveState::Inactive { .. } => false,
1229        }
1230    }
1231}
1232
1233#[apply(async_trait_maybe_send!)]
1234impl IExecutor for Executor {
1235    async fn get_active_states(&self) -> Vec<(DynState, ActiveStateMeta)> {
1236        Self::get_active_states(self).await
1237    }
1238
1239    async fn add_state_machines_dbtx(
1240        &self,
1241        dbtx: &mut DatabaseTransaction<'_>,
1242        states: Vec<DynState>,
1243    ) -> AddStateMachinesResult {
1244        Self::add_state_machines_dbtx(self, dbtx, states).await
1245    }
1246}
1247
1248#[cfg(test)]
1249mod tests;