#[cfg(feature = "shuttle-test")]
use shuttle::sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
};
#[cfg(not(feature = "shuttle-test"))]
use std::sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
};
use {
crate::poh_recorder::Record,
crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError, bounded},
solana_clock::BankId,
std::time::Duration,
};
pub fn record_channels(track_transaction_indexes: bool) -> (RecordSender, RecordReceiver) {
const CAPACITY: usize = BankIdAllowedInsertions::MAX_ALLOWED_INSERTIONS as usize;
let (sender, receiver) = bounded(CAPACITY);
let bank_id_allowed_insertions = BankIdAllowedInsertions::new_shutdown();
let transaction_indexes = if track_transaction_indexes {
Some(Arc::new(Mutex::new(0)))
} else {
None
};
let active_senders = Arc::new(AtomicU64::new(0));
(
RecordSender {
active_senders: active_senders.clone(),
bank_id_allowed_insertions: bank_id_allowed_insertions.clone(),
sender,
transaction_indexes: transaction_indexes.clone(),
},
RecordReceiver {
active_senders,
bank_id_allowed_insertions,
receiver,
capacity: CAPACITY as u64,
transaction_indexes,
},
)
}
#[derive(Debug)]
pub enum RecordSenderError {
Full,
Shutdown,
InactiveBankId,
Disconnected,
}
#[derive(Clone, Debug)]
pub struct RecordSender {
active_senders: Arc<AtomicU64>,
bank_id_allowed_insertions: BankIdAllowedInsertions,
sender: Sender<Record>,
transaction_indexes: Option<Arc<Mutex<usize>>>,
}
impl RecordSender {
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.sender.is_empty()
}
pub fn try_send(&self, record: Record) -> Result<Option<usize>, RecordSenderError> {
let num_transactions = record.transactions.len();
assert!(num_transactions > 0);
loop {
let transaction_indexes = self
.transaction_indexes
.as_ref()
.map(|transaction_indexes| transaction_indexes.lock().unwrap());
let current_bank_id_allowed_insertions =
self.bank_id_allowed_insertions.0.load(Ordering::Acquire);
let (bank_id, allowed_insertions) = (
BankIdAllowedInsertions::bank_id(current_bank_id_allowed_insertions),
BankIdAllowedInsertions::allowed_insertions(current_bank_id_allowed_insertions),
);
if bank_id == BankIdAllowedInsertions::DISABLED_BANK_ID {
return Err(RecordSenderError::Shutdown);
}
if bank_id != record.bank_id {
return Err(RecordSenderError::InactiveBankId);
}
if allowed_insertions == 0 {
return Err(RecordSenderError::Full);
}
let new_bank_id_allowed_insertions =
BankIdAllowedInsertions::encoded_value(bank_id, allowed_insertions.wrapping_sub(1));
self.active_senders.fetch_add(1, Ordering::AcqRel);
if self
.bank_id_allowed_insertions
.0
.compare_exchange(
current_bank_id_allowed_insertions,
new_bank_id_allowed_insertions,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err()
{
self.active_senders.fetch_sub(1, Ordering::AcqRel);
continue;
}
match self.sender.try_send(record) {
Ok(_) => {
self.active_senders.fetch_sub(1, Ordering::AcqRel);
return Ok(transaction_indexes.map(|mut transaction_indexes| {
let transaction_starting_index = *transaction_indexes;
*transaction_indexes += num_transactions;
transaction_starting_index
}));
}
Err(err) => {
assert!(err.is_disconnected());
self.active_senders.fetch_sub(1, Ordering::AcqRel);
return Err(RecordSenderError::Disconnected);
}
}
}
}
}
pub struct RecordReceiver {
capacity: u64,
active_senders: Arc<AtomicU64>,
bank_id_allowed_insertions: BankIdAllowedInsertions,
receiver: Receiver<Record>,
transaction_indexes: Option<Arc<Mutex<usize>>>,
}
impl RecordReceiver {
pub fn inner(&self) -> &Receiver<Record> {
&self.receiver
}
pub fn should_shutdown(&self, remaining_hashes_in_slot: u64, ticks_per_slot: u64) -> bool {
remaining_hashes_in_slot.saturating_sub(ticks_per_slot) <= self.capacity
}
pub fn shutdown(&mut self) {
self.bank_id_allowed_insertions.shutdown();
}
pub fn is_shutdown(&self) -> bool {
BankIdAllowedInsertions::bank_id(self.bank_id_allowed_insertions.0.load(Ordering::Acquire))
== BankIdAllowedInsertions::DISABLED_BANK_ID
}
pub fn restart(&mut self, bank_id: BankId) {
assert!(bank_id <= BankIdAllowedInsertions::MAX_BANK_ID);
assert!(self.receiver.is_empty());
let transaction_indexes_lock =
self.transaction_indexes
.as_ref()
.map(|transaction_indexes| {
let mut lock = transaction_indexes.lock().unwrap();
*lock = 0;
lock
});
self.bank_id_allowed_insertions.0.store(
BankIdAllowedInsertions::encoded_value(bank_id, self.capacity),
Ordering::Release,
);
drop(transaction_indexes_lock);
}
pub fn drain(&self) -> impl Iterator<Item = Record> + '_ {
core::iter::from_fn(|| self.try_recv().ok())
}
pub fn drain_after_shutdown(&self) -> impl Iterator<Item = Record> + '_ {
self.drain()
}
pub fn is_safe_to_restart(&self) -> bool {
self.active_senders.load(Ordering::Acquire) == 0 && self.receiver.is_empty()
}
pub fn try_recv(&self) -> Result<Record, TryRecvError> {
loop {
match self.receiver.try_recv() {
Ok(record) => {
self.on_received_record();
return Ok(record);
}
Err(TryRecvError::Empty) => {
if self.active_senders.load(Ordering::Acquire) != 0 {
core::hint::spin_loop();
continue;
}
match self.receiver.try_recv() {
Ok(record) => {
self.on_received_record();
return Ok(record);
}
Err(TryRecvError::Empty) => return Err(TryRecvError::Empty),
Err(e) => return Err(e),
}
}
Err(e) => return Err(e),
}
}
}
pub fn recv_timeout(&self, duration: Duration) -> Result<Record, RecvTimeoutError> {
let record = self.receiver.recv_timeout(duration)?;
self.on_received_record();
Ok(record)
}
pub fn on_received_record(&self) {
self.bank_id_allowed_insertions
.0
.fetch_add(1, Ordering::AcqRel);
}
}
#[derive(Clone, Debug)]
struct BankIdAllowedInsertions(Arc<AtomicU64>);
impl BankIdAllowedInsertions {
const NUM_BITS: u64 = 64;
const ALLOWED_INSERTIONS_BITS: u64 = 10;
const BANK_ID_BITS: u64 = Self::NUM_BITS - Self::ALLOWED_INSERTIONS_BITS;
const DISABLED_BANK_ID: BankId = (1 << Self::BANK_ID_BITS) - 1;
const MAX_BANK_ID: BankId = Self::DISABLED_BANK_ID - 1;
const MAX_ALLOWED_INSERTIONS: u64 = (1 << Self::ALLOWED_INSERTIONS_BITS) - 1;
const SHUTDOWN: u64 = Self::encoded_value(Self::DISABLED_BANK_ID, 0);
fn new_shutdown() -> Self {
Self(Arc::new(AtomicU64::new(Self::SHUTDOWN)))
}
fn shutdown(&self) {
self.0.store(Self::SHUTDOWN, Ordering::Release);
}
const fn encoded_value(bank_id: BankId, allowed_insertions: u64) -> u64 {
assert!(bank_id <= Self::DISABLED_BANK_ID);
assert!(allowed_insertions <= Self::MAX_ALLOWED_INSERTIONS);
(bank_id << Self::ALLOWED_INSERTIONS_BITS) | allowed_insertions
}
fn bank_id(value: u64) -> BankId {
(value >> Self::ALLOWED_INSERTIONS_BITS) & Self::DISABLED_BANK_ID
}
fn allowed_insertions(value: u64) -> u64 {
value & Self::MAX_ALLOWED_INSERTIONS
}
}
#[cfg(test)]
mod tests {
use {super::*, solana_hash::Hash, solana_transaction::versioned::VersionedTransaction};
pub(super) fn test_record(bank_id: BankId, num_transactions: usize) -> Record {
Record {
bank_id,
transactions: vec![VersionedTransaction::default(); num_transactions],
mixin: Hash::default(),
}
}
#[test]
fn test_record_channels() {
let (sender, mut receiver) = record_channels(false);
assert!(matches!(
sender.try_send(test_record(0, 1)),
Err(RecordSenderError::Shutdown)
));
receiver.restart(1);
assert!(matches!(
sender.try_send(test_record(0, 1)),
Err(RecordSenderError::InactiveBankId)
));
assert!(matches!(sender.try_send(test_record(1, 1)), Ok(None)));
for _ in 1..BankIdAllowedInsertions::MAX_ALLOWED_INSERTIONS {
assert!(matches!(sender.try_send(test_record(1, 1)), Ok(None)));
}
assert!(matches!(
sender.try_send(test_record(1, 1)),
Err(RecordSenderError::Full)
));
assert!(!receiver.is_safe_to_restart());
for _ in 0..BankIdAllowedInsertions::MAX_ALLOWED_INSERTIONS {
assert!(receiver.try_recv().is_ok());
}
assert!(receiver.is_safe_to_restart());
}
#[test]
fn test_record_channels_track_indexes() {
let (sender, mut receiver) = record_channels(true);
assert!(matches!(
sender.try_send(test_record(0, 1)),
Err(RecordSenderError::Shutdown)
));
receiver.restart(1);
assert!(matches!(
sender.try_send(test_record(0, 1)),
Err(RecordSenderError::InactiveBankId)
));
assert!(matches!(sender.try_send(test_record(1, 1)), Ok(Some(0))));
assert!(matches!(sender.try_send(test_record(1, 3)), Ok(Some(1))));
assert!(*sender.transaction_indexes.as_ref().unwrap().lock().unwrap() == 4);
}
}
#[cfg(all(test, feature = "shuttle-test"))]
mod shuttle_tests {
use super::{tests::test_record, *};
#[test]
fn test_sender_shutdown_safety_race() {
const NUM_TEST_RUNS: usize = 100;
shuttle::check_random(
|| {
let (sender, mut receiver) = record_channels(false);
const ITERATIONS_PER_RUN: usize = 1024;
shuttle::thread::spawn(move || {
let mut successful_sends = 0;
let mut bank_id = 0;
let mut had_successful_send = false;
while successful_sends < ITERATIONS_PER_RUN {
if sender.try_send(test_record(bank_id, 1)).is_ok() {
had_successful_send = true;
successful_sends += 1;
} else if had_successful_send {
bank_id += 1;
had_successful_send = false;
}
}
});
let mut current_bank_id = 0;
receiver.restart(current_bank_id);
let mut receives = 0;
while receives < ITERATIONS_PER_RUN {
if receiver.is_shutdown() && receiver.is_safe_to_restart() {
current_bank_id += 1;
receiver.restart(current_bank_id);
}
if let Ok(record) = receiver.try_recv() {
assert!(record.bank_id == current_bank_id, "bank_id mismatch!");
receives += 1;
receiver.shutdown();
}
}
},
NUM_TEST_RUNS,
)
}
#[test]
fn test_try_recv_waits_active() {
const NUM_TEST_RUNS: usize = 100_000;
shuttle::check_random(
|| {
let (sender, mut receiver) = record_channels(false);
receiver.restart(0);
{
let sender = sender.clone();
shuttle::thread::spawn(move || {
let _ = sender.try_send(test_record(0, 1));
});
}
let active_at_start = sender.active_senders.load(Ordering::Acquire);
let result = receiver.try_recv();
if result.is_err() && active_at_start > 0 {
panic!(
"try_recv returned Empty while a sender was active at start of call \
(active_senders={active_at_start})"
);
}
},
NUM_TEST_RUNS,
)
}
#[test]
fn test_drain_shutdown_waits_active() {
const NUM_TEST_RUNS: usize = 100_000;
shuttle::check_random(
|| {
let (sender, mut receiver) = record_channels(false);
receiver.restart(0);
sender.active_senders.fetch_add(1, Ordering::AcqRel);
let active_senders = sender.active_senders.clone();
let inner_sender = sender.sender.clone();
shuttle::thread::spawn(move || {
inner_sender.try_send(test_record(0, 1)).unwrap();
active_senders.fetch_sub(1, Ordering::AcqRel);
});
receiver.shutdown();
let records: Vec<_> = receiver.drain_after_shutdown().collect();
assert_eq!(records.len(), 1);
assert_eq!(records[0].bank_id, 0);
assert!(receiver.is_safe_to_restart());
},
NUM_TEST_RUNS,
)
}
}