use std::any::Any;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(test)]
use crate::param::{AccountId, Pnl};
#[cfg(test)]
use crate::pretrade::AccountBlock;
static NEXT_MUTATION_OWNER_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg(test)]
pub(crate) struct AccountPnlReconciliation {
pub(crate) account_id: AccountId,
pub(crate) discarded_delta: Option<Pnl>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg(test)]
pub(crate) struct AccountAdjustmentRollbackReport {
pub(crate) account_blocks: Vec<AccountBlock>,
pub(crate) invalidated_account_blocks: Vec<AccountBlock>,
pub(crate) reconciliations: Vec<AccountPnlReconciliation>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MutationProvenance {
EngineOwned,
CustomPolicy,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MutationFailureScope {
Account,
Global,
}
#[derive(Default)]
#[must_use = "mutation finalizer failures must be inspected"]
pub(crate) struct MutationFailure {
details: Vec<String>,
scope: Option<MutationFailureScope>,
}
impl MutationFailure {
fn record(&mut self, provenance: MutationProvenance, details: String) {
let scope = match provenance {
MutationProvenance::EngineOwned => MutationFailureScope::Account,
MutationProvenance::CustomPolicy => MutationFailureScope::Global,
};
self.widen(scope);
self.details.push(details);
}
fn widen(&mut self, scope: MutationFailureScope) {
if scope == MutationFailureScope::Global || self.scope.is_none() {
self.scope = Some(scope);
}
}
pub(crate) fn append(&mut self, mut other: Self) {
if let Some(scope) = other.scope {
self.widen(scope);
}
self.details.append(&mut other.details);
}
pub(crate) fn scope(&self) -> Option<MutationFailureScope> {
self.scope
}
pub(crate) fn failed(&self) -> bool {
self.scope.is_some()
}
pub(crate) fn details(&self) -> String {
self.details.join("; ")
}
}
#[derive(Default)]
#[must_use = "mutation rollback failures must be inspected"]
pub(crate) struct MutationRollbackResult {
#[cfg(test)]
pub(crate) report: AccountAdjustmentRollbackReport,
failure: MutationFailure,
}
impl MutationRollbackResult {
pub(crate) fn append(&mut self, other: Self) {
#[cfg(test)]
let mut other = other;
self.failure.append(other.failure);
#[cfg(test)]
{
self.report
.account_blocks
.append(&mut other.report.account_blocks);
self.report
.invalidated_account_blocks
.append(&mut other.report.invalidated_account_blocks);
self.report
.reconciliations
.append(&mut other.report.reconciliations);
}
}
#[cfg(test)]
pub(crate) fn engine_owned_callback_failure(details: impl Into<String>) -> Self {
Self::callback_failure(MutationProvenance::EngineOwned, details.into())
}
fn callback_failure(provenance: MutationProvenance, details: String) -> Self {
let mut failure = MutationFailure::default();
failure.record(provenance, details);
Self {
failure,
#[cfg(test)]
report: AccountAdjustmentRollbackReport::default(),
}
}
#[must_use = "mutation rollback failures must arm the engine kill switch"]
pub(crate) fn failure(&self) -> &MutationFailure {
&self.failure
}
pub(crate) fn callback_failed(&self) -> bool {
self.failure.failed()
}
pub(crate) fn callback_failure_details(&self) -> String {
self.failure.details()
}
}
pub(crate) struct MutationFailureKillSwitch(Box<dyn Fn(MutationFailureScope)>);
impl MutationFailureKillSwitch {
pub(crate) fn new(arm: impl Fn(MutationFailureScope) + 'static) -> Self {
Self(Box::new(arm))
}
#[cfg(test)]
pub(crate) fn inert() -> Self {
Self::new(|_| {})
}
pub(crate) fn arm(&self, failure: &MutationFailure) {
if let Some(scope) = failure.scope() {
(self.0)(scope);
}
}
}
pub(crate) fn next_mutation_owner_id() -> u64 {
loop {
let id = NEXT_MUTATION_OWNER_ID.fetch_add(1, Ordering::Relaxed);
if id != 0 {
return id;
}
}
}
pub struct Mutation {
commit: Option<Box<dyn FnOnce() -> Result<(), String>>>,
rollback: Box<dyn FnOnce() -> MutationRollbackResult>,
lifetime_guard: Option<Box<dyn Any>>,
provenance: MutationProvenance,
}
impl Mutation {
pub fn new(commit: impl FnOnce() + 'static, rollback: impl FnOnce() + 'static) -> Self {
Self::new_infallible(MutationProvenance::CustomPolicy, commit, rollback)
}
#[doc(hidden)]
pub fn new_fallible(
commit: impl FnOnce() -> bool + 'static,
rollback: impl FnOnce() -> bool + 'static,
) -> Self {
Self::new_fallible_with_error(
move || {
commit()
.then_some(())
.ok_or_else(|| "mutation commit callback failed".to_owned())
},
move || {
rollback()
.then_some(())
.ok_or_else(|| "mutation rollback callback failed".to_owned())
},
)
}
#[doc(hidden)]
pub fn new_fallible_with_error(
commit: impl FnOnce() -> Result<(), String> + 'static,
rollback: impl FnOnce() -> Result<(), String> + 'static,
) -> Self {
Self::new_reporting_with_error(MutationProvenance::CustomPolicy, commit, move || {
match rollback() {
Ok(()) => MutationRollbackResult::default(),
Err(details) => MutationRollbackResult::callback_failure(
MutationProvenance::CustomPolicy,
details,
),
}
})
}
pub(crate) fn new_engine_owned(
commit: impl FnOnce() + 'static,
rollback: impl FnOnce() + 'static,
) -> Self {
Self::new_infallible(MutationProvenance::EngineOwned, commit, rollback)
}
pub(crate) fn new_engine_owned_with_guard<Guard>(
commit: impl FnOnce() + 'static,
rollback: impl FnOnce() + 'static,
guard: Guard,
) -> Self
where
Guard: 'static,
{
let mut mutation = Self::new_engine_owned(commit, rollback);
mutation.lifetime_guard = Some(Box::new(guard));
mutation
}
fn new_infallible(
provenance: MutationProvenance,
commit: impl FnOnce() + 'static,
rollback: impl FnOnce() + 'static,
) -> Self {
Self::new_reporting_with_error(
provenance,
move || {
commit();
Ok(())
},
move || {
rollback();
MutationRollbackResult::default()
},
)
}
pub(crate) fn new_reporting(
commit: impl FnOnce() -> bool + 'static,
rollback: impl FnOnce() -> MutationRollbackResult + 'static,
) -> Self {
Self::new_reporting_with_error(
MutationProvenance::EngineOwned,
move || {
commit()
.then_some(())
.ok_or_else(|| "mutation commit callback failed".to_owned())
},
rollback,
)
}
fn new_reporting_with_error(
provenance: MutationProvenance,
commit: impl FnOnce() -> Result<(), String> + 'static,
rollback: impl FnOnce() -> MutationRollbackResult + 'static,
) -> Self {
Self {
commit: Some(Box::new(commit)),
rollback: Box::new(rollback),
lifetime_guard: None,
provenance,
}
}
pub(crate) fn new_reporting_with_guard<Guard>(
commit: impl FnOnce() -> bool + 'static,
rollback: impl FnOnce() -> MutationRollbackResult + 'static,
guard: Guard,
) -> Self
where
Guard: 'static,
{
let mut mutation = Self::new_reporting(commit, rollback);
mutation.lifetime_guard = Some(Box::new(guard));
mutation
}
fn commit(&mut self) -> Result<(), String> {
match self.commit.take() {
Some(commit) => commit(),
None => Ok(()),
}
}
fn rollback(self) -> MutationRollbackResult {
let Self {
commit,
rollback,
lifetime_guard,
provenance: _,
} = self;
drop(commit);
let result = rollback();
drop(lifetime_guard);
result
}
}
pub struct Mutations {
mutations: Vec<Mutation>,
owner_id: u64,
}
impl Default for Mutations {
fn default() -> Self {
Self::new()
}
}
impl Mutations {
pub fn new() -> Self {
Self {
mutations: Vec::new(),
owner_id: next_mutation_owner_id(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
mutations: Vec::with_capacity(capacity),
owner_id: next_mutation_owner_id(),
}
}
pub fn push(&mut self, mutation: Mutation) {
self.mutations.push(mutation);
}
pub(crate) fn append(&mut self, mut other: Self) {
self.mutations.append(&mut other.mutations);
}
pub(crate) fn owner_id(&self) -> u64 {
self.owner_id
}
#[must_use = "mutation finalizer failures must arm the engine kill switch"]
pub(crate) fn commit_all(self) -> MutationFailure {
let mut failure = MutationFailure::default();
for mut mutation in self.mutations {
let provenance = mutation.provenance;
if let Err(details) = mutation.commit() {
failure.record(provenance, details);
}
}
failure
}
#[must_use = "mutation rollback failures must arm the engine kill switch"]
pub(crate) fn rollback_all(self) -> MutationRollbackResult {
let mut result = MutationRollbackResult::default();
for mutation in self.mutations.into_iter().rev() {
result.append(mutation.rollback());
}
result
}
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.mutations.is_empty()
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use super::{Mutation, MutationFailureScope, Mutations};
fn noop_action() {}
fn always_fails() -> bool {
false
}
fn always_succeeds() -> bool {
true
}
#[test]
fn commit_all_applies_in_registration_order() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(3);
for id in ["a", "b", "c"] {
let c = Rc::clone(&calls);
mutations.push(Mutation::new(
move || {
c.borrow_mut().push(id);
},
noop_action,
));
}
let failure = mutations.commit_all();
assert!(!failure.failed());
assert_eq!(&*calls.borrow(), &["a", "b", "c"]);
}
#[test]
fn commit_all_reports_failure_and_keeps_applying() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(2);
for (id, succeeds) in [("a", false), ("b", true)] {
let commit_calls = Rc::clone(&calls);
mutations.push(Mutation::new_fallible(
move || {
commit_calls.borrow_mut().push(id);
succeeds
},
always_succeeds,
));
}
let failure = mutations.commit_all();
assert_eq!(&*calls.borrow(), &["a", "b"]);
assert_eq!(failure.scope(), Some(MutationFailureScope::Global));
assert!(failure.details().contains("commit callback failed"));
}
#[test]
fn engine_owned_finalizer_failure_stays_account_scoped() {
let mut mutations = Mutations::with_capacity(1);
mutations.push(Mutation::new_reporting(
always_fails,
super::MutationRollbackResult::default,
));
let failure = mutations.commit_all();
assert_eq!(failure.scope(), Some(MutationFailureScope::Account));
}
#[test]
fn custom_policy_failure_widens_an_engine_owned_one() {
let mut mutations = Mutations::with_capacity(2);
mutations.push(Mutation::new_reporting(
always_fails,
super::MutationRollbackResult::default,
));
mutations.push(Mutation::new_fallible(always_fails, always_succeeds));
let failure = mutations.commit_all();
assert_eq!(failure.scope(), Some(MutationFailureScope::Global));
}
#[test]
fn rollback_all_reports_custom_policy_failure_globally() {
let mut mutations = Mutations::with_capacity(1);
mutations.push(Mutation::new_fallible(always_succeeds, always_fails));
let result = mutations.rollback_all();
assert!(result.callback_failed());
assert_eq!(result.failure().scope(), Some(MutationFailureScope::Global));
assert!(result
.callback_failure_details()
.contains("rollback callback failed"));
}
#[test]
fn rollback_all_applies_in_reverse_order() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(3);
for id in ["a", "b", "c"] {
let r = Rc::clone(&calls);
mutations.push(Mutation::new(noop_action, move || {
r.borrow_mut().push(id);
}));
}
let _ = mutations.rollback_all();
assert_eq!(&*calls.borrow(), &["c", "b", "a"]);
}
#[test]
fn default_creates_empty_mutations() {
let mutations = Mutations::default();
assert!(mutations.is_empty());
}
#[test]
fn new_creates_empty_mutations() {
let mutations = Mutations::new();
assert!(mutations.is_empty());
}
#[test]
fn commit_all_on_empty_is_noop() {
noop_action();
assert!(!Mutations::new().commit_all().failed());
}
#[test]
fn rollback_all_on_empty_is_noop() {
let _ = Mutations::new().rollback_all();
}
}