Files
inbuxa-server/crates/imap/src/op/mod.rs
T
jcoffey-dev 1518c69033 Scale-out storage: PostgreSQL and MySQL read replicas (ST-5 to ST-15)
A data store with readReplicas becomes a replicated store. Writes,
operator-written SQL and everything outside a read scope go to the
primary. JMAP reads before a request's first write, IMAP LIST, STATUS,
SEARCH, SORT and FETCH, POP3 RETR and TOP, DAV GET, PROPFIND and REPORT,
and blob downloads run in a read scope. Only account data (properties,
indexes, change logs, counters, ACLs, blobs, the search index) is read
from a replica; the registry, in-memory values, the task queue and the
rest stay on the primary.

In a scope, the first read picks a replica round-robin among those up
and under the lag limit, and only if it has every change this node has
written or heard of for the scope's accounts: marks come from write
results, the cluster's state-change broadcasts, a sinceState the client
presents, and, with more than one node, Redis. A write inside the scope
sends the rest of it to the primary. A miss on a replica is looked up on
the primary, and a replica error retries the read there and marks the
replica down.

Each node samples lag every second (WAL positions on PostgreSQL; GTID
sets or Seconds_Behind_Source on MySQL), stops reading from a replica
over 5 s and starts again under 2.5 s, and probes a down replica every
10 s. At startup a replica is left out if it's the primary, isn't
read-only, applies out of commit order, or doesn't show a marker written
to the primary within six tries.

replica_tests (postgres, STORE=PostgreSqlReplicated) runs a primary and a
streaming hot standby in containers: tests 9, 10, 12, 13, 14 and 15 pass.
2026-09-19 14:05:59 -07:00

102 lines
2.3 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ::store::query::log::Query;
use imap_proto::ResponseCode;
pub mod acl;
pub mod append;
pub mod authenticate;
pub mod capability;
pub mod close;
pub mod copy_move;
pub mod create;
pub mod delete;
pub mod enable;
pub mod expunge;
pub mod fetch;
pub mod idle;
pub mod list;
pub mod login;
pub mod logout;
pub mod namespace;
pub mod noop;
pub mod quota;
pub mod rename;
pub mod search;
pub mod select;
pub mod status;
pub mod store;
pub mod subscribe;
pub mod thread;
pub mod uidbatches;
trait FromModSeq {
fn from_modseq(modseq: u64) -> Self;
}
trait ToModSeq {
fn to_modseq(&self) -> u64;
}
impl FromModSeq for Query {
fn from_modseq(modseq: u64) -> Self {
if modseq > 0 {
Query::Since(modseq - 1)
} else {
Query::All
}
}
}
impl ToModSeq for u64 {
fn to_modseq(&self) -> u64 {
if *self > 0 { *self + 1 } else { 0 }
}
}
#[macro_export]
macro_rules! spawn_op {
($data:expr, $($code:tt)*) => {
{
// inbuxa: ST-6: the operation keeps its read scope
tokio::spawn(store::backend::scaleout::replica::carry(async move {
let data = &($data);
if let Err(err) = (async {
$($code)*
})
.await
{
let _ = data.write_error(err).await;
}
}));
Ok(())}
};
}
pub trait ImapContext<T> {
fn imap_ctx(self, tag: &str, location: &'static str) -> trc::Result<T>;
}
impl<T> ImapContext<T> for trc::Result<T> {
fn imap_ctx(self, tag: &str, location: &'static str) -> trc::Result<T> {
match self {
Ok(value) => Ok(value),
Err(err) => Err(
if !err.matches(trc::EventType::Imap(trc::ImapEvent::Error)) {
err.ctx(trc::Key::Id, tag.to_string())
.ctx(trc::Key::Details, "Internal Server Error")
.ctx(trc::Key::Code, ResponseCode::ContactAdmin)
.ctx(trc::Key::CausedBy, location)
} else {
err.ctx(trc::Key::Id, tag.to_string())
},
),
}
}
}