use std::fmt::{Display, Formatter};
use crate::core::account_groups::AccountGroups;
use crate::core::HasAccountId;
use crate::param::{AccountGroupId, AccountId, DEFAULT_ACCOUNT_GROUP};
use crate::pretrade::{AccountBlock, Reject, RejectCode, RejectScope, Rejects};
use crate::storage::{self, IndexFlag, LockingPolicy, Storage, StorageBuilder};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccountBlockError {
ReservedGroup,
AccountNotBlocked {
account: AccountId,
},
GroupNotBlocked {
group: AccountGroupId,
},
}
impl Display for AccountBlockError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReservedGroup => {
formatter.write_str("the reserved default account group is not a valid target")
}
Self::AccountNotBlocked { account } => {
write!(formatter, "account {account} is not blocked")
}
Self::GroupNotBlocked { group } => {
write!(formatter, "account group {group} is not blocked")
}
}
}
}
impl std::error::Error for AccountBlockError {}
pub struct AccountControl<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
handle: AccountBlockHandle<StorageFactory>,
account_id: AccountId,
}
impl<StorageFactory> Clone for AccountControl<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
fn clone(&self) -> Self {
Self {
handle: self.handle.clone(),
account_id: self.account_id,
}
}
}
impl<StorageFactory> AccountControl<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
pub(crate) fn new(handle: AccountBlockHandle<StorageFactory>, account_id: AccountId) -> Self {
Self { handle, account_id }
}
pub fn block(&self, block: AccountBlock) {
self.handle.record(self.account_id, block);
}
}
pub struct AccountBlockHandle<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
inner: StorageFactory::Shared<BlockedAccounts<StorageFactory>>,
}
impl<StorageFactory> Clone for AccountBlockHandle<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<StorageFactory> AccountBlockHandle<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
pub(crate) fn from_inner(
inner: StorageFactory::Shared<BlockedAccounts<StorageFactory>>,
) -> Self {
Self { inner }
}
pub(crate) fn record(&self, account_id: AccountId, block: AccountBlock) {
self.inner.block_account(account_id, block);
}
pub(crate) fn unblock_account(&self, account_id: AccountId) {
self.inner.unblock_account(account_id);
}
pub(crate) fn replace_reason(
&self,
account_id: AccountId,
block: AccountBlock,
) -> Result<(), AccountBlockError> {
self.inner.replace_reason(account_id, block)
}
pub(crate) fn block_group(
&self,
group: AccountGroupId,
block: AccountBlock,
) -> Result<(), AccountBlockError> {
self.inner.block_group(group, block)
}
pub(crate) fn unblock_group(&self, group: AccountGroupId) -> Result<(), AccountBlockError> {
self.inner.unblock_group(group)
}
pub(crate) fn replace_group_reason(
&self,
group: AccountGroupId,
block: AccountBlock,
) -> Result<(), AccountBlockError> {
self.inner.replace_group_reason(group, block)
}
}
fn new_account_blocked_rejects() -> Rejects {
Rejects::new(vec![Reject::new(
"Engine",
RejectScope::Account,
RejectCode::AccountBlocked,
"account is blocked due to kill-switch",
"kill-switch was previously triggered for this account".to_owned(),
)])
}
fn new_unverifiable_blocked_rejects(scope: RejectScope) -> Rejects {
Rejects::new(vec![Reject::new(
"Engine",
scope,
RejectCode::MissingRequiredField,
"account could not be verified as account ID is missing",
"unable to check account for blocking".to_owned(),
)])
}
pub(crate) struct BlockedAccounts<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
mutation_guard: <StorageFactory as storage::LockingPolicyFactory>::Policy,
any_flag: <StorageFactory as storage::LockingPolicyFactory>::IndexFlag,
all_flag: <StorageFactory as storage::LockingPolicyFactory>::IndexFlag,
blocked_groups_any_flag: <StorageFactory as storage::LockingPolicyFactory>::IndexFlag,
accounts: Storage<AccountId, AccountBlock, StorageFactory::Policy>,
blocked_groups: Storage<AccountGroupId, AccountBlock, StorageFactory::Policy>,
}
impl<StorageLockingPolicyFactory> BlockedAccounts<StorageLockingPolicyFactory>
where
StorageLockingPolicyFactory:
storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
pub(crate) fn new(builder: &StorageBuilder<StorageLockingPolicyFactory>) -> Self {
Self {
mutation_guard: builder.create_policy(),
any_flag:
<StorageLockingPolicyFactory as storage::LockingPolicyFactory>::IndexFlag::new(
false,
),
all_flag:
<StorageLockingPolicyFactory as storage::LockingPolicyFactory>::IndexFlag::new(
false,
),
blocked_groups_any_flag:
<StorageLockingPolicyFactory as storage::LockingPolicyFactory>::IndexFlag::new(
false,
),
accounts: builder.create_for_bound_key(),
blocked_groups: builder.create_for_any_key(),
}
}
pub(crate) fn check<Order: HasAccountId>(
&self,
groups: &AccountGroups<StorageLockingPolicyFactory>,
order: &Order,
operation_scope: RejectScope,
) -> Option<Rejects> {
let all_blocking = self.all_flag.load();
let account_blocking = all_blocking || self.any_flag.load();
let group_blocking = self.blocked_groups_any_flag.load();
if !account_blocking && !group_blocking {
debug_assert!(!all_blocking);
return None;
}
match order.account_id() {
Err(_) => Some(new_unverifiable_blocked_rejects(operation_scope)),
Ok(id) => {
if account_blocking {
if let Some(rejects) = self
.accounts
.with(&id, |b| Rejects::new(vec![Reject::from(b.clone())]))
{
return Some(rejects);
}
}
if group_blocking {
if let Some(group) = groups.group_of(id) {
if let Some(rejects) = self
.blocked_groups
.with(&group, |b| Rejects::new(vec![Reject::from(b.clone())]))
{
return Some(rejects);
}
}
}
if all_blocking {
return Some(new_account_blocked_rejects());
}
None
}
}
}
pub(crate) fn record<Report: HasAccountId>(&self, report: &Report, cause: AccountBlock) {
match report.account_id() {
Ok(id) => self.block_account(id, cause),
Err(_) => self.block_all(),
}
}
pub(crate) fn block_account(&self, id: AccountId, cause: AccountBlock) {
let _guard = self.mutation_guard.write_index();
self.accounts.with_mut(id, || cause, |_, _| ());
self.any_flag.store(true);
}
fn block_all(&self) {
let _guard = self.mutation_guard.write_index();
self.any_flag.store(true);
self.all_flag.store(true);
}
pub(crate) fn unblock_account(&self, id: AccountId) {
let _guard = self.mutation_guard.write_index();
self.accounts.remove(&id);
if self.accounts.is_empty() && !self.all_flag.load() {
self.any_flag.store(false);
}
}
pub(crate) fn replace_reason(
&self,
id: AccountId,
block: AccountBlock,
) -> Result<(), AccountBlockError> {
let _guard = self.mutation_guard.write_index();
self.accounts
.with_mut_if_present_exclusive_index(&id, |slot| *slot = block)
.ok_or(AccountBlockError::AccountNotBlocked { account: id })
}
pub(crate) fn block_group(
&self,
group: AccountGroupId,
cause: AccountBlock,
) -> Result<(), AccountBlockError> {
if group == DEFAULT_ACCOUNT_GROUP {
return Err(AccountBlockError::ReservedGroup);
}
let _guard = self.mutation_guard.write_index();
self.blocked_groups.with_mut(group, || cause, |_, _| ());
self.blocked_groups_any_flag.store(true);
Ok(())
}
pub(crate) fn unblock_group(&self, group: AccountGroupId) -> Result<(), AccountBlockError> {
if group == DEFAULT_ACCOUNT_GROUP {
return Err(AccountBlockError::ReservedGroup);
}
let _guard = self.mutation_guard.write_index();
self.blocked_groups.remove(&group);
if self.blocked_groups.is_empty() {
self.blocked_groups_any_flag.store(false);
}
Ok(())
}
pub(crate) fn replace_group_reason(
&self,
group: AccountGroupId,
block: AccountBlock,
) -> Result<(), AccountBlockError> {
if group == DEFAULT_ACCOUNT_GROUP {
return Err(AccountBlockError::ReservedGroup);
}
let _guard = self.mutation_guard.write_index();
self.blocked_groups
.with_mut_if_present_exclusive_index(&group, |slot| *slot = block)
.ok_or(AccountBlockError::GroupNotBlocked { group })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::HasAccountId;
use crate::param::{AccountGroupId, AccountId};
use crate::pretrade::RejectCode;
use crate::storage::{NoLocking, StorageBuilder};
use crate::RequestFieldAccessError;
fn new_set() -> BlockedAccounts<NoLocking> {
BlockedAccounts::new(&StorageBuilder::new(NoLocking))
}
fn empty_groups() -> AccountGroups<NoLocking> {
AccountGroups::new(&StorageBuilder::new(NoLocking))
}
fn cause(policy: &str, code: RejectCode) -> AccountBlock {
AccountBlock::new(policy, code, "test block", "details")
}
fn admin(reason: &str) -> AccountBlock {
AccountBlock::new("Engine", RejectCode::AccountBlocked, reason, reason)
}
struct AccountOrder(AccountId);
impl HasAccountId for AccountOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Ok(self.0)
}
}
struct NoAccountOrder;
impl HasAccountId for NoAccountOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Err(RequestFieldAccessError::new("account_id"))
}
}
fn account(id: u64) -> AccountId {
AccountId::from_u64(id)
}
fn group(id: u32) -> AccountGroupId {
AccountGroupId::from_u32(id).expect("account group id must be valid")
}
#[test]
fn initially_nothing_blocked() {
let set = new_set();
let groups = empty_groups();
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_none());
}
#[test]
fn record_account_blocks_that_account() {
let set = new_set();
let groups = empty_groups();
set.record(
&AccountOrder(account(1)),
cause("Policy", RejectCode::PnlKillSwitchTriggered),
);
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
}
#[test]
fn record_account_does_not_block_other_accounts() {
let set = new_set();
let groups = empty_groups();
set.record(
&AccountOrder(account(1)),
cause("Policy", RejectCode::PnlKillSwitchTriggered),
);
assert!(set
.check(&groups, &AccountOrder(account(2)), RejectScope::Order)
.is_none());
}
#[test]
fn record_no_account_blocks_every_account() {
let set = new_set();
let groups = empty_groups();
set.record(
&NoAccountOrder,
cause("Policy", RejectCode::PnlKillSwitchTriggered),
);
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
assert!(set
.check(&groups, &AccountOrder(account(99)), RejectScope::Order)
.is_some());
}
#[test]
fn record_no_account_blocks_unidentifiable_orders() {
let set = new_set();
let groups = empty_groups();
set.record(
&NoAccountOrder,
cause("Policy", RejectCode::PnlKillSwitchTriggered),
);
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_some());
}
#[test]
fn record_account_blocks_unidentifiable_orders() {
let set = new_set();
let groups = empty_groups();
set.record(
&AccountOrder(account(1)),
cause("Policy", RejectCode::PnlKillSwitchTriggered),
);
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_some());
}
#[test]
fn initially_unidentifiable_order_is_allowed() {
let set = new_set();
let groups = empty_groups();
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_none());
}
#[test]
fn check_returns_cause_for_blocked_account() {
let set = new_set();
let groups = empty_groups();
set.record(
&AccountOrder(account(1)),
cause("KillSwitch", RejectCode::PnlKillSwitchTriggered),
);
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("blocked account must return rejects");
assert_eq!(rejects.len(), 1);
assert_eq!(rejects[0].policy, "KillSwitch");
assert_eq!(rejects[0].code, RejectCode::PnlKillSwitchTriggered);
assert_eq!(rejects[0].scope, RejectScope::Account);
}
#[test]
fn first_cause_wins_on_repeated_block() {
let set = new_set();
let groups = empty_groups();
set.record(
&AccountOrder(account(1)),
cause("First", RejectCode::PnlKillSwitchTriggered),
);
set.record(
&AccountOrder(account(1)),
cause("Second", RejectCode::Other),
);
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("blocked account must return rejects");
assert_eq!(rejects[0].policy, "First");
}
#[test]
fn admin_block_then_check_rejects() {
let set = new_set();
let groups = empty_groups();
set.block_account(account(1), admin("manual review"));
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("admin-blocked account must return rejects");
assert_eq!(rejects[0].policy, "Engine");
assert_eq!(rejects[0].code, RejectCode::AccountBlocked);
assert_eq!(rejects[0].reason, "manual review");
}
#[test]
fn unblock_then_check_passes() {
let set = new_set();
let groups = empty_groups();
set.block_account(account(1), admin("manual review"));
set.unblock_account(account(1));
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn unblock_of_non_blocked_account_is_noop() {
let set = new_set();
let groups = empty_groups();
set.unblock_account(account(1));
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn unblock_clears_kill_switch_block() {
let set = new_set();
let groups = empty_groups();
set.record(
&AccountOrder(account(1)),
cause("KillSwitch", RejectCode::PnlKillSwitchTriggered),
);
set.unblock_account(account(1));
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn replace_reason_updates_stored_cause() {
let set = new_set();
let groups = empty_groups();
set.block_account(account(1), admin("first"));
set.replace_reason(account(1), admin("second"))
.expect("replacing reason on a blocked account must succeed");
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("blocked account must return rejects");
assert_eq!(rejects[0].reason, "second");
}
#[test]
fn replace_reason_errors_when_not_blocked() {
let set = new_set();
let error = set
.replace_reason(account(1), admin("reason"))
.expect_err("replacing reason on an unblocked account must fail");
assert_eq!(
error,
AccountBlockError::AccountNotBlocked {
account: account(1)
}
);
}
#[test]
fn block_does_not_overwrite_first_reason() {
let set = new_set();
let groups = empty_groups();
set.block_account(account(1), admin("first"));
set.block_account(account(1), admin("second"));
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("blocked account must return rejects");
assert_eq!(rejects[0].reason, "first");
}
#[test]
fn block_group_blocks_current_members() {
let set = new_set();
let groups = empty_groups();
groups
.register_group(&[account(1), account(2)], group(7))
.expect("registration must succeed");
set.block_group(group(7), admin("group halt"))
.expect("group block must succeed");
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("member of blocked group must be rejected");
assert_eq!(rejects[0].policy, "Engine");
assert_eq!(rejects[0].code, RejectCode::AccountBlocked);
assert_eq!(rejects[0].reason, "group halt");
assert!(set
.check(&groups, &AccountOrder(account(2)), RejectScope::Order)
.is_some());
}
#[test]
fn member_registered_into_blocked_group_is_blocked_automatically() {
let set = new_set();
let groups = empty_groups();
set.block_group(group(7), admin("group halt"))
.expect("group block must succeed");
groups
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
}
#[test]
fn account_removed_from_blocked_group_is_no_longer_group_blocked() {
let set = new_set();
let groups = empty_groups();
groups
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
set.block_group(group(7), admin("group halt"))
.expect("group block must succeed");
groups
.unregister_group(&[account(1)], group(7))
.expect("unregistration must succeed");
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn account_removed_from_blocked_group_stays_blocked_if_individually_blocked() {
let set = new_set();
let groups = empty_groups();
groups
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
set.block_group(group(7), admin("group halt"))
.expect("group block must succeed");
set.block_account(account(1), admin("individual"));
groups
.unregister_group(&[account(1)], group(7))
.expect("unregistration must succeed");
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("individually blocked account must remain blocked");
assert_eq!(rejects[0].reason, "individual");
}
#[test]
fn unblock_group_lets_members_pass() {
let set = new_set();
let groups = empty_groups();
groups
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
set.block_group(group(7), admin("group halt"))
.expect("group block must succeed");
set.unblock_group(group(7))
.expect("group unblock must succeed");
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn unblock_group_of_non_blocked_group_is_noop() {
let set = new_set();
set.unblock_group(group(7))
.expect("unblocking a non-blocked group must be a no-op");
}
#[test]
fn block_group_first_cause_wins() {
let set = new_set();
let groups = empty_groups();
groups
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
set.block_group(group(7), admin("first"))
.expect("group block must succeed");
set.block_group(group(7), admin("second"))
.expect("re-blocking a group must be a no-op");
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("member of blocked group must be rejected");
assert_eq!(rejects[0].reason, "first");
}
#[test]
fn replace_group_reason_updates_stored_cause() {
let set = new_set();
let groups = empty_groups();
groups
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
set.block_group(group(7), admin("first"))
.expect("group block must succeed");
set.replace_group_reason(group(7), admin("second"))
.expect("replacing reason on a blocked group must succeed");
let rejects = set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.expect("member of blocked group must be rejected");
assert_eq!(rejects[0].reason, "second");
}
#[test]
fn replace_group_reason_errors_when_not_blocked() {
let set = new_set();
let error = set
.replace_group_reason(group(7), admin("reason"))
.expect_err("replacing reason on an unblocked group must fail");
assert_eq!(
error,
AccountBlockError::GroupNotBlocked { group: group(7) }
);
}
#[test]
fn group_operations_reject_reserved_default_group() {
let set = new_set();
assert_eq!(
set.block_group(DEFAULT_ACCOUNT_GROUP, admin("reason")),
Err(AccountBlockError::ReservedGroup)
);
assert_eq!(
set.unblock_group(DEFAULT_ACCOUNT_GROUP),
Err(AccountBlockError::ReservedGroup)
);
assert_eq!(
set.replace_group_reason(DEFAULT_ACCOUNT_GROUP, admin("reason")),
Err(AccountBlockError::ReservedGroup)
);
}
#[test]
fn account_block_error_display_is_stable() {
assert_eq!(
AccountBlockError::ReservedGroup.to_string(),
"the reserved default account group is not a valid target"
);
assert_eq!(
AccountBlockError::AccountNotBlocked {
account: account(1)
}
.to_string(),
"account 1 is not blocked"
);
assert_eq!(
AccountBlockError::GroupNotBlocked { group: group(2) }.to_string(),
"account group 2 is not blocked"
);
}
#[test]
fn unblock_last_account_clears_unidentifiable_order_rejection() {
let set = new_set();
let groups = empty_groups();
set.block_account(account(1), admin("manual review"));
set.unblock_account(account(1));
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_none());
}
#[test]
fn unblock_last_group_clears_unidentifiable_order_rejection() {
let set = new_set();
let groups = empty_groups();
set.block_group(group(7), admin("group halt"))
.expect("group block must succeed");
set.unblock_group(group(7))
.expect("group unblock must succeed");
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_none());
}
#[test]
fn unblock_one_of_two_accounts_keeps_flag_active() {
let set = new_set();
let groups = empty_groups();
set.block_account(account(1), admin("first"));
set.block_account(account(2), admin("second"));
set.unblock_account(account(1));
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_some());
}
#[test]
fn unblock_one_of_two_groups_keeps_flag_active() {
let set = new_set();
let groups = empty_groups();
set.block_group(group(7), admin("first"))
.expect("group block must succeed");
set.block_group(group(8), admin("second"))
.expect("group block must succeed");
set.unblock_group(group(7))
.expect("group unblock must succeed");
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_some());
}
#[test]
fn unblock_account_does_not_clear_global_block() {
let set = new_set();
let groups = empty_groups();
set.record(
&NoAccountOrder,
cause("Policy", RejectCode::PnlKillSwitchTriggered),
);
set.unblock_account(account(1));
assert!(set
.check(&groups, &AccountOrder(account(2)), RejectScope::Order)
.is_some());
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_some());
}
#[test]
fn full_locking_concurrent_block_and_unblock_preserve_account_flag() {
use crate::storage::FullLocking;
use std::sync::{Arc, Barrier};
use std::thread;
for _ in 0..128 {
let set: Arc<BlockedAccounts<FullLocking>> =
Arc::new(BlockedAccounts::new(&StorageBuilder::new(FullLocking)));
let groups = AccountGroups::new(&StorageBuilder::new(FullLocking));
set.block_account(account(2), admin("old"));
let barrier = Arc::new(Barrier::new(3));
thread::scope(|scope| {
let block_set = Arc::clone(&set);
let block_barrier = Arc::clone(&barrier);
scope.spawn(move || {
block_barrier.wait();
block_set.block_account(account(1), admin("new"));
});
let unblock_set = Arc::clone(&set);
let unblock_barrier = Arc::clone(&barrier);
scope.spawn(move || {
unblock_barrier.wait();
unblock_set.unblock_account(account(2));
});
barrier.wait();
});
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
}
}
#[test]
fn full_locking_concurrent_block_all_and_unblock_preserve_global_flag() {
use crate::storage::FullLocking;
use std::sync::{Arc, Barrier};
use std::thread;
for _ in 0..128 {
let set: Arc<BlockedAccounts<FullLocking>> =
Arc::new(BlockedAccounts::new(&StorageBuilder::new(FullLocking)));
let groups = AccountGroups::new(&StorageBuilder::new(FullLocking));
set.block_account(account(2), admin("old"));
let barrier = Arc::new(Barrier::new(3));
thread::scope(|scope| {
let block_set = Arc::clone(&set);
let block_barrier = Arc::clone(&barrier);
scope.spawn(move || {
block_barrier.wait();
block_set.block_all();
});
let unblock_set = Arc::clone(&set);
let unblock_barrier = Arc::clone(&barrier);
scope.spawn(move || {
unblock_barrier.wait();
unblock_set.unblock_account(account(2));
});
barrier.wait();
});
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
assert!(set
.check(&groups, &NoAccountOrder, RejectScope::Order)
.is_some());
}
}
#[test]
fn full_locking_concurrent_group_block_and_unblock_preserve_group_flag() {
use crate::storage::FullLocking;
use std::sync::{Arc, Barrier};
use std::thread;
for _ in 0..128 {
let set: Arc<BlockedAccounts<FullLocking>> =
Arc::new(BlockedAccounts::new(&StorageBuilder::new(FullLocking)));
let groups = AccountGroups::new(&StorageBuilder::new(FullLocking));
groups
.register_group(&[account(1)], group(10))
.expect("group registration must succeed");
set.block_group(group(20), admin("old"))
.expect("group block must succeed");
let barrier = Arc::new(Barrier::new(3));
thread::scope(|scope| {
let block_set = Arc::clone(&set);
let block_barrier = Arc::clone(&barrier);
scope.spawn(move || {
block_barrier.wait();
block_set
.block_group(group(10), admin("new"))
.expect("group block must succeed");
});
let unblock_set = Arc::clone(&set);
let unblock_barrier = Arc::clone(&barrier);
scope.spawn(move || {
unblock_barrier.wait();
unblock_set
.unblock_group(group(20))
.expect("group unblock must succeed");
});
barrier.wait();
});
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
}
}
#[test]
fn full_locking_admin_mutations_race_check_without_corruption() {
use crate::storage::FullLocking;
use std::sync::Arc;
use std::thread;
let set: Arc<BlockedAccounts<FullLocking>> =
Arc::new(BlockedAccounts::new(&StorageBuilder::new(FullLocking)));
let groups: Arc<AccountGroups<FullLocking>> =
Arc::new(AccountGroups::new(&StorageBuilder::new(FullLocking)));
thread::scope(|scope| {
for tid in 0..4u64 {
let set = Arc::clone(&set);
scope.spawn(move || {
for round in 0..500 {
set.block_account(account(1), admin("blocked"));
let _ =
set.replace_reason(account(1), admin(&format!("reason-{tid}-{round}")));
set.unblock_account(account(1));
}
});
}
for _ in 0..4 {
let set = Arc::clone(&set);
let groups = Arc::clone(&groups);
scope.spawn(move || {
for _ in 0..500 {
let _ = set.check(&groups, &AccountOrder(account(1)), RejectScope::Order);
}
});
}
});
set.unblock_account(account(1));
assert!(set
.check(&groups, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
assert!(set
.check(&groups, &AccountOrder(account(2)), RejectScope::Order)
.is_none());
}
}