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