feat(postgres): Introduce chained error variants

Introduces error variants for external crate errors and internal
errors.

Additional context can be provided at sites where errors occur using a
simple `.context()` call.
This commit is contained in:
Vincent Ambo 2018-12-13 14:40:59 +01:00
parent 43f71ae82f
commit 68060fea13
2 changed files with 91 additions and 33 deletions

View file

@ -2,8 +2,68 @@
//! occur while dealing with persisted state machines. //! occur while dealing with persisted state machines.
use std::result; use std::result;
use std::fmt::Display;
use uuid::Uuid;
// errors to chain:
use serde_json::Error as JsonError;
use postgres::Error as PgError;
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { SomeError } pub struct Error {
pub kind: ErrorKind,
pub context: Option<String>,
}
#[derive(Debug)]
pub enum ErrorKind {
/// Errors occuring during JSON serialization of FSM types.
Serialization(String),
/// Errors occuring during communication with the database.
Database(String),
/// State machine could not be found.
FSMNotFound(Uuid),
/// Action could not be found.
ActionNotFound(Uuid),
}
impl <E: Into<ErrorKind>> From<E> for Error {
fn from(err: E) -> Error {
Error {
kind: err.into(),
context: None,
}
}
}
impl From<JsonError> for ErrorKind {
fn from(err: JsonError) -> ErrorKind {
ErrorKind::Serialization(err.to_string())
}
}
impl From<PgError> for ErrorKind {
fn from(err: PgError) -> ErrorKind {
ErrorKind::Database(err.to_string())
}
}
/// Helper trait that makes it possible to supply contextual
/// information with an error.
pub trait ResultExt<T> {
fn context<C: Display>(self, ctx: C) -> Result<T>;
}
impl <T, E: Into<Error>> ResultExt<T> for result::Result<T, E> {
fn context<C: Display>(self, ctx: C) -> Result<T> {
self.map_err(|err| Error {
context: Some(format!("{}", ctx)),
.. err.into()
})
}
}

View file

@ -17,8 +17,9 @@ extern crate uuid;
#[cfg(test)] extern crate finito_door; #[cfg(test)] extern crate finito_door;
mod error; mod error;
pub use error::{Result, Error}; pub use error::{Result, Error, ErrorKind};
use error::ResultExt;
use chrono::prelude::{DateTime, Utc}; use chrono::prelude::{DateTime, Utc};
use finito::{FSM, FSMBackend}; use finito::{FSM, FSMBackend};
use postgres::{Connection, GenericConnection}; use postgres::{Connection, GenericConnection};
@ -118,9 +119,9 @@ impl <State: 'static> FSMBackend<State> for FinitoPostgres<State> {
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let fsm = S::FSM_NAME.to_string(); let fsm = S::FSM_NAME.to_string();
let state = serde_json::to_value(initial).expect("TODO"); let state = serde_json::to_value(initial).context("failed to serialise FSM")?;
self.conn.execute(query, &[&id, &fsm, &state]).expect("TODO"); self.conn.execute(query, &[&id, &fsm, &state]).context("failed to insert FSM")?;
return Ok(id); return Ok(id);
@ -146,7 +147,7 @@ impl <State: 'static> FSMBackend<State> for FinitoPostgres<State> {
S::State: From<&'a State>, S::State: From<&'a State>,
S::Event: Serialize + DeserializeOwned, S::Event: Serialize + DeserializeOwned,
S::Action: Serialize + DeserializeOwned { S::Action: Serialize + DeserializeOwned {
let tx = self.conn.transaction().expect("TODO"); let tx = self.conn.transaction().context("could not begin transaction")?;
let state = get_machine_internal(&tx, key, true)?; let state = get_machine_internal(&tx, key, true)?;
// Advancing the FSM consumes the event, so it is persisted first: // Advancing the FSM consumes the event, so it is persisted first:
@ -163,8 +164,8 @@ impl <State: 'static> FSMBackend<State> for FinitoPostgres<State> {
} }
// And finally the state is updated: // And finally the state is updated:
update_state(&tx, key, &new_state).expect("TODO"); update_state(&tx, key, &new_state)?;
tx.commit().expect("TODO"); tx.commit().context("could not commit transaction")?;
self.run_actions::<S>(key, action_ids); self.run_actions::<S>(key, action_ids);
@ -211,9 +212,9 @@ pub fn insert_machine<C, S>(conn: &C, initial: S) -> Result<Uuid> where
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let fsm = S::FSM_NAME.to_string(); let fsm = S::FSM_NAME.to_string();
let state = serde_json::to_value(initial).expect("TODO"); let state = serde_json::to_value(initial).context("failed to serialize FSM")?;
conn.execute(query, &[&id, &fsm, &state]).expect("TODO"); conn.execute(query, &[&id, &fsm, &state])?;
return Ok(id); return Ok(id);
} }
@ -233,9 +234,10 @@ where
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let fsm = S::FSM_NAME.to_string(); let fsm = S::FSM_NAME.to_string();
let event_value = serde_json::to_value(event).expect("TODO"); let event_value = serde_json::to_value(event)
.context("failed to serialize event")?;
conn.execute(query, &[&id, &fsm, &fsm_id, &event_value]).expect("TODO"); conn.execute(query, &[&id, &fsm, &fsm_id, &event_value])?;
return Ok(id) return Ok(id)
} }
@ -254,12 +256,13 @@ fn insert_action<C, S>(conn: &C,
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let fsm = S::FSM_NAME.to_string(); let fsm = S::FSM_NAME.to_string();
let action_value = serde_json::to_value(action).expect("TODO"); let action_value = serde_json::to_value(action)
.context("failed to serialize action")?;
conn.execute( conn.execute(
query, query,
&[&id, &fsm, &fsm_id, &event_id, &action_value, &ActionStatus::Pending] &[&id, &fsm, &fsm_id, &event_id, &action_value, &ActionStatus::Pending]
).expect("TODO"); )?;
return Ok(id) return Ok(id)
} }
@ -274,13 +277,11 @@ fn update_state<C, S>(conn: &C,
UPDATE machines SET state = $1 WHERE id = $2 UPDATE machines SET state = $1 WHERE id = $2
"#; "#;
let state_value = serde_json::to_value(state).expect("TODO"); let state_value = serde_json::to_value(state).context("failed to serialize FSM")?;
let res_count = conn.execute(query, &[&state_value, &fsm_id]) let res_count = conn.execute(query, &[&state_value, &fsm_id])?;
.expect("TODO");
if res_count != 1 { if res_count != 1 {
// TODO: not found error! Err(ErrorKind::FSMNotFound(fsm_id).into())
unimplemented!()
} else { } else {
Ok(()) Ok(())
} }
@ -307,13 +308,12 @@ fn get_machine_internal<C, S>(conn: &C,
SELECT state FROM machines WHERE id = $1 SELECT state FROM machines WHERE id = $1
"#); "#);
let rows = conn.query(&query, &[&id]).expect("TODO"); let rows = conn.query(&query, &[&id]).context("failed to retrieve FSM")?;
if let Some(row) = rows.into_iter().next() { if let Some(row) = rows.into_iter().next() {
Ok(serde_json::from_value(row.get(0)).expect("TODO")) Ok(serde_json::from_value(row.get(0)).context("failed to deserialize FSM")?)
} else { } else {
// TODO: return appropriate not found error Err(ErrorKind::FSMNotFound(id).into())
Err(Error::SomeError)
} }
} }
@ -328,14 +328,14 @@ fn get_action<C, S>(conn: &C, id: Uuid) -> Result<(ActionStatus, S::Action)> whe
WHERE id = $1 AND fsm = $2 WHERE id = $1 AND fsm = $2
"#); "#);
let rows = conn.query(&query, &[&id, &S::FSM_NAME]).expect("TODO"); let rows = conn.query(&query, &[&id, &S::FSM_NAME])?;
if let Some(row) = rows.into_iter().next() { if let Some(row) = rows.into_iter().next() {
let action = serde_json::from_value(row.get(1)).expect("TODO"); let action = serde_json::from_value(row.get(1))
.context("failed to deserialize FSM action")?;
Ok((row.get(0), action)) Ok((row.get(0), action))
} else { } else {
// TODO: return appropriate not found error Err(ErrorKind::ActionNotFound(id).into())
Err(Error::SomeError)
} }
} }
@ -352,15 +352,13 @@ fn update_action_status<C, S>(conn: &C,
WHERE id = $3 AND fsm = $4 WHERE id = $3 AND fsm = $4
"#; "#;
let result = conn.execute(&query, &[&status, &error, &id, &S::FSM_NAME]) let result = conn.execute(&query, &[&status, &error, &id, &S::FSM_NAME])?;
.expect("TODO");
if result != 1 { if result != 1 {
// TODO: Fail in the most gruesome way! Err(ErrorKind::ActionNotFound(id).into())
unimplemented!() } else {
Ok(())
} }
Ok(())
} }
/// Execute a single action in case it is pending or retryable. Holds /// Execute a single action in case it is pending or retryable. Holds
@ -408,6 +406,6 @@ fn run_action<S>(tx: Transaction, id: Uuid, state: &S::State, _fsm: PhantomData<
}, },
}; };
tx.commit().expect("TODO"); tx.commit().context("failed to commit transaction")?;
Ok(result) Ok(result)
} }