use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Mutex, MutexGuard};
use fred::clients::{Client, Pipeline, WithOptions};
use fred::error::{Error, ErrorKind};
use fred::interfaces::{
ClientLike, KeysInterface, ListInterface, PubsubInterface, StreamsInterface,
};
use fred::types::config::Options;
use fred::types::{ClusterHash, CustomCommand, Value};
use futures::future::join_all;
use ruststream::{AckError, IncomingMessage};
use tokio::runtime::Handle;
use super::{Rounds, pipeline_on};
use crate::connection::Connection;
use crate::error::RedisError;
#[cfg(feature = "testing")]
use crate::loopback::InFlight;
#[derive(Clone)]
pub enum Segment {
Plain(Pipeline<Client>),
Atomic(WithOptions<Pipeline<Client>>),
}
impl Debug for Segment {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Plain(_) => "Segment::Plain",
Self::Atomic(_) => "Segment::Atomic",
})
}
}
impl Segment {
fn pipeline(&self) -> &Pipeline<Client> {
match self {
Self::Plain(pipeline) => pipeline,
Self::Atomic(pinned) => pinned,
}
}
}
fn transaction_edge(name: &'static str, slot: u16) -> CustomCommand {
CustomCommand::new_static(name, ClusterHash::Custom(slot), false)
}
pub(crate) struct Segments {
client: Client,
rounds: Arc<Rounds>,
atomic: bool,
hash_slot: u16,
slots: Mutex<Slots>,
}
#[derive(Default)]
struct Slots {
segments: Vec<Option<Segment>>,
members: Vec<u32>,
clean: Vec<bool>,
generations: Vec<u32>,
free: Vec<u32>,
}
struct Closed {
segment: Option<Segment>,
clean: bool,
}
impl Segments {
fn lock(&self) -> MutexGuard<'_, Slots> {
self.slots.lock().expect("pipeline slots poisoned")
}
fn open(&self, members: u32) -> (u32, u32) {
let mut slots = self.lock();
let slot = slots.free.pop().unwrap_or_else(|| {
slots.segments.push(None);
slots.members.push(0);
slots.clean.push(true);
slots.generations.push(0);
u32::try_from(slots.segments.len() - 1).expect("fewer than 2^32 deliveries in flight")
});
slots.members[slot as usize] = members;
slots.clean[slot as usize] = true;
let generation = slots.generations[slot as usize].wrapping_add(1);
slots.generations[slot as usize] = generation;
drop(slots);
(slot, generation)
}
fn close(&self, slot: u32, acknowledged: bool) -> Option<Closed> {
let mut slots = self.lock();
let at = slot as usize;
slots.clean[at] &= acknowledged;
slots.members[at] = slots.members[at].saturating_sub(1);
if slots.members[at] > 0 {
return None;
}
let closed = Closed {
segment: slots.segments[at].take(),
clean: slots.clean[at],
};
slots.free.push(slot);
drop(slots);
Some(closed)
}
async fn segment(&self, slot: u32, generation: u32) -> Result<Segment, Error> {
if let Some(segment) = self.existing(slot, generation)? {
return Ok(segment);
}
let pipeline = pipeline_on(&self.client);
let segment = if self.atomic {
let pinned = pipeline.with_options(&Options {
cluster_hash: Some(ClusterHash::Custom(self.hash_slot)),
max_redirections: Some(0),
..Options::default()
});
pinned
.custom::<(), Value>(transaction_edge("MULTI", self.hash_slot), Vec::new())
.await?;
Segment::Atomic(pinned)
} else {
Segment::Plain(pipeline)
};
let mut slots = self.lock();
if slots.generations[slot as usize] != generation {
return Err(settled_round());
}
let entry = slots
.segments
.get_mut(slot as usize)
.expect("a round names a slot of its own window");
let segment = entry.get_or_insert(segment).clone();
drop(slots);
Ok(segment)
}
fn existing(&self, slot: u32, generation: u32) -> Result<Option<Segment>, Error> {
let slots = self.lock();
if slots.generations[slot as usize] != generation {
return Err(settled_round());
}
let segment = slots
.segments
.get(slot as usize)
.expect("a round names a slot of its own window")
.clone();
drop(slots);
Ok(segment)
}
}
fn settled_round() -> Error {
Error::new(
ErrorKind::InvalidCommand,
"the delivery this pipeline belongs to has settled, so a command can no longer join it",
)
}
#[derive(Clone)]
pub(crate) struct Round {
segments: Arc<Segments>,
slot: u32,
generation: u32,
}
impl Round {
pub(crate) async fn segment(&self) -> Result<Segment, Error> {
self.segments.segment(self.slot, self.generation).await
}
pub(crate) fn same(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.segments, &other.segments)
&& self.slot == other.slot
&& self.generation == other.generation
}
pub(crate) fn enter(&self) {
self.segments.rounds.enter(self);
}
pub(crate) fn bind(&self, publisher: u64) {
self.segments.rounds.bind(self, publisher);
}
pub(crate) async fn xadd(
&self,
key: &str,
fields: Vec<(String, Vec<u8>)>,
) -> Result<(), Error> {
match self.segment().await? {
Segment::Plain(segment) => {
segment
.xadd::<(), _, _, _, _>(key, false, None::<()>, "*", fields)
.await
}
Segment::Atomic(segment) => {
segment
.xadd::<(), _, _, _, _>(key, false, None::<()>, "*", fields)
.await
}
}
}
pub(crate) async fn lpush(
&self,
key: &str,
body: Vec<u8>,
ttl_millis: Option<i64>,
) -> Result<(), Error> {
match self.segment().await? {
Segment::Plain(segment) => {
segment.lpush::<(), _, _>(key, body).await?;
if let Some(ttl) = ttl_millis {
segment.pexpire::<(), _>(key, ttl, None).await?;
}
}
Segment::Atomic(segment) => {
segment.lpush::<(), _, _>(key, body).await?;
if let Some(ttl) = ttl_millis {
segment.pexpire::<(), _>(key, ttl, None).await?;
}
}
}
Ok(())
}
pub(crate) async fn publish(
&self,
channel: &str,
body: Vec<u8>,
sharded: bool,
) -> Result<(), Error> {
match (self.segment().await?, sharded) {
(Segment::Plain(segment), false) => segment.publish::<(), _, _>(channel, body).await,
(Segment::Plain(segment), true) => segment.spublish::<(), _, _>(channel, body).await,
(Segment::Atomic(segment), false) => segment.publish::<(), _, _>(channel, body).await,
(Segment::Atomic(segment), true) => segment.spublish::<(), _, _>(channel, body).await,
}
}
}
pub trait Form: Send + Sync + 'static {
type Message: IncomingMessage + Send + Sync + 'static;
fn pool(msg: &Self::Message) -> &fred::clients::Pool;
#[cfg(feature = "testing")]
fn take_flight(msg: &mut Self::Message) -> InFlight;
type Op: Send + 'static;
fn ack(&self, msg: Self::Message, ops: &mut Vec<Self::Op>) -> Result<(), AckError>;
fn nack(
&self,
msg: Self::Message,
requeue: bool,
ops: &mut Vec<Self::Op>,
) -> Result<(), AckError>;
fn nack_after(
&self,
msg: Self::Message,
delay: std::time::Duration,
ops: &mut Vec<Self::Op>,
) -> Result<(), AckError>;
fn close_atomic(
&self,
segment: &Segment,
ops: &mut Vec<Self::Op>,
) -> impl Future<Output = Result<(), Error>> + Send;
fn send_ops(
&self,
client: &Client,
ops: &mut Vec<Self::Op>,
) -> impl Future<Output = Sent> + Send;
}
#[derive(Debug, Default)]
pub struct Sent {
pub(crate) commands: usize,
pub(crate) failed: usize,
pub(crate) first_error: Option<Error>,
}
impl Sent {
pub(crate) fn record(&mut self, results: Vec<Result<Value, Error>>) {
self.commands += results.len();
for result in results {
if let Err(err) = result {
self.failed += 1;
self.first_error.get_or_insert(err);
}
}
}
fn merge(&mut self, other: Self) {
self.commands += other.commands;
self.failed += other.failed;
if self.first_error.is_none() {
self.first_error = other.first_error;
}
}
}
struct Owed<Op> {
outstanding: usize,
settled: usize,
capacity: usize,
waiting: usize,
committed: Vec<Segment>,
ops: Vec<Op>,
scratch: Vec<Op>,
staged: Vec<Vec<Op>>,
failure: Option<RedisError>,
#[cfg(feature = "testing")]
held: Vec<InFlight>,
#[cfg(feature = "testing")]
flushing: usize,
}
pub(crate) struct Window<F: Form> {
form: F,
segments: Arc<Segments>,
name: Arc<str>,
owed: Mutex<Owed<F::Op>>,
runtime: Handle,
connection: Arc<Connection>,
}
impl<F: Form> Debug for Window<F> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Window")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
struct Flush<Op> {
committed: Vec<Segment>,
ops: Vec<Op>,
}
pub(crate) struct Owner {
pub(crate) rounds: Arc<Rounds>,
pub(crate) runtime: Handle,
pub(crate) connection: Arc<Connection>,
}
impl<F: Form> Window<F> {
pub(crate) fn new(
form: F,
client: Client,
atomic: bool,
name: impl Into<Arc<str>>,
capacity: usize,
owner: Owner,
) -> Self {
let Owner {
rounds,
runtime,
connection,
} = owner;
let name = name.into();
let hash_slot = fred::util::redis_keyslot(name.as_bytes());
Self {
form,
segments: Arc::new(Segments {
client,
rounds,
atomic,
hash_slot,
slots: Mutex::new(Slots::default()),
}),
name,
owed: Mutex::new(Owed {
outstanding: 0,
settled: 0,
capacity: capacity.max(1),
waiting: 0,
committed: Vec::new(),
ops: Vec::new(),
scratch: Vec::new(),
staged: Vec::new(),
failure: None,
#[cfg(feature = "testing")]
held: Vec::new(),
#[cfg(feature = "testing")]
flushing: 0,
}),
runtime,
connection,
}
}
fn owed(&self) -> MutexGuard<'_, Owed<F::Op>> {
self.owed.lock().expect("pipeline window poisoned")
}
pub(crate) fn fill_at(&self, capacity: usize) {
self.owed().capacity = capacity.max(1);
}
pub(crate) fn fetched(&self, n: usize) {
self.owed().outstanding += n;
}
pub(crate) fn discarded(&self, n: usize) {
let mut owed = self.owed();
owed.outstanding = owed.outstanding.saturating_sub(n);
}
pub(crate) fn yielded(&self, waiting: usize) {
let mut owed = self.owed();
owed.outstanding += 1;
owed.waiting = waiting;
}
pub(crate) fn open(&self) -> Round {
self.open_batch(1)
}
pub(crate) fn open_batch(&self, members: u32) -> Round {
let (slot, generation) = self.segments.open(members);
Round {
segments: Arc::clone(&self.segments),
slot,
generation,
}
}
pub(crate) fn take_failure(&self) -> Option<RedisError> {
self.owed().failure.take()
}
pub(crate) async fn settle(
&self,
round: &Round,
commit: bool,
settle: impl FnOnce(&F, &mut Vec<F::Op>) -> Result<(), AckError>,
) -> Result<(), AckError> {
self.connection.ensure_open_to_settle()?;
let closed = self.segments.close(round.slot, commit);
if closed.is_some() {
self.segments.rounds.leave(round);
}
let (mut result, closing) = {
let mut guard = self.owed();
let owed = &mut *guard;
let slot = round.slot as usize;
match closed {
None => {
if owed.staged.len() <= slot {
owed.staged.resize_with(slot + 1, Vec::new);
}
(settle(&self.form, &mut owed.staged[slot]), None)
}
Some(Closed {
segment: Some(segment),
clean: true,
}) if commit => {
let mut scratch = std::mem::take(&mut owed.scratch);
if let Some(staged) = owed.staged.get_mut(slot) {
scratch.append(staged);
}
let result = settle(&self.form, &mut scratch);
drop(guard);
(result, Some((segment, scratch)))
}
Some(_) => {
if let Some(staged) = owed.staged.get_mut(slot) {
owed.ops.append(staged);
}
(settle(&self.form, &mut owed.ops), None)
}
}
};
let mut kept = None;
if let Some((segment, mut scratch)) = closing {
let mut keep = matches!(result, Ok(()) | Err(AckError::Unsupported));
if keep
&& self.segments.atomic
&& let Err(err) = self.close_atomic(&segment, &mut scratch).await
{
keep = false;
result = Err(AckError::Broker(Box::new(err)));
}
kept = Some((keep.then_some(segment), scratch));
}
let flush = {
let mut guard = self.owed();
let owed = &mut *guard;
if let Some((segment, mut scratch)) = kept {
if let Some(segment) = segment {
owed.committed.push(segment);
}
owed.ops.append(&mut scratch);
owed.scratch = scratch;
}
owed.outstanding = owed.outstanding.saturating_sub(1);
owed.settled += 1;
let due = (owed.outstanding == 0 && owed.waiting == 0) || owed.settled >= owed.capacity;
let flush = due.then(|| Self::take(owed));
drop(guard);
flush
};
if let Some(flush) = flush {
self.send(flush).await;
}
result
}
pub(crate) fn abandon(self: &Arc<Self>, round: &Round) {
let closed = self.segments.close(round.slot, false);
if closed.is_some() {
self.segments.rounds.leave(round);
}
let mut guard = self.owed();
let owed = &mut *guard;
if closed.is_some()
&& let Some(staged) = owed.staged.get_mut(round.slot as usize)
{
owed.ops.append(staged);
}
owed.outstanding = owed.outstanding.saturating_sub(1);
let pending = !owed.committed.is_empty() || !owed.ops.is_empty();
let due = pending && owed.outstanding == 0 && owed.waiting == 0;
let flush = due.then(|| Self::take(owed));
drop(guard);
match flush {
Some(flush) => {
let window = Arc::clone(self);
self.runtime.spawn(async move { window.send(flush).await });
}
#[cfg(feature = "testing")]
None => self.release_if_sent(),
#[cfg(not(feature = "testing"))]
None => {}
}
}
#[cfg(feature = "testing")]
pub(crate) fn hold(&self, flight: InFlight) {
self.owed().held.push(flight);
}
#[cfg(feature = "testing")]
fn release_if_sent(&self) {
let released = {
let mut owed = self.owed();
let sent = owed.outstanding == 0
&& owed.flushing == 0
&& owed.ops.is_empty()
&& owed.committed.is_empty()
&& owed.staged.iter().all(Vec::is_empty);
if sent {
std::mem::take(&mut owed.held)
} else {
Vec::new()
}
};
drop(released);
}
pub(crate) fn spawn(&self, task: impl Future<Output = ()> + Send + 'static) {
self.runtime.spawn(task);
}
pub(crate) fn drain(&self) -> impl Future<Output = ()> + Send + '_ {
let flush = Self::take(&mut self.owed());
async move { self.send(flush).await }
}
async fn close_atomic(&self, segment: &Segment, ops: &mut Vec<F::Op>) -> Result<(), Error> {
self.form.close_atomic(segment, ops).await?;
segment
.pipeline()
.custom::<(), Value>(
transaction_edge("EXEC", self.segments.hash_slot),
Vec::new(),
)
.await
}
fn take(owed: &mut Owed<F::Op>) -> Flush<F::Op> {
owed.settled = 0;
#[cfg(feature = "testing")]
{
owed.flushing += 1;
}
Flush {
committed: std::mem::take(&mut owed.committed),
ops: std::mem::take(&mut owed.ops),
}
}
async fn send(&self, mut flush: Flush<F::Op>) {
if flush.committed.is_empty() && flush.ops.is_empty() {
self.give_back(flush);
#[cfg(feature = "testing")]
self.sent_one();
return;
}
let segments = join_all(
flush
.committed
.iter()
.map(|segment| segment.pipeline().try_all::<Value>()),
);
let settles = self.form.send_ops(&self.segments.client, &mut flush.ops);
let (segments, settles) = futures::join!(segments, settles);
let mut commands = Sent::default();
for results in segments {
commands.record(results);
}
let carried = (commands.commands, settles.commands);
commands.merge(settles);
if let Some(error) = commands.first_error {
let failure = RedisError::Flush(format!(
"the window of `{}` carried {} commands and {} settles, and {} of them failed: \
{error}",
self.name, carried.0, carried.1, commands.failed,
));
self.owed().failure.get_or_insert(failure);
}
self.give_back(flush);
#[cfg(feature = "testing")]
self.sent_one();
}
#[cfg(feature = "testing")]
fn sent_one(&self) {
self.owed().flushing -= 1;
self.release_if_sent();
}
fn give_back(&self, mut flush: Flush<F::Op>) {
flush.committed.clear();
flush.ops.clear();
let mut owed = self.owed();
if owed.committed.capacity() < flush.committed.capacity() && owed.committed.is_empty() {
owed.committed = flush.committed;
}
if owed.ops.capacity() < flush.ops.capacity() && owed.ops.is_empty() {
owed.ops = flush.ops;
}
}
}
#[cfg(test)]
mod tests {
use fred::clients::{Client, Pool};
use ruststream::AckError;
use super::*;
use crate::pipeline::forms::PubSubForm;
fn window() -> Arc<Window<PubSubForm>> {
Arc::new(Window::new(
PubSubForm,
Client::default(),
false,
"orders",
8,
Owner {
rounds: Arc::new(Rounds::default()),
runtime: Handle::current(),
connection: Connection::new(
Pool::from_clients(vec![Client::default()]).expect("a pool of one"),
),
},
))
}
#[tokio::test]
async fn a_round_kept_past_its_delivery_joins_no_later_one() {
let window = window();
window.yielded(0);
let first = window.open();
first.segment().await.expect("the first delivery's segment");
window.abandon(&first);
window.yielded(0);
let second = window.open();
assert_eq!(first.slot, second.slot, "the slot is reused");
assert!(
first.segment().await.is_err(),
"a stale round reached the next delivery's segment"
);
assert!(!first.same(&second));
second
.segment()
.await
.expect("the new delivery's own segment");
}
#[tokio::test]
async fn the_last_delivery_abandoned_flushes_what_the_others_settled() {
let window = window();
window.yielded(1);
let settled = window.open();
window.yielded(0);
let dropped = window.open();
settled.segment().await.expect("segment");
let answer = window
.settle(&settled, true, |_, _| Err(AckError::Unsupported))
.await;
assert!(matches!(answer, Err(AckError::Unsupported)));
assert_eq!(
window.owed().committed.len(),
1,
"waits for the other delivery"
);
window.abandon(&dropped);
assert!(
window.owed().committed.is_empty(),
"the acknowledged delivery's commands stayed buffered"
);
}
}