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.