use crate::envelope::Limits;
use crate::replica::{self, Care, Lane, Outbox, Received, Stop};
use crate::run::Backoff;
use crate::wire::Codec;
pub use crate::wish::{CarryOut, Direct, Outcome, Wish, Wishes};
use crate::wish::{Offered, Run};
use crate::{Conn, Error};
use sqlx::SqlitePool;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::task::Poll;
use std::time::Duration;
use tokio::sync::{watch, Notify};
use uuid::Uuid;
pub trait Identity: Send + Sync + 'static {
fn id(&self, conn: &mut Conn) -> impl Future<Output = Result<Option<Uuid>, Error>> + Send;
}
impl Identity for Uuid {
async fn id(&self, _conn: &mut Conn) -> Result<Option<Uuid>, Error> {
Ok(Some(*self))
}
}
pub trait Uplink: Send + Sync + 'static {
fn send(
&self,
replica: Uuid,
lane: &'static str,
message: Vec<u8>,
) -> impl Future<Output = Result<(), Error>> + Send;
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Settings {
pub outbox: Outbox,
pub inbound: Limits,
pub lanes: Vec<Lane>,
pub idle: Duration,
pub backoff: Backoff,
pub send_timeout: Duration,
pub digests_first: Duration,
pub digests_every: Duration,
pub wishes: Wishes,
pub wishes_every: Duration,
pub wishes_timeout: Duration,
}
impl Default for Settings {
fn default() -> Self {
Settings {
outbox: Outbox::default(),
inbound: Limits::default(),
lanes: vec![Lane::new("main", &[])],
idle: Duration::from_secs(1),
backoff: Backoff::new(Duration::from_secs(1), Duration::from_secs(60)),
send_timeout: Duration::from_secs(30),
digests_first: Duration::from_secs(5 * 60),
digests_every: Duration::from_secs(60 * 60),
wishes: Wishes::new(),
wishes_every: Duration::from_secs(60),
wishes_timeout: Duration::from_secs(60),
}
}
}
pub struct Replica<P: 'static, I: Identity, U: Uplink> {
pool: SqlitePool,
codecs: Vec<Codec<P>>,
identity: I,
uplink: U,
settings: Settings,
wake: Vec<Notify>,
offer: Notify,
running: AtomicBool,
caps: Vec<AtomicUsize>,
witnessed: Vec<AtomicBool>,
skips: Vec<AtomicUsize>,
#[cfg(test)]
pub(crate) again: AtomicUsize,
}
impl<P: 'static, I: Identity, U: Uplink> std::fmt::Debug for Replica<P, I, U> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let tables: Vec<_> = self.codecs.iter().map(|c| c.table.name).collect();
f.debug_struct("Replica")
.field("tables", &tables)
.field("settings", &self.settings)
.finish_non_exhaustive()
}
}
impl<P: Sync + 'static, I: Identity, U: Uplink> Replica<P, I, U> {
pub async fn new(
pool: SqlitePool,
codecs: Vec<Codec<P>>,
identity: I,
uplink: U,
settings: Settings,
) -> Result<Self, Error> {
let codecs = crate::wish::with_wishes(codecs)?;
replica::check_settings(&codecs, &settings.lanes)?;
settings.wishes.check(&codecs)?;
settings.backoff.check("backoff")?;
let zero = [
("idle", settings.idle),
("send_timeout", settings.send_timeout),
("digests_first", settings.digests_first),
("digests_every", settings.digests_every),
("wishes_every", settings.wishes_every),
("wishes_timeout", settings.wishes_timeout),
("outbox.resend_after", settings.outbox.resend_after),
("outbox.backlog_every", settings.outbox.backlog_every),
];
if let Some((what, _)) = zero.iter().find(|(_, d)| d.is_zero()) {
return Err(Error::Misuse(format!(
"`{what}` is zero: the replica would spin, or say the same thing without end"
)));
}
let mut conn = pool.acquire().await?;
replica::checked(&mut conn, &codecs.iter().collect::<Vec<_>>()).await?;
let offered = replica::retry_set_aside(&mut conn).await?;
if offered > 0 {
tracing::info!(offered, "sync: writes set aside go up again");
}
drop(conn);
Ok(Replica {
pool,
codecs,
identity,
uplink,
caps: settings.lanes.iter().map(|_| AtomicUsize::new(0)).collect(),
witnessed: settings
.lanes
.iter()
.map(|_| AtomicBool::new(false))
.collect(),
skips: settings.lanes.iter().map(|_| AtomicUsize::new(0)).collect(),
wake: settings.lanes.iter().map(|_| Notify::new()).collect(),
offer: Notify::new(),
running: AtomicBool::new(false),
#[cfg(test)]
again: AtomicUsize::new(0),
settings,
})
}
pub async fn receive(&self, bytes: &[u8]) -> Result<Received, Error> {
let mut care = Care::default();
let received = loop {
let mut conn = self.pool.acquire().await?;
let mut tx = sqlx::Connection::begin_with(&mut *conn, "BEGIN IMMEDIATE").await?;
replica::settled(&mut tx).await?;
let limits = &self.settings.inbound;
let stop = match replica::receive(&mut tx, &self.codecs, bytes, limits, &care).await {
Ok(received) => match tx.commit().await {
Ok(()) => break received,
Err(e) => Stop::Failed(e.into()),
},
Err(stop) => {
drop(tx);
stop
}
};
conn.close_on_drop();
let again = match stop {
Stop::Culprits(culprits) => care.refuse(culprits) || care.heed(),
Stop::Failed(e) if e.is_refusal() => care.heed(),
Stop::Failed(e) => return Err(e),
};
#[cfg(test)]
self.again.fetch_add(1, Ordering::Relaxed);
if !again {
return Ok(replica::malformed(
"the database refuses it even with every row of it refused".to_owned(),
));
}
};
if matches!(
received,
Received::Applied(_) | Received::SnapshotEnded | Received::Filed
) {
if let Err(e) = replica::collect(&self.pool, &self.codecs).await {
tracing::warn!(
"sync: retired rows nothing keeps stay, until the next message: {e}"
);
}
}
let wished = match &received {
Received::Applied(applied) => applied.iter().any(|a| a.table == crate::wish::TABLE),
Received::SnapshotEnded => true,
_ => false,
};
if wished {
self.offer_wishes();
}
if matches!(
received,
Received::Applied(_) | Received::SnapshotEnded | Received::Gap
) {
self.nudge();
}
Ok(received)
}
pub fn offer_wishes(&self) {
self.offer.notify_one();
}
pub(crate) async fn carry_out_wishes(&self) -> Result<Offered, Error> {
let Some(me) = self.identity.id(&mut *self.pool.acquire().await?).await? else {
return Ok(Offered::default());
};
let run = Run {
pool: self.pool.clone(),
me,
timeout: self.settings.wishes_timeout,
};
let offered = self.settings.wishes.offer(&run, &self.codecs).await?;
if offered.wrote {
self.nudge();
}
Ok(offered)
}
async fn wishes(&self, mut stopped: watch::Receiver<bool>) {
if !self
.codecs
.iter()
.any(|c| c.table.name == crate::wish::TABLE)
{
return;
}
let first = self.settings.backoff.first;
let mut backoff = first;
while !*stopped.borrow() {
let offered = tokio::select! {
biased;
_ = stopped.wait_for(|stop| *stop) => return,
offered = self.carry_out_wishes() => offered,
};
let wait = match offered {
Ok(offered) => {
backoff = first;
offered.waiting.then_some(self.settings.wishes_every)
}
Err(e) => {
tracing::warn!("sync: wishes not offered, and offered again: {e}");
let wait = backoff;
backoff = self.settings.backoff.after(wait);
Some(wait)
}
};
tokio::select! {
biased;
_ = stopped.wait_for(|stop| *stop) => return,
() = self.offer.notified() => {}
() = async {
match wait {
Some(wait) => tokio::time::sleep(wait).await,
None => std::future::pending().await,
}
} => {}
}
}
}
pub fn nudge(&self) {
for wake in &self.wake {
wake.notify_one();
}
}
pub async fn run(&self, shutdown: impl Future<Output = ()>) {
if self.running.swap(true, Ordering::AcqRel) {
tracing::error!("sync: this replica runs already — a second run returns");
return;
}
let _running = Running(&self.running);
let (stop, stopped) = watch::channel(false);
let mut lanes: Vec<Option<Pin<Box<dyn Future<Output = ()> + Send + '_>>>> =
(0..self.settings.lanes.len())
.map(|lane| Some(Box::pin(self.lane(lane, stopped.clone())) as Pin<Box<_>>))
.chain([Some(Box::pin(self.wishes(stopped.clone())) as Pin<Box<_>>)])
.collect();
let all = std::future::poll_fn(|cx| {
for lane in &mut lanes {
if lane
.as_mut()
.is_some_and(|f| f.as_mut().poll(cx).is_ready())
{
*lane = None;
}
}
if lanes.iter().all(Option::is_none) {
Poll::Ready(())
} else {
Poll::Pending
}
});
tokio::pin!(shutdown, all);
tokio::select! {
() = &mut shutdown => {
let _ = stop.send(true);
all.await;
}
() = &mut all => {}
}
}
async fn lane(&self, lane: usize, mut stopped: watch::Receiver<bool>) {
let first = self.settings.backoff.first;
let mut backoff = first;
while !*stopped.borrow() {
let (wait, failed) = match self.turn(lane).await {
Ok(went) => {
backoff = first;
if went.message {
continue;
}
(self.settings.idle, false)
}
Err(e) => {
tracing::warn!(
lane = self.settings.lanes[lane].name,
"sync: what this replica has to say did not go up, and goes again: {e}"
);
let wait = backoff;
backoff = self.settings.backoff.after(wait);
(wait, true)
}
};
tokio::select! {
biased;
_ = stopped.wait_for(|stop| *stop) => return,
() = self.wake[lane].notified(), if !failed => {}
() = tokio::time::sleep(wait) => {}
}
}
}
async fn turn(&self, lane: usize) -> Result<Went, Error> {
let settings = &self.settings;
let digests = lane == 0
&& replica::digests_due(
&mut *self.pool.acquire().await?,
settings.digests_first,
settings.digests_every,
)
.await?;
self.send_lane(lane, digests).await
}
pub(crate) async fn send_lane(&self, lane: usize, digests: bool) -> Result<Went, Error> {
let Some(id) = self.identity.id(&mut *self.pool.acquire().await?).await? else {
return Ok(Went::default());
};
let cap = self.caps[lane].load(Ordering::Relaxed);
let skip = self.skips[lane].load(Ordering::Relaxed);
let held = if digests {
let mut tx = self.pool.begin().await?;
let held = replica::held(&mut tx, &self.codecs).await?;
tx.commit().await?;
Some(held)
} else {
None
};
let mut tx = self.pool.begin_with("BEGIN IMMEDIATE").await?;
replica::identified(&mut tx, id).await?;
let outgoing = replica::outbox(
&mut tx,
&self.codecs,
&self.settings.lanes,
lane,
&self.settings.outbox,
held,
(cap > 0).then_some(replica::Window { take: cap, skip }),
)
.await?;
tx.commit().await?;
let witnessed = &self.witnessed[lane];
let Some(outgoing) = outgoing.filter(|o| skip == 0 || o.items() > 0) else {
witnessed.store(false, Ordering::Relaxed);
if skip > 0 {
self.skips[lane].store(0, Ordering::Relaxed);
self.caps[lane].store(0, Ordering::Relaxed);
tracing::error!(
lane = self.settings.lanes[lane].name,
"sync: the uplink refuses what this lane sends for good, and has no other \
row to try it with — nothing is set aside, and it goes again"
);
return Err(Error::transport(
true,
"the uplink refuses the only messages this lane has".to_owned(),
));
}
return Ok(Went::default());
};
let timeout = self.settings.send_timeout;
let sent = tokio::time::timeout(
timeout,
self.uplink.send(id, outgoing.lane, outgoing.bytes()),
)
.await
.unwrap_or_else(|_| {
Err(Error::transport(
true,
format!("the uplink did not take the message within {timeout:?}"),
))
});
let mut went = Went {
message: true,
digests: false,
};
let items = outgoing.items();
match sent {
Ok(()) => {
let mut tx = self.pool.begin_with("BEGIN IMMEDIATE").await?;
replica::delivered(&mut tx, &outgoing).await?;
went.digests = !outgoing.up.digests.is_empty();
if went.digests {
replica::digested(&mut tx, self.settings.digests_every).await?;
}
tx.commit().await?;
witnessed.store(cap > 0, Ordering::Relaxed);
self.skips[lane].store(0, Ordering::Relaxed);
self.caps[lane].store(0, Ordering::Relaxed);
}
Err(e) if !e.is_transient() && items > 1 => {
tracing::warn!(
lane = outgoing.lane,
items,
"sync: the uplink refuses a message for good — half of it goes, to find why: {e}"
);
self.caps[lane].store(items / 2, Ordering::Relaxed);
}
Err(e) if !e.is_transient() && items == 1 && witnessed.load(Ordering::Relaxed) => {
tracing::error!(
lane = outgoing.lane,
"sync: the uplink refuses a row or a rejection for good — set aside: {e}"
);
let mut tx = self.pool.begin_with("BEGIN IMMEDIATE").await?;
replica::refused(&mut tx, &outgoing, &e.to_string()).await?;
tx.commit().await?;
witnessed.store(false, Ordering::Relaxed);
self.skips[lane].store(0, Ordering::Relaxed);
self.caps[lane].store(0, Ordering::Relaxed);
}
Err(e) if !e.is_transient() && items == 1 => {
tracing::warn!(
lane = outgoing.lane,
"sync: the uplink refuses a row for good, and has taken nothing else of this \
lane meanwhile — another goes, to see whether it is the row: {e}"
);
self.skips[lane].store(if skip == 0 { 1 } else { skip * 2 }, Ordering::Relaxed);
self.caps[lane].store(1, Ordering::Relaxed);
}
Err(e) => {
witnessed.store(false, Ordering::Relaxed);
if !e.is_transient() {
tracing::error!(
lane = outgoing.lane,
items,
"sync: the uplink refuses for good a message that carries no row — \
nothing to set aside, and it goes again: {e}"
);
}
return Err(e);
}
}
Ok(went)
}
}
struct Running<'a>(&'a AtomicBool);
impl Drop for Running<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Went {
pub(crate) message: bool,
pub(crate) digests: bool,
}
#[cfg(test)]
#[path = "replica_tests.rs"]
mod tests;