Disclaimer: These are planning documents. The functionalities described here may be unimplemented, partially implemented, or implemented differently than the original design.

StoreCommand for Scabbard v0.7

Summary

This feature aims to add a component which can make updates to the database atomically. Commands to update the database can be created by any part of the system without it having to have access to the underlying database. This component is intended to be used by any part of the system which needs to make database updates.

Guide-level explanation

StoreCommand

StoreCommand is a trait for defining commands which make updates to a database. StoreCommands can be created by any part of the system to update tables in the database. A StoreCommand should have all of the information necessary for making a database update this includes the values being added and the receiver. The receiver is the object which implements the method called in the execute function. In the diagram below the ExampleStoreCommand’s receiver is the store_factory, this store factory produces the store that is updated by the ExampleStoreCommand.

StoreCommandExecutor

The StoreCommandExecutor provides an API for executing StoreCommands. The StoreCommandExecutor has access to the underlying database and provides the database connection when calling execute on the StoreCommands. The StoreCommandExecutor has its own execute method which takes a list of commands that implement the StoreCommand trait and executes them within the context of a single transaction. The StoreCommandExecutor has no knowledge of how the StoreCommands are implemented, it only knows their interface.

Reference-level explanation

The command module will provide a trait for defining StoreCommands. The execute function will take a generic argument, conn. conn is a connection to the database being updated by the execute function in implementations of the StoreCommand trait. The StoreCommandExecutor trait will also be provided in the command module. This trait defines the command invoker and provides an API for executing StoreCommands. The StoreCommandExecutor trait can be implemented for various database backends.

StoreCommand

/// Trait for defining a command
///
/// A command will contain information that is to be applied to a database
pub trait StoreCommand {
    type Context;

    fn execute(&self, conn: &Self::Context) -> Result<(), InternalError>;
}

StoreCommandExecutor

/// Provides an API for executing `StoreCommand`s
pub trait StoreCommandExecutor {
    type Context;

    fn execute<C: StoreCommand<Context = Self::Context>>(
        &self,
        store_commands: Vec<C>,
    ) -> Result<(), InternalError>;
}

DieselStoreCommandExecutor

A diesel powered struct that implements the StoreCommandExecutor trait for SQLite and PostgreSQL backends.

/// A `StoreCommandExecutor`, powered by [`Diesel`](https://crates.io/crates/diesel).
pub struct DieselStoreCommandExecutor<C: diesel::Connection + 'static> {
    conn: ConnectionPool<C>,
}

impl<C: diesel::Connection> DieselStoreCommandExecutor<C> {
    /// Creates a new `DieselStoreCommandExecutor`.
    ///
    /// # Arguments
    ///
    ///  * `conn`: connection pool for the database
    pub fn new(conn: Pool<ConnectionManager<C>>) -> Self {
        DieselStoreCommandExecutor { conn: conn.into() }
    }

    /// Create a new `DieselStoreCommandExecutor` with write exclusivity enabled.
    ///
    /// Write exclusivity is enforced by providing a connection pool that is wrapped in a
    /// [`RwLock`]. This ensures that there may be only one writer, but many readers.
    ///
    /// # Arguments
    ///
    ///  * `conn`: read-write lock-guarded connection pool for the database
    pub fn new_with_write_exclusivity(
        conn: Arc<RwLock<Pool<ConnectionManager<C>>>>
    ) -> Self {
        Self { conn: conn.into() }
    }
}

impl StoreCommandExecutor for DieselStoreCommandExecutor<PgConnection> {
    type Context = PgConnection;

    fn execute<C: StoreCommand<Context = Self::Context>>(
        &self,
        store_commands: Vec<C>,
    ) -> Result<(), InternalError> {
        self.conn.execute_write(|conn| {
            conn.transaction::<(), InternalError, _>(|| {
                for cmd in store_commands {
                    cmd.execute(conn)?;
                }
                Ok(())
            })
        })
    }
}

impl StoreCommandExecutor for DieselStoreCommandExecutor<SqliteConnection> {
    type Context = SqliteConnection;

    fn execute<C: StoreCommand<Context = Self::Context>>(
        &self,
        store_commands: Vec<C>,
    ) -> Result<(), InternalError> {
        self.conn.execute_write(|conn| {
            conn.transaction::<(), InternalError, _>(|| {
                for cmd in store_commands {
                    cmd.execute(conn)?;
                }
                Ok(())
            })
        })
    }
}

StoreCommand Example

The following is an example StoreCommand which operates on the ExampleStore. This store command adds a string, value, to a table in the ExampleStore.

/// Stores the value that will be set and the store factory for the store
/// being updated
pub struct SetValueExampleStoreCommand<C> {
    value: String,
    store_factory: Arc<dyn ExampleStoreFactory<Connection = C>>,
}

impl<C> SetValueExampleStoreCommand<C> {
    /// Creates a new `SetValueExampleStoreCommand`
    ///
    /// # Arguments
    ///
    /// * `value` - the value that will be added to the database
    /// * `store_factory` - a factory that can be used to retrieve an instance
    ///    of the `ExampleStore`
    pub fn new(
        value: String,
        store_factory: Arc<dyn ExampleStoreFactory<Connection = C>>,
    ) -> Self {
        SetValueExampleStoreCommand {
            value,
            store_factory,
        }
    }
}

impl<C> StoreCommand for SetValueExampleStoreCommand<C> {
    type Context = C;
    type Error = InternalError;

    /// Gets an instance of the `ExampleStore` from the store factory and uses
    /// its `set_value` method to update a specific table in the database
    ///
    /// # Arguments
    ///
    /// * `conn` - the transaction context
    fn execute(self, conn: &Self::Context) -> Result<(), Self::Error> {
        self.store_factory
            .get_store(&conn)
            .set_value(self.value.clone())
            .map_err(|e| InternalError::from_source(Box::new(e)))
    }
}

Drawbacks

In the current store pattern used throughout the system, a store has a connection pool and each transaction is executed in a different transaction context. The addition of this component will require that all stores that are used in StoreCommands be updated so that they can operate within the context of a transaction.

Rationale and alternatives

Another option would be to provide an instance of a store to each component that needs to make database updates. This practice is already used in various parts of the system. The problem with this approach is in the way that stores are currently implemented, each transaction is executed in a separate context which prevents multiple database updates executed together from being atomic.

Prior art

This component follows the command design pattern. This pattern encapsulates a request as an object which is passed to an invoker to call execute on the command. An invoker knows how to execute a given command but has no knowledge of what it does.