use std::{
collections::BTreeMap,
time::{Duration, Instant},
};
use ts_l3::{Executor, Operation, StorageKey};
pub use ts_l3::{StorageCompletion, StoragePoll, StoragePressure};
use ts_metrics::L3SchedulerStats;
use ts_scheduler::{
ExecutionPlan, Scheduler, StorageCancellationView, StorageDoneView, StorageOpKind,
StorageRound, StorageStatsView, StorageTickView,
};
use crate::codec::{Reader, Writer};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StorageLimits {
pub max_operations: usize,
pub max_keys_per_operation: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum L3Error {
#[error("L3 replica polling failed: {0:?}")]
Polling(Vec<String>),
#[error("L3: {0}")]
Protocol(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Report {
now_us: i64,
pressure: StoragePressure,
error: Option<String>,
completions: Vec<StorageCompletion>,
}
type OwnerReport = (Vec<bool>, bool, Vec<bool>);
#[derive(Debug)]
pub struct L3 {
limits: StorageLimits,
reports: BTreeMap<u64, BTreeMap<usize, OwnerReport>>,
cancellations: Vec<StorageCancellationView>,
metrics_sampled: Option<(Instant, bool)>,
}
impl L3 {
pub fn new(limits: StorageLimits) -> Self {
Self {
limits,
reports: BTreeMap::new(),
cancellations: Vec::new(),
metrics_sampled: None,
}
}
pub fn cancel(&mut self, scheduler: &Scheduler, request_id: &str) -> bool {
if scheduler.request_token_size(request_id) < 0 {
return false;
}
let abort = scheduler.can_abort_before_admission(request_id);
let cancellation = StorageCancellationView {
request_id: request_id.to_owned(),
abort,
};
match self
.cancellations
.iter_mut()
.find(|queued| queued.request_id == request_id)
{
Some(queued) => *queued = cancellation,
None => self.cancellations.push(cancellation),
}
abort
}
pub fn report(&self, poll: StoragePoll) -> Vec<u8> {
encode(&self.validated(poll))
}
pub fn agree(
&mut self,
reports: &[Vec<u8>],
allow_forward: bool,
allow_storage: bool,
) -> Result<StorageRound, L3Error> {
let reports = reports
.iter()
.map(|bytes| decode(bytes).ok_or(L3Error::Protocol("a malformed owner report")))
.collect::<Result<Vec<_>, _>>()?;
if reports.is_empty() {
return Err(L3Error::Protocol("a round without reports"));
}
let errors: Vec<String> = reports
.iter()
.filter_map(|report| report.error.clone())
.collect();
if !errors.is_empty() {
return Err(L3Error::Polling(errors));
}
for (rank, report) in reports.iter().enumerate() {
if report.completions.len() > self.limits.max_operations {
return Err(L3Error::Protocol("a peer exceeded its report credits"));
}
for completion in &report.completions {
if completion.success.len() > self.limits.max_keys_per_operation {
return Err(L3Error::Protocol("a peer exceeded its key credits"));
}
let value = (
completion.success.clone(),
completion.retired,
completion.object_failures.clone(),
);
let row = self.reports.entry(completion.op_id).or_default();
match row.get(&rank) {
Some(held) if *held != value => {
return Err(L3Error::Protocol("conflicting owner reports"));
}
_ => {
row.insert(rank, value);
}
}
}
}
if self.reports.len() > self.limits.max_operations {
return Err(L3Error::Protocol(
"the aggregation exceeded its operation credits",
));
}
let worst = |field: fn(&StoragePressure) -> i64| {
reports
.iter()
.map(|report| field(&report.pressure))
.max()
.unwrap_or_default()
};
let tick = StorageTickView {
now_us: reports[0].now_us,
allow_forward,
allow_storage,
queued_operations: worst(|pressure| pressure.queued_operations),
queued_bytes: worst(|pressure| pressure.queued_bytes),
oldest_operation_us: worst(|pressure| pressure.oldest_operation_us),
};
let owners = reports.len();
let mut done = Vec::new();
for (&op_id, row) in &self.reports {
if row.len() != owners {
continue;
}
let count = row[&0].0.len();
if row.values().any(|(success, _, _)| success.len() != count) {
return Err(L3Error::Protocol("mirrored operation shapes differ"));
}
done.push(StorageDoneView {
op_id,
success: (0..count)
.map(|key| u8::from(row.values().all(|(success, _, _)| success[key])))
.collect(),
retired: row.values().all(|(_, retired, _)| *retired),
object_failures: (0..count)
.map(|key| u8::from(row.values().any(|(_, _, failed)| failed[key])))
.collect(),
});
}
for completion in &done {
self.reports.remove(&completion.op_id);
}
Ok(StorageRound {
tick: Some(tick),
cancellations: std::mem::take(&mut self.cancellations),
done,
})
}
pub fn metrics_due(&mut self, pending: bool) -> bool {
let due = match self.metrics_sampled {
None => true,
Some((at, was_pending)) => {
at.elapsed() >= Duration::from_secs(1) || was_pending != pending
}
};
if due {
self.metrics_sampled = Some((Instant::now(), pending));
}
due
}
fn validated(&self, poll: StoragePoll) -> Report {
let StoragePoll {
now_us,
completions,
pressure,
} = poll;
let invalid = if [
pressure.queued_operations,
pressure.queued_bytes,
pressure.oldest_operation_us,
]
.iter()
.any(|&value| value < 0)
{
Some("Invalid L3 pressure observation".to_owned())
} else if completions.len() > self.limits.max_operations {
Some("L3 completion window exceeded operation credits".to_owned())
} else if completions.iter().any(|completion| {
completion.op_id == 0
|| completion.success.len() > self.limits.max_keys_per_operation
|| completion.object_failures.len() != completion.success.len()
|| completion
.success
.iter()
.zip(&completion.object_failures)
.any(|(&ok, &failed)| ok && failed)
}) {
Some("Malformed L3 completion".to_owned())
} else {
None
};
match invalid {
Some(error) => Report {
now_us,
pressure: StoragePressure::default(),
error: Some(error.chars().take(512).collect()),
completions: Vec::new(),
},
None => Report {
now_us,
pressure,
error: None,
completions,
},
}
}
}
pub struct Tier {
pub executor: Executor,
pub agreement: L3,
}
impl Tier {
pub fn new(executor: Executor) -> Self {
let config = executor.config();
Self {
executor,
agreement: L3::new(StorageLimits {
max_operations: config.max_operations,
max_keys_per_operation: config.max_keys_per_operation,
}),
}
}
pub fn submit(&self, plan: &ExecutionPlan) -> Result<(), ts_l3::ExecutorError> {
let operations = plan
.storage_ops()
.into_iter()
.map(|op| {
let keys = || -> Vec<StorageKey> {
op.keys
.iter()
.map(|key| StorageKey {
group: key.group_id,
content_hash: key.content_hash.clone(),
page_offset: key.page_offset,
})
.collect()
};
let operation = match op.kind {
StorageOpKind::Query => Operation::Query {
op_id: op.op_id,
keys: keys(),
},
StorageOpKind::Get => Operation::Get {
op_id: op.op_id,
keys: keys(),
host_blocks: op.host_blocks.clone(),
},
StorageOpKind::Put => Operation::Put {
op_id: op.op_id,
keys: keys(),
host_blocks: op.host_blocks.clone(),
},
StorageOpKind::Cancel => Operation::Cancel { op_id: op.op_id },
_ => return Err(ts_l3::ExecutorError::Invalid("its kind")),
};
Ok(operation)
})
.collect::<Result<Vec<_>, _>>()?;
if operations.is_empty() {
return Ok(());
}
self.executor.submit(&operations)
}
pub fn report(&self) -> Vec<u8> {
self.agreement.report(self.executor.poll())
}
pub fn agree(
&mut self,
reports: &[Vec<u8>],
allow_forward: bool,
allow_storage: bool,
) -> Result<StorageRound, L3Error> {
self.agreement.agree(reports, allow_forward, allow_storage)
}
}
pub fn scheduler_stats(stats: StorageStatsView) -> L3SchedulerStats {
L3SchedulerStats {
operations: stats.operations as f64,
held_blocks: stats.held_blocks as f64,
quarantined: stats.quarantined as f64,
negative_keys: stats.negative_keys as f64,
backup_candidates: stats.backup_candidates as f64,
waiting_requests: stats.waiting_requests as f64,
ready_requests: stats.ready_requests as f64,
wait_timeouts: stats.wait_timeouts as f64,
ready_expirations: stats.ready_expirations as f64,
pressure_gate_closed: stats.pressure_gate_closed,
pressure_bypasses: stats.pressure_bypasses as f64,
pressure_dropped_backup_keys: stats.pressure_dropped_backup_keys as f64,
overflow_dropped_backup_keys: stats.overflow_dropped_backup_keys as f64,
unavailable_dropped_backup_keys: stats.unavailable_dropped_backup_keys as f64,
disabled_dropped_backup_keys: stats.disabled_dropped_backup_keys as f64,
cleanup_dropped_backup_keys: stats.cleanup_dropped_backup_keys as f64,
}
}
fn encode(report: &Report) -> Vec<u8> {
let mut out = Writer::new();
out.i64(report.now_us);
out.i64(report.pressure.queued_operations);
out.i64(report.pressure.queued_bytes);
out.i64(report.pressure.oldest_operation_us);
out.option(report.error.as_deref(), |out, error| {
out.bytes(error.as_bytes())
});
out.count(report.completions.len());
for completion in &report.completions {
out.u64(completion.op_id);
out.flag(completion.retired);
out.flags(&completion.success);
out.flags(&completion.object_failures);
}
out.finish()
}
fn decode(bytes: &[u8]) -> Option<Report> {
let mut reader = Reader::new(bytes);
let now_us = reader.i64()?;
let pressure = StoragePressure {
queued_operations: reader.i64()?,
queued_bytes: reader.i64()?,
oldest_operation_us: reader.i64()?,
};
let error = reader.option(|reader| String::from_utf8(reader.bytes()?.to_vec()).ok())?;
let count = reader.count()?;
let completions = (0..count)
.map(|_| {
Some(StorageCompletion {
op_id: reader.u64()?,
retired: reader.flag()?,
success: reader.flags()?,
object_failures: reader.flags()?,
})
})
.collect::<Option<Vec<_>>>()?;
reader.finish(Report {
now_us,
pressure,
error,
completions,
})
}
#[cfg(test)]
mod tests {
use std::thread;
use ts_collective::{Group, Listener, Mesh};
use super::*;
const LIMITS: StorageLimits = StorageLimits {
max_operations: 8,
max_keys_per_operation: 4,
};
fn completion(
op_id: u64,
success: &[bool],
retired: bool,
failed: &[bool],
) -> StorageCompletion {
StorageCompletion {
op_id,
success: success.to_vec(),
retired,
object_failures: failed.to_vec(),
}
}
fn poll(now_us: i64, completions: Vec<StorageCompletion>) -> StoragePoll {
StoragePoll {
now_us,
completions,
..StoragePoll::default()
}
}
fn replica(polls: Vec<Vec<StoragePoll>>) -> Vec<Vec<Result<StorageRound, String>>> {
let listeners: Vec<_> = polls
.iter()
.map(|_| Listener::bind("127.0.0.1").unwrap())
.collect();
let addresses: Vec<String> = listeners
.iter()
.map(|listener| format!("127.0.0.1:{}", listener.port().unwrap()))
.collect();
let size = polls.len();
let threads: Vec<_> = listeners
.into_iter()
.zip(polls)
.enumerate()
.map(|(rank, (listener, rounds))| {
let addresses = addresses.clone();
thread::spawn(move || {
let mut mesh =
Mesh::connect(rank, listener, &addresses, Duration::from_secs(5)).unwrap();
let group = Group::new(0, (0..size).collect(), rank).unwrap();
let mut l3 = L3::new(LIMITS);
let mut outcomes = Vec::new();
for poll in rounds {
let reports = mesh.all_gather(&group, &l3.report(poll)).unwrap();
let outcome = l3
.agree(&reports, true, true)
.map_err(|error| error.to_string());
let failed = outcome.is_err();
outcomes.push(outcome);
if failed {
break;
}
}
outcomes
})
})
.collect();
threads
.into_iter()
.map(|thread| thread.join().unwrap())
.collect()
}
#[test]
fn owners_complete_out_of_order_and_converge_on_a_fault() {
let rank0 = vec![
poll(
10,
vec![completion(1, &[true, true], true, &[false, false])],
),
poll(
20,
vec![completion(2, &[true, true], true, &[false, false])],
),
poll(30, vec![]),
poll(40, vec![completion(0, &[true], true, &[false])]),
];
let rank1 = vec![
poll(
11,
vec![completion(2, &[true, false], true, &[false, true])],
),
poll(21, vec![]),
poll(
31,
vec![completion(1, &[false, false], false, &[false, false])],
),
poll(41, vec![]),
];
for outcomes in replica(vec![rank0, rank1]) {
let first = outcomes[0].as_ref().unwrap();
assert_eq!(first.tick.unwrap().now_us, 10);
assert!(first.done.is_empty());
let second = outcomes[1].as_ref().unwrap();
assert_eq!(
second.done,
[StorageDoneView {
op_id: 2,
success: vec![1, 0],
retired: true,
object_failures: vec![0, 1],
}]
);
let third = outcomes[2].as_ref().unwrap();
assert_eq!(third.done.len(), 1);
assert_eq!(third.done[0].op_id, 1);
assert!(!third.done[0].retired);
let failure = outcomes[3].as_ref().unwrap_err();
assert!(failure.contains("Malformed L3 completion"), "{failure}");
}
}
#[test]
fn idle_rounds_agree_on_the_first_members_clock() {
let rounds = |rank: usize| {
(0..3)
.map(|round| poll(100 * round + rank as i64, vec![]))
.collect::<Vec<_>>()
};
for outcomes in replica((0..3).map(rounds).collect()) {
let clocks: Vec<i64> = outcomes
.into_iter()
.map(|outcome| outcome.unwrap().tick.unwrap().now_us)
.collect();
assert_eq!(clocks, [0, 100, 200]);
}
}
#[test]
fn the_tick_carries_the_permissions_it_is_given() {
let mut l3 = L3::new(LIMITS);
let reports = [l3.report(poll(5, vec![]))];
let tick = l3.agree(&reports, false, true).unwrap().tick.unwrap();
assert!(!tick.allow_forward && tick.allow_storage);
let tick = l3.agree(&reports, true, false).unwrap().tick.unwrap();
assert!(tick.allow_forward && !tick.allow_storage);
}
#[test]
fn the_worst_owner_pressure_wins_and_an_invalid_one_fails_every_rank() {
let pressured = |queued_operations, queued_bytes, oldest_operation_us| StoragePoll {
pressure: StoragePressure {
queued_operations,
queued_bytes,
oldest_operation_us,
},
..StoragePoll::default()
};
let rank0 = vec![pressured(9, 0, 0), pressured(0, 0, 0), pressured(-1, 0, 0)];
let rank1 = vec![
pressured(0, 4096, 700_000),
pressured(0, 0, 0),
pressured(0, 0, 0),
];
for outcomes in replica(vec![rank0, rank1]) {
let tick = outcomes[0].as_ref().unwrap().tick.unwrap();
assert_eq!(
(
tick.queued_operations,
tick.queued_bytes,
tick.oldest_operation_us
),
(9, 4096, 700_000)
);
let tick = outcomes[1].as_ref().unwrap().tick.unwrap();
assert_eq!(
(
tick.queued_operations,
tick.queued_bytes,
tick.oldest_operation_us
),
(0, 0, 0)
);
let failure = outcomes[2].as_ref().unwrap_err();
assert!(
failure.contains("Invalid L3 pressure observation"),
"{failure}"
);
}
}
#[test]
fn a_malformed_completion_fails_every_rank_before_publication() {
for bad in [
completion(7, &[true], true, &[]),
completion(7, &[true], true, &[true]),
completion(0, &[true], true, &[false]),
completion(7, &[true; 5], true, &[false; 5]),
] {
let outcomes = replica(vec![vec![poll(1, vec![bad])], vec![poll(1, vec![])]]);
for outcome in outcomes {
let failure = outcome[0].as_ref().unwrap_err();
assert!(failure.contains("L3 replica polling failed"), "{failure}");
}
}
}
#[test]
fn a_rejection_is_not_a_confirmed_peer_miss() {
let outcomes = replica(vec![
vec![poll(
1,
vec![completion(
1,
&[false, false, true],
true,
&[false, false, false],
)],
)],
vec![poll(
1,
vec![completion(
1,
&[true, false, true],
true,
&[false, true, false],
)],
)],
]);
for outcome in outcomes {
let done = &outcome[0].as_ref().unwrap().done[0];
assert_eq!(done.success, [0, 0, 1]);
assert_eq!(done.object_failures, [0, 1, 0]);
assert!(done.retired);
}
}
#[test]
fn a_conflicting_owner_report_fails_every_rank() {
let outcomes = replica(vec![
vec![poll(1, vec![]), poll(2, vec![])],
vec![
poll(1, vec![completion(5, &[true], true, &[false])]),
poll(2, vec![completion(5, &[false], true, &[false])]),
],
]);
for outcome in outcomes {
assert!(outcome[0].as_ref().unwrap().done.is_empty());
let failure = outcome[1].as_ref().unwrap_err();
assert!(failure.contains("conflicting"), "{failure}");
}
}
#[test]
fn reports_round_trip_their_encoding() {
let report = Report {
now_us: -3,
pressure: StoragePressure {
queued_operations: 1,
queued_bytes: 2_i64.pow(33),
oldest_operation_us: 7,
},
error: Some("boom".to_owned()),
completions: vec![completion(9, &[true, false], false, &[false, true])],
};
assert_eq!(decode(&encode(&report)), Some(report.clone()));
let mut cut = encode(&report);
cut.pop();
assert_eq!(decode(&cut), None);
let mut l3 = L3::new(LIMITS);
let failure = l3.agree(&[cut], true, true).unwrap_err();
assert!(failure.to_string().contains("malformed"), "{failure}");
}
}