use std::sync::Arc;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64};
use yo_common::lock::Lock;
use yo_common::{Code, Error, Result};
use yo_kv::value::{Kind, Str};
use yo_kv::{Ask, rdb};
use crate::proto::{Limits, Proto};
use crate::reply::Out;
use crate::request::{Argv, Step};
use super::super::args::Args;
use super::super::clients::Client;
use super::super::pubsub::Envelope;
use super::super::{Server, keyspec, table};
use super::{ID_LEN, key_slot};
const MAX_ARCHIVED: i64 = 32;
const MAX_LAG: i64 = 1024 * 1024;
const WRITE_PAUSE: i64 = 10 * 1000;
const DRAIN: i64 = 60 * 1000;
struct Knobs {
lag: AtomicI64,
pause: AtomicI64,
drain: AtomicI64,
archived: AtomicI64,
}
impl Default for Knobs {
fn default() -> Self {
Self {
lag: AtomicI64::new(MAX_LAG),
pause: AtomicI64::new(WRITE_PAUSE),
drain: AtomicI64::new(DRAIN),
archived: AtomicI64::new(MAX_ARCHIVED),
}
}
}
impl Knobs {
fn of(&self, which: Migration) -> &AtomicI64 {
match which {
Migration::Lag => &self.lag,
Migration::Pause => &self.pause,
Migration::Drain => &self.drain,
Migration::Archived => &self.archived,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Migration {
Lag,
Pause,
Drain,
Archived,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum State {
None,
Canceled,
Completed,
Failed,
WaitRdbChannel,
WaitBgsaveStart,
SendStream,
HandoffPrep,
Handoff,
StreamEof,
StreamingBuf,
WaitStreamEof,
}
impl State {
fn word(self) -> &'static str {
match self {
State::None => "none",
State::Canceled => "canceled",
State::Completed => "completed",
State::Failed => "failed",
State::WaitRdbChannel => "wait-rdbchannel",
State::WaitBgsaveStart => "wait-bgsave-start",
State::SendStream => "send-stream",
State::HandoffPrep => "handoff-prep",
State::Handoff => "handoff",
State::StreamEof => "stream-eof",
State::StreamingBuf => "streaming-buffer",
State::WaitStreamEof => "wait-stream-eof",
}
}
fn dest_word(word: &[u8]) -> Option<State> {
match word {
_ if word.eq_ignore_ascii_case(b"streaming-buffer") => Some(State::StreamingBuf),
_ if word.eq_ignore_ascii_case(b"wait-stream-eof") => Some(State::WaitStreamEof),
_ => None,
}
}
fn streaming(self) -> bool {
matches!(
self,
State::SendStream | State::HandoffPrep | State::Handoff
)
}
fn pausing(self) -> bool {
matches!(self, State::Handoff | State::StreamEof)
}
}
pub(super) struct Task {
id: String,
slots: Vec<(u16, u16)>,
source: Vec<u8>,
dest: Vec<u8>,
import: bool,
state: State,
error: String,
retries: i64,
created: i64,
started: i64,
ended: i64,
main: Option<Arc<Client>>,
rdb: Option<u64>,
sent: u64,
acked: u64,
dest_state: State,
rdb_state: State,
paused: i64,
}
impl Task {
fn slot_words(&self) -> String {
let mut text = String::new();
for (at, (from, to)) in self.slots.iter().enumerate() {
if at > 0 {
text.push(' ');
}
yo_alloc::allow(|| {
use core::fmt::Write as _;
let _ = write!(text, "{from}-{to}");
});
}
text
}
fn report(&self, out: &mut Out) {
out.map(12);
out.bulk(b"id");
out.bulk(self.id.as_bytes());
out.bulk(b"slots");
out.bulk(self.slot_words().as_bytes());
out.bulk(b"source");
out.bulk(&self.source);
out.bulk(b"dest");
out.bulk(&self.dest);
out.bulk(b"operation");
out.bulk(if self.import { b"import" } else { b"migrate" });
out.bulk(b"state");
out.bulk(self.state.word().as_bytes());
out.bulk(b"last_error");
out.bulk(self.error.as_bytes());
out.bulk(b"retries");
out.int(self.retries);
out.bulk(b"create_time");
out.int(self.created);
out.bulk(b"start_time");
out.int(self.started);
out.bulk(b"end_time");
out.int(self.ended);
out.bulk(b"write_pause_ms");
out.int(if self.import || self.state != State::Completed {
0
} else {
self.ended - self.paused
});
}
fn blame(&mut self, why: &str) {
self.error = yo_alloc::allow(|| {
format!(
"{why} (state: {}, rdb_channel_state: {})",
self.state.word(),
self.rdb_state.word()
)
});
}
}
#[derive(Default)]
pub(super) struct Asm {
inner: Lock<Tasks>,
knobs: Knobs,
streaming: AtomicBool,
armed: AtomicU64,
}
#[derive(Default)]
struct Tasks {
live: Option<Task>,
done: Vec<Task>,
}
impl Tasks {
fn finish(&mut self, now: i64, keep: usize) {
let Some(mut task) = self.live.take() else {
return;
};
task.ended = now;
self.done.insert(0, task);
self.done.truncate(keep);
}
}
impl Asm {
pub(super) fn report_all(&self, out: &mut Out) {
let tasks = self.inner.lock();
out.array(usize::from(tasks.live.is_some()) + tasks.done.len());
for task in tasks.live.iter().chain(tasks.done.iter()) {
task.report(out);
}
}
pub(super) fn report_one(&self, id: &[u8], out: &mut Out) {
let tasks = self.inner.lock();
let found = tasks
.live
.iter()
.chain(tasks.done.iter())
.find(|task| task.id.as_bytes() == id);
match found {
Some(task) => {
out.array(1);
task.report(out);
}
None => out.array(0),
}
}
pub(super) fn cancel(&self, id: Option<&[u8]>, now: i64) -> i64 {
let mut tasks = self.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return 0;
};
if id.is_some_and(|want| task.id.as_bytes() != want) {
return 0;
}
task.blame("Cancelled due to user request");
task.state = State::Canceled;
self.retire(&mut tasks, now);
1
}
fn retire(&self, tasks: &mut Tasks, now: i64) {
tasks.finish(now, self.knobs.archived.load(Relaxed).max(1) as usize);
self.streaming.store(false, Relaxed);
}
fn start_stream(&self) {
let mut tasks = self.inner.lock();
if let Some(task) = tasks.live.as_mut()
&& task.state == State::WaitBgsaveStart
{
task.state = State::SendStream;
task.rdb_state = State::Completed;
self.streaming.store(true, Relaxed);
}
}
fn cross_slot(&self, now: i64) {
let mut tasks = self.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return;
};
task.blame("Cancelled due to propagating cross slot command");
task.state = State::Canceled;
self.retire(&mut tasks, now);
}
pub(super) fn forget(&self, conn: u64, now: i64) {
let mut tasks = self.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return;
};
let main = task.main.as_ref().is_some_and(|row| row.id == conn);
if !main && task.rdb != Some(conn) {
return;
}
let which = if main { "Main" } else { "RDB" };
task.blame(&yo_alloc::allow(|| {
format!("{which} channel - Connection with the peer node was lost")
}));
task.state = State::Failed;
self.retire(&mut tasks, now);
}
fn ack(&self, conn: u64, state: State, offset: u64) -> bool {
let mut tasks = self.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return false;
};
if task.import || !task.main.as_ref().is_some_and(|row| row.id == conn) {
return false;
}
task.dest_state = state;
if offset < task.acked {
return false;
}
task.acked = offset;
let lag = self.knobs.lag.load(Relaxed).max(0) as u64;
if task.state == State::SendStream && task.acked + lag >= task.sent {
task.state = State::HandoffPrep;
return true;
}
false
}
fn begin_handoff(&self, now: i64, until: u64) -> bool {
let mut tasks = self.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return false;
};
if task.state != State::HandoffPrep {
return false;
}
task.state = State::Handoff;
task.paused = now;
self.armed.store(until, Relaxed);
true
}
fn end_stream(&self) -> Option<Arc<Client>> {
let mut tasks = self.inner.lock();
let task = tasks.live.as_mut()?;
if task.state != State::Handoff {
return None;
}
task.state = State::StreamEof;
self.streaming.store(false, Relaxed);
task.rdb = None;
task.main.take()
}
fn pause_expired(&self, now: i64, timeout: i64) -> bool {
let mut tasks = self.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return false;
};
if !task.state.pausing() || now - task.paused < timeout {
return false;
}
task.blame(&yo_alloc::allow(|| {
format!(
"Write pause timeout during slot handoff: destination did not take ownership within {timeout} ms."
)
}));
task.state = State::Failed;
self.retire(&mut tasks, now);
true
}
fn pausing(&self) -> bool {
let tasks = self.inner.lock();
tasks.live.as_ref().is_some_and(|task| task.state.pausing())
}
}
impl Server {
pub(crate) fn asm_forget(&self, conn: u64) {
self.cluster.asm.forget(conn, self.now_ms() as i64);
self.asm_relax();
}
pub(super) fn asm_begin_migrate(
&self,
id: &[u8],
dest: &[u8],
slots: Vec<(u16, u16)>,
row: &Arc<Client>,
) -> Result<()> {
let now = self.now_ms() as i64;
let source = self.cluster.map.lock().nodes[0].id.clone();
let dest = if dest.is_empty() {
vec![0u8; ID_LEN]
} else {
dest.to_vec()
};
let mut tasks = self.cluster.asm.inner.lock();
let mut retries = 0;
let mut replace = false;
if let Some(live) = tasks.live.as_ref() {
if live.state != State::Failed {
return Err(Error::new(
Code::Invalid,
"Another ASM task is already in progress",
));
}
if live.id.as_bytes() == id && !live.import && live.slots == slots && live.dest == dest
{
retries = live.retries + 1;
} else {
replace = true;
}
}
if replace {
let live = tasks
.live
.as_mut()
.expect("there is one, or replace is not set");
live.blame("Cancelled due to new migration requested");
live.state = State::Canceled;
self.cluster.asm.retire(&mut tasks, now);
}
tasks.live = Some(Task {
id: yo_alloc::allow(|| String::from_utf8_lossy(id).into_owned()),
slots,
source: source.into_bytes(),
dest,
import: false,
state: State::WaitRdbChannel,
error: String::new(),
retries,
created: now,
started: now,
ended: -1,
main: Some(Arc::clone(row)),
rdb: None,
sent: 0,
acked: 0,
dest_state: State::None,
paused: 0,
rdb_state: State::None,
});
Ok(())
}
pub(super) fn asm_take_rdb_channel(&self, id: &[u8], conn: u64) -> Result<Vec<(u16, u16)>> {
let mut tasks = self.cluster.asm.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return Err(Error::new(
Code::Invalid,
"No slot migration task in progress",
));
};
if task.import || task.state != State::WaitRdbChannel || task.id.as_bytes() != id {
return Err(Error::new(
Code::Invalid,
"Another migration task is already in progress",
));
}
if task.main.is_none() {
return Err(Error::new(
Code::Invalid,
"Main channel connection is not established",
));
}
task.rdb = Some(conn);
task.state = State::WaitBgsaveStart;
Ok(task.slots.clone())
}
pub(super) fn asm_snapshot(&self, slots: &[(u16, u16)]) -> Vec<u8> {
let (image, _at) = self.at_an_instant(|| {
let mut out = Out::with_capacity(Proto::Resp2, 4096);
self.write_snapshot(slots, &mut out);
self.cluster.asm.start_stream();
out.into_inner()
});
image
}
pub(crate) fn propagating(&self) -> bool {
self.replicated() || self.cluster.asm.streaming.load(Relaxed)
}
pub(crate) fn asm_feed(&self, wire: &[u8]) {
if !self.cluster.asm.streaming.load(Relaxed) {
return;
}
let mut argv = Argv::new();
let read = yo_alloc::allow(|| argv.decode(wire, &Limits::DEFAULT));
if !matches!(read, Ok(Step::Command { .. })) {
return;
}
let args = Args::new(&argv, wire);
let Some(spec) = table::lookup(args.name()) else {
return;
};
if !keyspec::takes_keys(spec, args, 0) {
return;
}
let mut slot: Option<u16> = None;
let mut crossed = false;
keyspec::find(spec, args, 0, &mut |run| {
for i in 0..run.count {
let at = run.first + i * run.step;
if at >= args.len() {
continue;
}
let this = key_slot(args.get(at));
match slot {
None => slot = Some(this),
Some(first) if first != this => crossed = true,
Some(_) => {}
}
}
});
if crossed {
self.cluster.asm.cross_slot(self.now_ms() as i64);
return;
}
let Some(slot) = slot else {
return;
};
let mut tasks = self.cluster.asm.inner.lock();
let Some(task) = tasks.live.as_mut() else {
return;
};
if task.import || !task.state.streaming() {
return;
}
if !task
.slots
.iter()
.any(|(from, to)| (*from..=*to).contains(&slot))
{
return;
}
task.sent += wire.len() as u64;
let Some(row) = task.main.clone() else {
return;
};
let shared = yo_alloc::allow(|| Arc::new(wire.to_vec()));
self.post(
row.thread.load(Relaxed),
Envelope::raw(row.conn.load(Relaxed), row.id, shared),
);
}
pub(super) fn asm_ack(&self, conn: u64, state: &[u8], offset: u64) {
let Some(state) = State::dest_word(state) else {
return;
};
if self.cluster.asm.ack(conn, state, offset) {
self.asm_handoff();
}
}
fn asm_handoff(&self) {
let now = self.now_ms();
let timeout = self.migration_knob(Migration::Pause).max(0) as u64;
let until = now.saturating_add(timeout);
if !self.cluster.asm.begin_handoff(now as i64, until) {
return;
}
self.pause(until, false);
let done = self.at_an_instant(|| self.cluster.asm.end_stream()).0;
let Some(row) = done else {
return;
};
let mut out = Out::with_capacity(Proto::Resp2, 64);
out.array(3);
out.bulk(b"CLUSTER");
out.bulk(b"SYNCSLOTS");
out.bulk(b"STREAM-EOF");
let shared = yo_alloc::allow(|| Arc::new(out.into_inner()));
self.post(
row.thread.load(Relaxed),
Envelope::raw(row.conn.load(Relaxed), row.id, shared),
);
}
pub(crate) fn asm_cron(&self) {
let timeout = self.migration_knob(Migration::Pause).max(0);
if self
.cluster
.asm
.pause_expired(self.now_ms() as i64, timeout)
{
self.asm_relax();
}
}
pub(crate) fn asm_relax(&self) {
let armed = self.cluster.asm.armed.load(Relaxed);
if armed == 0 || self.cluster.asm.pausing() {
return;
}
self.cluster.asm.armed.store(0, Relaxed);
self.lift(armed, false);
}
pub(crate) fn migration_knob(&self, which: Migration) -> i64 {
self.cluster.asm.knobs.of(which).load(Relaxed)
}
pub(crate) fn set_migration_knob(&self, which: Migration, value: i64) {
self.cluster.asm.knobs.of(which).store(value, Relaxed);
}
fn write_snapshot(&self, slots: &[(u16, u16)], out: &mut Out) {
let functions = yo_alloc::allow(|| {
let held = self.libraries.lock();
rdb::functions(held.all().iter().map(|l| &*l.code))
});
out.array(4);
out.bulk(b"FUNCTION");
out.bulk(b"RESTORE");
out.bulk(&functions);
out.bulk(b"REPLACE");
let wanted = |slot: u16| slots.iter().any(|(from, to)| (*from..=*to).contains(&slot));
let mut scratch = Out::with_capacity(Proto::Resp2, 4096);
for (at, db) in self.dbs.iter().enumerate() {
if db.is_empty() {
continue;
}
out.array(2);
out.bulk(b"SELECT");
out.bulk_int(at as i64);
let mut mine: Vec<(u16, Vec<u8>)> = Vec::new();
db.keys(|key| {
let slot = key_slot(key);
if wanted(slot) {
mine.push((slot, key.to_vec()));
}
});
mine.sort_unstable();
let mut from = 0;
while from < mine.len() {
let slot = mine[from].0;
let mut to = from;
let mut expires = 0;
scratch.clear();
while to < mine.len() && mine[to].0 == slot {
let key = &mine[to].1;
let mut stripe = db.hold(key);
let deadline = match stripe.deadline_of(key) {
Ask::At(when) => {
expires += 1;
Some(when as i64)
}
_ => None,
};
let mut wrote = stripe.kind_of(key) == Some(Kind::String);
if wrote {
let value = stripe.get(key).ok().flatten();
match value {
Some(value) => {
scratch.array(3);
scratch.bulk(b"SET");
scratch.bulk(key);
match value {
Str::Int(n) => scratch.bulk_int(n),
Str::Bytes(b) => scratch.bulk(b),
}
}
None => wrote = false,
}
if let (true, Some(when)) = (wrote, deadline) {
scratch.array(3);
scratch.bulk(b"PEXPIREAT");
scratch.bulk(key);
scratch.bulk_int(when);
}
} else if let Some(payload) = stripe.dump(key) {
scratch.array(5);
scratch.bulk(b"RESTORE");
scratch.bulk(key);
scratch.bulk_int(deadline.unwrap_or(0));
scratch.bulk(&payload);
scratch.bulk(b"ABSTTL");
}
to += 1;
}
let info = yo_alloc::allow(|| format!("{slot}:{}:{expires}", to - from));
out.array(5);
out.bulk(b"CLUSTER");
out.bulk(b"SYNCSLOTS");
out.bulk(b"CONF");
out.bulk(b"SLOT-INFO");
out.bulk(info.as_bytes());
out.raw(scratch.as_slice());
from = to;
}
}
out.array(3);
out.bulk(b"CLUSTER");
out.bulk(b"SYNCSLOTS");
out.bulk(b"SNAPSHOT-EOF");
}
}
#[cfg(test)]
mod tests {
use yo_kv::End;
use yo_kv::strings::{Expire, SetOptions};
use std::sync::Arc;
use super::super::super::Server;
use super::super::super::clients::Client;
use super::{State, key_slot};
use crate::proto::Proto;
use crate::reply::Out;
const LAG: u64 = super::MAX_LAG as u64;
const KEEP: usize = super::MAX_ARCHIVED as usize;
fn node() -> Server {
let mut server = Server::new();
server.enable_cluster("", 7351);
server
}
fn text(out: &Out) -> String {
String::from_utf8_lossy(out.as_slice()).into_owned()
}
fn set(server: &Server, key: &[u8], value: &[u8], deadline: Option<u64>) {
let mut opts = SetOptions::PLAIN;
if let Some(when) = deadline {
opts.expire = Expire::At(when);
}
server.dbs[0]
.hold(key)
.set(key, value, opts)
.expect("the key is new");
}
fn wire(id: u64) -> Arc<Client> {
Arc::new(Client::new(id))
}
fn snapshot(server: &Server) -> String {
let mut out = Out::with_capacity(Proto::Resp2, 1024);
server.write_snapshot(&[(0, 16383)], &mut out);
text(&out)
}
#[test]
fn an_empty_node_still_sends_the_functions_and_the_end() {
let got = snapshot(&node());
assert!(
got.starts_with("*4\r\n$8\r\nFUNCTION\r\n$7\r\nRESTORE\r\n"),
"{got:?}"
);
assert!(got.ends_with("*3\r\n$7\r\nCLUSTER\r\n$9\r\nSYNCSLOTS\r\n$12\r\nSNAPSHOT-EOF\r\n"));
assert!(!got.contains("SELECT"), "{got:?}");
assert!(!got.contains("SLOT-INFO"), "{got:?}");
}
#[test]
fn a_string_goes_as_a_set_and_its_deadline_follows_it() {
let server = node();
set(&server, b"plain", b"a string", None);
set(&server, b"dated", b"goes", Some(4_000_000_000_000));
let got = snapshot(&server);
assert!(
got.contains("$3\r\nSET\r\n$5\r\nplain\r\n$8\r\na string\r\n"),
"{got:?}"
);
assert!(
got.contains("$3\r\nSET\r\n$5\r\ndated\r\n$4\r\ngoes\r\n"),
"{got:?}"
);
assert!(
got.contains("$9\r\nPEXPIREAT\r\n$5\r\ndated\r\n$13\r\n4000000000000\r\n"),
"{got:?}"
);
assert!(!got.contains("RESTORE\r\n$5\r\ndated"), "{got:?}");
}
#[test]
fn a_collection_goes_as_a_restore() {
let server = node();
server.dbs[0]
.hold(b"list")
.push(b"list", End::Right, [b"a".as_slice(), b"b"].into_iter())
.expect("the key is new");
let got = snapshot(&server);
assert!(
got.contains("$7\r\nRESTORE\r\n$4\r\nlist\r\n$1\r\n0\r\n"),
"{got:?}"
);
assert!(got.contains("$6\r\nABSTTL\r\n"), "{got:?}");
}
#[test]
fn each_slot_is_counted_before_its_keys_arrive() {
let server = node();
set(&server, b"{tag}one", b"1", None);
set(&server, b"{tag}two", b"2", Some(4_000_000_000_000));
let slot = key_slot(b"{tag}one");
let got = snapshot(&server);
let want = format!(
"$9\r\nSLOT-INFO\r\n${}\r\n{slot}:2:1\r\n",
format!("{slot}:2:1").len()
);
assert!(got.contains(&want), "{want:?} in {got:?}");
assert_eq!(got.matches("$6\r\nSELECT\r\n").count(), 1, "{got:?}");
}
#[test]
fn a_key_outside_the_range_is_left_alone() {
let server = node();
set(&server, b"foo", b"in", None);
set(&server, b"bar", b"out", None);
let foo = key_slot(b"foo");
let bar = key_slot(b"bar");
assert_ne!(foo, bar);
let mut out = Out::with_capacity(Proto::Resp2, 1024);
server.write_snapshot(&[(foo, foo)], &mut out);
let got = text(&out);
assert!(got.contains("$3\r\nfoo\r\n"), "{got:?}");
assert!(!got.contains("$3\r\nbar\r\n"), "{got:?}");
}
#[test]
fn a_migration_walks_from_the_sync_to_the_snapshot() {
let server = node();
let id = [b'b'; 40];
let dest = [b'c'; 40];
server
.asm_begin_migrate(&id, &dest, vec![(0, 100)], &wire(7))
.expect("nothing else is running");
let mut out = Out::new(Proto::Resp3);
server.cluster.asm.report_all(&mut out);
let got = text(&out);
assert!(got.contains("wait-rdbchannel"), "{got:?}");
assert!(got.contains("$7\r\nmigrate\r\n"), "{got:?}");
assert!(got.contains("$5\r\n0-100\r\n"), "{got:?}");
let slots = server
.asm_take_rdb_channel(&id, 8)
.expect("the task is waiting for it");
assert_eq!(slots, vec![(0, 100)]);
let image = server.asm_snapshot(&slots);
assert!(image.ends_with(b"$12\r\nSNAPSHOT-EOF\r\n"));
let mut out = Out::new(Proto::Resp3);
server.cluster.asm.report_one(&id, &mut out);
assert!(text(&out).contains("send-stream"), "{:?}", text(&out));
assert!(server.propagating());
}
#[test]
fn only_the_slots_that_are_moving_are_streamed() {
let server = node();
let foo = key_slot(b"foo");
let bar = key_slot(b"bar");
assert_ne!(foo, bar);
server
.asm_begin_migrate(&[b'b'; 40], &[b'c'; 40], vec![(foo, foo)], &wire(7))
.expect("nothing else is running");
server
.asm_take_rdb_channel(&[b'b'; 40], 8)
.expect("the task is waiting for it");
server.asm_snapshot(&[(foo, foo)]);
let sent = |server: &Server| server.cluster.asm.inner.lock().live.as_ref().unwrap().sent;
assert_eq!(sent(&server), 0);
server.asm_feed(b"*3\r\n$3\r\nSET\r\n$3\r\nbar\r\n$1\r\nx\r\n");
assert_eq!(sent(&server), 0, "another slot is not this migration");
server.asm_feed(b"*1\r\n$4\r\nPING\r\n");
assert_eq!(sent(&server), 0, "a command with no key is not sent");
let write = b"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$1\r\nx\r\n";
server.asm_feed(write);
assert_eq!(sent(&server), write.len() as u64);
}
#[test]
fn a_cross_slot_command_ends_the_migration() {
let server = node();
let foo = key_slot(b"foo");
server
.asm_begin_migrate(&[b'b'; 40], &[b'c'; 40], vec![(foo, foo)], &wire(7))
.expect("nothing else is running");
server
.asm_take_rdb_channel(&[b'b'; 40], 8)
.expect("the task is waiting for it");
server.asm_snapshot(&[(foo, foo)]);
server.asm_feed(b"*3\r\n$3\r\nDEL\r\n$3\r\nfoo\r\n$3\r\nbar\r\n");
assert!(!server.propagating());
let mut out = Out::new(Proto::Resp3);
server.cluster.asm.report_one(&[b'b'; 40], &mut out);
let got = text(&out);
assert!(got.contains("canceled"), "{got:?}");
assert!(
got.contains(
"Cancelled due to propagating cross slot command (state: send-stream, rdb_channel_state: completed)"
),
"{got:?}"
);
}
#[test]
fn catching_up_stops_the_writes_and_ends_the_stream() {
let server = node();
let id = [b'b'; 40];
server
.asm_begin_migrate(&id, &[b'c'; 40], vec![(0, 16383)], &wire(7))
.expect("nothing else is running");
server
.asm_take_rdb_channel(&id, 8)
.expect("the task is waiting for it");
server.asm_snapshot(&[(0, 16383)]);
{
let mut tasks = server.cluster.asm.inner.lock();
tasks.live.as_mut().unwrap().sent = LAG * 4;
}
server.asm_ack(7, b"takeover", LAG * 4);
server.asm_ack(9, b"streaming-buffer", LAG * 4);
server.asm_ack(7, b"streaming-buffer", LAG);
let state = |server: &Server| server.cluster.asm.inner.lock().live.as_ref().unwrap().state;
assert_eq!(state(&server), State::SendStream);
server.asm_ack(7, b"streaming-buffer", 0);
assert_eq!(
server.cluster.asm.inner.lock().live.as_ref().unwrap().acked,
LAG
);
server.asm_ack(7, b"wait-stream-eof", LAG * 3);
assert_eq!(state(&server), State::StreamEof);
assert_eq!(server.paused(server.now_ms()), Some(false), "writes only");
assert!(!server.propagating(), "nothing more is sent");
assert!(
server
.cluster
.asm
.inner
.lock()
.live
.as_ref()
.unwrap()
.main
.is_none()
);
server.asm_forget(7);
assert_eq!(state(&server), State::StreamEof);
server.asm_feed(b"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$1\r\nx\r\n");
assert_eq!(
server.cluster.asm.inner.lock().live.as_ref().unwrap().sent,
LAG * 4
);
}
#[test]
fn a_handoff_the_far_side_never_finishes_gives_up() {
let server = node();
let id = [b'b'; 40];
server
.asm_begin_migrate(&id, &[b'c'; 40], vec![(0, 16383)], &wire(7))
.expect("nothing else is running");
server
.asm_take_rdb_channel(&id, 8)
.expect("the task is waiting for it");
server.asm_snapshot(&[(0, 16383)]);
server.asm_ack(7, b"wait-stream-eof", 0);
assert_eq!(server.paused(server.now_ms()), Some(false));
server.asm_cron();
assert_eq!(server.paused(server.now_ms()), Some(false));
{
let mut tasks = server.cluster.asm.inner.lock();
let task = tasks.live.as_mut().unwrap();
task.paused -= super::WRITE_PAUSE + 1;
}
server.asm_cron();
assert_eq!(server.paused(server.now_ms()), None);
let mut out = Out::new(Proto::Resp3);
server.cluster.asm.report_one(&id, &mut out);
let got = text(&out);
assert!(
got.contains(
"Write pause timeout during slot handoff: destination did not take ownership within 10000 ms. (state: stream-eof, rdb_channel_state: completed)"
),
"{got:?}"
);
assert!(got.contains("write_pause_ms\r\n:0\r\n"), "{got:?}");
}
#[test]
fn cancelling_a_handoff_lets_the_writes_go() {
let server = node();
let id = [b'b'; 40];
server
.asm_begin_migrate(&id, &[b'c'; 40], vec![(0, 16383)], &wire(7))
.expect("nothing else is running");
server
.asm_take_rdb_channel(&id, 8)
.expect("the task is waiting for it");
server.asm_snapshot(&[(0, 16383)]);
server.asm_ack(7, b"wait-stream-eof", 0);
assert_eq!(server.paused(server.now_ms()), Some(false));
assert_eq!(
server.cluster.asm.cancel(Some(&id), server.now_ms() as i64),
1
);
server.asm_relax();
assert_eq!(server.paused(server.now_ms()), None);
}
#[test]
fn a_second_migration_is_refused_while_one_is_running() {
let server = node();
server
.asm_begin_migrate(&[b'b'; 40], &[b'c'; 40], vec![(0, 100)], &wire(7))
.expect("nothing else is running");
let err = server
.asm_begin_migrate(&[b'd'; 40], &[b'c'; 40], vec![(200, 300)], &wire(9))
.expect_err("one is running");
assert!(
err.to_string()
.contains("Another ASM task is already in progress")
);
}
#[test]
fn a_retry_of_the_same_move_keeps_its_count() {
let server = node();
let id = [b'b'; 40];
let dest = [b'c'; 40];
server
.asm_begin_migrate(&id, &dest, vec![(0, 100)], &wire(7))
.expect("nothing else is running");
server.asm_forget(7);
{
let tasks = server.cluster.asm.inner.lock();
assert!(tasks.live.is_none());
assert_eq!(tasks.done[0].state, State::Failed);
assert!(
tasks.done[0]
.error
.contains("Connection with the peer node was lost")
);
}
server
.asm_begin_migrate(&id, &dest, vec![(0, 100)], &wire(11))
.expect("the failed one does not block it");
}
#[test]
fn cancelling_counts_once() {
let server = node();
let id = [b'b'; 40];
server
.asm_begin_migrate(&id, &[b'c'; 40], vec![(0, 100)], &wire(7))
.expect("nothing else is running");
assert_eq!(server.cluster.asm.cancel(Some(&[b'z'; 40]), 1), 0);
assert_eq!(server.cluster.asm.cancel(None, 2), 1);
assert_eq!(server.cluster.asm.cancel(None, 3), 0);
let mut out = Out::new(Proto::Resp3);
server.cluster.asm.report_one(&id, &mut out);
let got = text(&out);
assert!(got.contains("canceled"), "{got:?}");
assert!(got.contains("Cancelled due to user request"), "{got:?}");
}
#[test]
fn the_snapshot_connection_has_to_match_the_task() {
let server = node();
let err = server
.asm_take_rdb_channel(&[b'b'; 40], 8)
.expect_err("nothing is running");
assert!(
err.to_string()
.contains("No slot migration task in progress")
);
server
.asm_begin_migrate(&[b'b'; 40], &[b'c'; 40], vec![(0, 100)], &wire(7))
.expect("nothing else is running");
let err = server
.asm_take_rdb_channel(&[b'd'; 40], 8)
.expect_err("that is not the task");
assert!(
err.to_string()
.contains("Another migration task is already in progress")
);
server
.asm_take_rdb_channel(&[b'b'; 40], 8)
.expect("it matches");
let err = server
.asm_take_rdb_channel(&[b'b'; 40], 9)
.expect_err("it has already been taken");
assert!(
err.to_string()
.contains("Another migration task is already in progress")
);
}
#[test]
fn the_finished_list_is_bounded() {
let server = node();
for i in 0..(KEEP + 5) {
let id = format!("{i:040}");
server
.asm_begin_migrate(id.as_bytes(), &[b'c'; 40], vec![(0, 100)], &wire(7))
.expect("the last one was cancelled");
assert_eq!(server.cluster.asm.cancel(None, 1), 1);
}
let mut out = Out::new(Proto::Resp3);
server.cluster.asm.report_all(&mut out);
let got = text(&out);
assert!(got.starts_with(&format!("*{KEEP}\r\n")), "{got:?}");
let newest = format!("{:040}", KEEP + 4);
assert!(got.contains(&newest), "{got:?}");
}
}