use super::reservation::ReservationHandle;
use super::{AccountBlock, PreTradeLock};
use crate::core::account_outcome::AccountAdjustmentOutcome;
pub struct DropCopyOperation {
account_block: Option<AccountBlock>,
lock: PreTradeLock,
account_adjustments: Vec<AccountAdjustmentOutcome>,
inner: Option<Box<dyn ReservationHandle>>,
account_blocked: bool,
}
impl std::fmt::Debug for DropCopyOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DropCopyOperation").finish_non_exhaustive()
}
}
impl DropCopyOperation {
pub fn commit(&mut self) {
self.inner
.take()
.expect("drop-copy operation already consumed")
.commit();
}
pub fn rollback(&mut self) {
if let Some(inner) = self.inner.take() {
inner.rollback();
}
}
pub fn lock(&self) -> &PreTradeLock {
&self.lock
}
pub fn account_adjustments(&self) -> &[AccountAdjustmentOutcome] {
&self.account_adjustments
}
pub fn account_block(&self) -> Option<&AccountBlock> {
self.account_block.as_ref()
}
pub fn is_account_blocked(&self) -> bool {
self.account_blocked
}
pub(crate) fn from_handle(
inner: Box<dyn ReservationHandle>,
lock: PreTradeLock,
account_adjustments: Vec<AccountAdjustmentOutcome>,
account_block: Option<AccountBlock>,
account_blocked: bool,
) -> Self {
Self {
account_block,
lock,
account_adjustments,
inner: Some(inner),
account_blocked,
}
}
}
impl Drop for DropCopyOperation {
fn drop(&mut self) {
if let Some(inner) = self.inner.take() {
inner.rollback();
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use super::{AccountBlock, DropCopyOperation, PreTradeLock};
use crate::core::mutation::MutationFailureKillSwitch;
use crate::core::DEFAULT_POLICY_GROUP_ID;
use crate::param::Price;
use crate::pretrade::handle::ReservationHandleImpl;
use crate::pretrade::RejectCode;
use crate::{Mutation, Mutations};
fn noop_action() {}
fn operation(mutations: Mutations, lock: PreTradeLock) -> DropCopyOperation {
DropCopyOperation::from_handle(
Box::new(ReservationHandleImpl::new(
mutations,
MutationFailureKillSwitch::inert(),
)),
lock,
Vec::new(),
None,
false,
)
}
#[test]
fn commit_executes_commit_mutations() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(1);
let commit_calls = Rc::clone(&calls);
mutations.push(Mutation::new(
move || {
commit_calls.borrow_mut().push("commit");
},
noop_action,
));
let mut operation = operation(mutations, PreTradeLock::default());
operation.commit();
assert_eq!(&*calls.borrow(), &["commit"]);
}
#[test]
fn rollback_executes_rollback_mutations_in_reverse_order() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(2);
for id in ["first", "second"] {
let rollback_calls = Rc::clone(&calls);
mutations.push(Mutation::new(noop_action, move || {
rollback_calls.borrow_mut().push(id);
}));
}
let mut operation = operation(mutations, PreTradeLock::default());
operation.rollback();
assert_eq!(&*calls.borrow(), &["second", "first"]);
}
#[test]
fn drop_without_explicit_finalize_rolls_back() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(2);
for id in ["first", "second"] {
let rollback_calls = Rc::clone(&calls);
mutations.push(Mutation::new(noop_action, move || {
rollback_calls.borrow_mut().push(id);
}));
}
drop(operation(mutations, PreTradeLock::default()));
assert_eq!(&*calls.borrow(), &["second", "first"]);
}
#[test]
fn drop_after_commit_does_not_roll_back() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(1);
let rollback_calls = Rc::clone(&calls);
mutations.push(Mutation::new(noop_action, move || {
rollback_calls.borrow_mut().push("rollback");
}));
let mut operation = operation(mutations, PreTradeLock::default());
operation.commit();
drop(operation);
assert!(calls.borrow().is_empty());
}
#[test]
#[should_panic(expected = "drop-copy operation already consumed")]
fn commit_panics_for_finalized_operation() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(1);
let commit_calls = Rc::clone(&calls);
mutations.push(Mutation::new(
move || commit_calls.borrow_mut().push("commit"),
noop_action,
));
let mut operation = operation(mutations, PreTradeLock::default());
operation.commit();
operation.commit();
}
#[test]
fn rollback_is_noop_for_finalized_operation() {
let calls = Rc::new(RefCell::new(Vec::new()));
let mut mutations = Mutations::with_capacity(1);
let rollback_calls = Rc::clone(&calls);
mutations.push(Mutation::new(noop_action, move || {
rollback_calls.borrow_mut().push("rollback");
}));
let mut operation = operation(mutations, PreTradeLock::default());
operation.rollback();
operation.rollback();
assert_eq!(&*calls.borrow(), &["rollback"]);
}
#[test]
fn accessors_report_the_evaluated_bookkeeping() {
let price = Price::from_str("185").expect("price must be valid");
let block = AccountBlock::new(
"policy",
RejectCode::PnlKillSwitchTriggered,
"pnl kill switch triggered",
"drop-copy block",
);
let mut operation = DropCopyOperation::from_handle(
Box::new(ReservationHandleImpl::new(
Mutations::new(),
MutationFailureKillSwitch::inert(),
)),
PreTradeLock::from_entries([(DEFAULT_POLICY_GROUP_ID, price)]),
Vec::new(),
Some(block),
true,
);
assert_eq!(
operation
.lock()
.prices_of(DEFAULT_POLICY_GROUP_ID)
.collect::<Vec<_>>(),
vec![price]
);
assert!(operation.account_adjustments().is_empty());
assert_eq!(
operation
.account_block()
.expect("the requested block must be reported")
.reason,
"pnl kill switch triggered"
);
assert!(operation.is_account_blocked());
operation.commit();
}
}