use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::proto::{Limits, Proto};
use crate::reply::Out;
use crate::request::{Argv, Step};
use super::super::args::Args;
use super::super::follow::{self, Link};
use super::super::{Flow, Server, Session};
use super::asm::State;
const POLL: Duration = Duration::from_millis(100);
const GLANCE: Duration = Duration::from_millis(2);
const SYNC_TIMEOUT: Duration = Duration::from_secs(60);
const ACK_EVERY: Duration = Duration::from_secs(1);
const RETRY: Duration = Duration::from_millis(200);
const RETRIES: u32 = 300;
pub(super) struct Job {
pub(super) id: String,
pub(super) host: String,
pub(super) port: u16,
pub(super) slots: Vec<(u16, u16)>,
}
pub(super) fn start(server: &Arc<Server>, job: Job) {
let name = yo_alloc::allow(|| format!("yo-import-{}", &job.id[..8]));
let id = job.id.as_bytes().to_vec();
let shared = Arc::clone(server);
let spawned = std::thread::Builder::new()
.name(name)
.spawn(move || drive(&shared, job));
if spawned.is_err() {
server.asm_import_failed(&id, "Failed to start the import thread");
}
}
fn drive(server: &Arc<Server>, job: Job) {
let id = job.id.as_bytes().to_vec();
let mut into = Landing::new(server);
let ended = run(server, &job, &mut into);
super::super::forget_session(server, &mut into.session);
if let Err(why) = ended {
server.asm_import_trim(&job.slots);
server.asm_import_failed(&id, &why);
}
}
fn run(server: &Arc<Server>, job: &Job, into: &mut Landing) -> Told {
let id = job.id.as_bytes();
server.asm_import_trim(&job.slots);
step(server, id, State::Connecting, None)?;
let mut main = dial(server, job, "Main channel")?;
step(server, id, State::AuthReply, None)?;
step(server, id, State::SendHandshake, None)?;
let me = server.cluster_id();
ask(
&mut main,
"Main channel",
&[b"CLUSTER", b"SYNCSLOTS", b"CONF", b"NODE-ID", me.as_bytes()],
b"+OK",
"CLUSTER SYNCSLOTS CONF",
)?;
step(server, id, State::HandshakeReply, None)?;
let held = sync_command(id, &job.slots);
let sync: Vec<&[u8]> = held.iter().map(Vec::as_slice).collect();
let mut tries = 0;
loop {
step(server, id, State::SendSyncslots, None)?;
main.write(&sync).map_err(|e| gone("Main channel", &e))?;
step(server, id, State::SyncslotsReply, None)?;
let said = main
.line(SYNC_TIMEOUT)
.map_err(|e| gone("Main channel", &e))?;
if said == b"+RDBCHANNELSYNCSLOTS" {
break;
}
if said.starts_with(b"-NOTREADY") && tries < RETRIES {
tries += 1;
std::thread::sleep(RETRY);
continue;
}
return Err(blame(
"Main channel - Error reply to CLUSTER SYNCSLOTS SYNC from the source",
&said,
));
}
step(server, id, State::InitRdbchannel, Some(State::Connecting))?;
let mut rdb = dial(server, job, "RDB channel")?;
let at = State::InitRdbchannel;
step(server, id, at, Some(State::AuthReply))?;
step(server, id, at, Some(State::RdbchannelRequest))?;
rdb.write(&[b"CLUSTER", b"SYNCSLOTS", b"RDBCHANNEL", id])
.map_err(|e| gone("RDB channel", &e))?;
step(server, id, at, Some(State::RdbchannelReply))?;
let said = rdb
.line(SYNC_TIMEOUT)
.map_err(|e| gone("RDB channel", &e))?;
if said != b"+SLOTSSNAPSHOT" {
return Err(blame(
"RDB channel - Failed to sync with the source node",
&said,
));
}
step(
server,
id,
State::AccumulateBuf,
Some(State::RdbchannelTransfer),
)?;
snapshot(server, id, &mut rdb, &mut main, into)?;
drop(rdb);
step(server, id, State::ReadyToStream, Some(State::Completed))?;
changes(server, id, &mut main, into)?;
step(server, id, State::Takeover, None)?;
super::take_slots(server, &job.slots).map_err(|e| yo_alloc::allow(|| e.to_string()))?;
server.asm_import_done(id);
Ok(())
}
fn snapshot(
server: &Arc<Server>,
id: &[u8],
rdb: &mut Link,
main: &mut Link,
into: &mut Landing,
) -> Told {
let limits = Limits::default();
let mut argv = Argv::new();
loop {
match argv.decode(rdb.held(), &limits) {
Err(_) => return Err(unreadable("RDB channel")),
Ok(Step::Command { consumed }) => {
let what = control(&argv, rdb.held());
if matches!(what, Control::Write) {
into.apply(server, &argv, rdb.held());
}
rdb.take(consumed);
if matches!(what, Control::SnapshotEof) {
return Ok(());
}
continue;
}
Ok(Step::Incomplete) => {}
}
alive(server, id)?;
rdb.fill(POLL).map_err(|e| gone("RDB channel", &e))?;
main.fill(GLANCE).map_err(|e| gone("Main channel", &e))?;
}
}
fn changes(server: &Arc<Server>, id: &[u8], main: &mut Link, into: &mut Landing) -> Told {
let limits = Limits::default();
let mut argv = Argv::new();
let mut caught_up = false;
let mut acked = Instant::now() - ACK_EVERY;
let mut sent = u64::MAX;
step(server, id, State::StreamingBuf, None)?;
loop {
let idle = match argv.decode(main.held(), &limits) {
Err(_) => return Err(unreadable("Main channel")),
Ok(Step::Command { consumed }) => {
match control(&argv, main.held()) {
Control::StreamEof => {
main.take(consumed);
return Ok(());
}
Control::SnapshotEof | Control::SlotInfo => {
main.take(consumed);
}
Control::Write => {
into.apply(server, &argv, main.held());
main.take(consumed);
applied(server, id, consumed as u64)?;
}
}
false
}
Ok(Step::Incomplete) => {
if !caught_up {
caught_up = true;
step(server, id, State::WaitStreamEof, None)?;
}
true
}
};
if idle || acked.elapsed() >= ACK_EVERY {
let now = applied(server, id, 0)?;
if acked.elapsed() >= ACK_EVERY || now != sent {
acked = Instant::now();
sent = now;
let word = if caught_up {
State::WaitStreamEof
} else {
State::StreamingBuf
};
let at = yo_alloc::allow(|| now.to_string());
main.write(&[
b"CLUSTER",
b"SYNCSLOTS",
b"ACK",
word.word().as_bytes(),
at.as_bytes(),
])
.map_err(|e| gone("Main channel - Failed to send ACK", &e))?;
}
}
if idle {
main.fill(POLL).map_err(|e| gone("Main channel", &e))?;
}
}
}
type Told = core::result::Result<(), String>;
#[derive(Clone, Copy)]
enum Control {
SnapshotEof,
StreamEof,
SlotInfo,
Write,
}
fn control(argv: &Argv, buf: &[u8]) -> Control {
let named = |at: usize, want: &[u8]| {
argv.arg(buf, at)
.is_some_and(|w| w.eq_ignore_ascii_case(want))
};
if argv.len() < 3 || !named(0, b"cluster") || !named(1, b"syncslots") {
return Control::Write;
}
if named(2, b"snapshot-eof") {
return Control::SnapshotEof;
}
if named(2, b"stream-eof") {
return Control::StreamEof;
}
Control::SlotInfo
}
struct Landing {
session: Session,
out: Out,
}
impl Landing {
fn new(server: &Arc<Server>) -> Landing {
let mut session = Session::new(server.next_client());
session.admit(true);
session.serve_master(true);
Landing {
session,
out: Out::new(Proto::Resp2),
}
}
fn apply(&mut self, server: &Server, argv: &Argv, buf: &[u8]) {
loop {
self.out.clear();
let args = Args::new(argv, buf);
if super::super::execute(server, &mut self.session, args, &mut self.out) != Flow::Hold {
return;
}
std::thread::sleep(Duration::from_millis(1));
}
}
}
fn sync_command(id: &[u8], slots: &[(u16, u16)]) -> Vec<Vec<u8>> {
let mut parts: Vec<Vec<u8>> = Vec::with_capacity(4 + slots.len() * 2);
yo_alloc::allow(|| {
parts.push(b"CLUSTER".to_vec());
parts.push(b"SYNCSLOTS".to_vec());
parts.push(b"SYNC".to_vec());
parts.push(id.to_vec());
for (from, to) in slots {
parts.push(from.to_string().into_bytes());
parts.push(to.to_string().into_bytes());
}
});
parts
}
fn applied(server: &Server, id: &[u8], n: u64) -> core::result::Result<u64, String> {
server.asm_import_applied(id, n).ok_or_else(cancelled)
}
fn alive(server: &Server, id: &[u8]) -> Told {
applied(server, id, 0).map(|_| ())
}
fn step(server: &Server, id: &[u8], state: State, rdb: Option<State>) -> Told {
if server.asm_import_at(id, state, rdb) {
Ok(())
} else {
Err(cancelled())
}
}
fn dial(server: &Server, job: &Job, which: &str) -> core::result::Result<Link, String> {
let mut wire = follow::connect(&job.host, job.port).map_err(|e| {
yo_alloc::allow(|| format!("{which} - Failed to connect to source node: {e}"))
})?;
let secret = server.cluster_secret();
ask(
&mut wire,
which,
&[b"AUTH", b"internal connection", secret.as_bytes()],
b"+OK",
"AUTH",
)?;
Ok(wire)
}
fn ask(wire: &mut Link, which: &str, parts: &[&[u8]], want: &[u8], what: &str) -> Told {
let said = wire.command(parts).map_err(|e| gone(which, &e))?;
if said == want {
return Ok(());
}
Err(blame(
&yo_alloc::allow(|| format!("{which} - Error reply to {what} from the source")),
&said,
))
}
fn gone(which: &str, e: &std::io::Error) -> String {
yo_alloc::allow(|| format!("{which} - Failed to sync with source node: {e}"))
}
fn unreadable(which: &str) -> String {
yo_alloc::allow(|| format!("{which} - the source sent something that is not a command"))
}
fn blame(what: &str, said: &[u8]) -> String {
yo_alloc::allow(|| {
let said = String::from_utf8_lossy(said);
let said = said.strip_prefix('-').unwrap_or(&said);
format!("{what}: {said}")
})
}
fn cancelled() -> String {
String::from("the task was cancelled")
}