use crate::{
PubSubPush, SentinelConnection, StandaloneConnection,
client::{Config, SentinelConfig},
resp::{Command, RespResponse, cmd},
sleep,
};
use std::time::Duration;
use tracing::{info, warn};
const SWITCH_MASTER_CHANNEL: &[u8] = b"+switch-master";
const FIRST_BACKOFF: Duration = Duration::from_millis(250);
const MAX_BACKOFF: Duration = Duration::from_secs(5);
pub(crate) struct MasterWatch {
sentinel_config: SentinelConfig,
probe_config: Config,
connection: Option<StandaloneConnection>,
backoff: Duration,
}
impl MasterWatch {
pub(crate) fn new(sentinel_config: &SentinelConfig, config: &Config) -> Self {
Self {
probe_config: SentinelConnection::probe_config(sentinel_config, config),
sentinel_config: sentinel_config.clone(),
connection: None,
backoff: Duration::ZERO,
}
}
pub(crate) async fn switched(&mut self) {
loop {
let Some(connection) = &mut self.connection else {
sleep(self.backoff).await;
self.subscribe_to_a_sentinel().await;
continue;
};
match connection.read().await {
Some(Ok(response)) => {
if announces_switch(&response, &self.sentinel_config.service_name) {
return;
}
}
Some(Err(e)) => {
info!("The `+switch-master` subscription failed to read: {e}");
self.lose_connection();
}
None => {
info!("The Sentinel holding the `+switch-master` subscription closed it");
self.lose_connection();
}
}
}
}
fn lose_connection(&mut self) {
self.connection = None;
self.backoff = next_backoff(self.backoff);
}
async fn subscribe_to_a_sentinel(&mut self) {
let instances = self.sentinel_config.instances.clone();
for (host, port) in &instances {
let mut connection = match StandaloneConnection::connect_control(
host,
*port,
&self.probe_config,
)
.await
{
Ok(connection) => connection,
Err(e) => {
info!("Cannot connect to Sentinel {host}:{port} to watch it: {e}");
continue;
}
};
SentinelConnection::learn_fleet(
&mut connection,
&mut self.sentinel_config,
(host, *port),
)
.await;
let subscribe = Command::from(cmd("SUBSCRIBE").arg(SWITCH_MASTER_CHANNEL));
if let Err(e) = connection
.feed(&subscribe, &[])
.await
.and(connection.flush().await)
{
info!("Cannot subscribe to `+switch-master` on {host}:{port}: {e}");
continue;
}
match connection.read().await {
Some(Ok(response)) if !response.is_error() => (),
_ => {
info!("Sentinel {host}:{port} refused the `+switch-master` subscription");
continue;
}
}
info!("Watching `+switch-master` on Sentinel {host}:{port}");
self.connection = Some(connection);
self.backoff = Duration::ZERO;
return;
}
if self.backoff.is_zero() {
warn!(
"No Sentinel accepted the `+switch-master` subscription for `{}`",
self.sentinel_config.service_name
);
}
self.backoff = next_backoff(self.backoff);
}
}
fn announces_switch(response: &RespResponse, service: &str) -> bool {
match PubSubPush::try_from(response) {
Ok(PubSubPush::Message(channel, payload)) => {
channel == SWITCH_MASTER_CHANNEL && names_service(payload, service)
}
_ => false,
}
}
fn names_service(payload: &[u8], service: &str) -> bool {
payload.split(|byte| *byte == b' ').next() == Some(service.as_bytes())
}
fn next_backoff(current: Duration) -> Duration {
if current.is_zero() {
FIRST_BACKOFF
} else {
current.saturating_mul(2).min(MAX_BACKOFF)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::indexing_slicing,
reason = "test code: a panic is how a test reports failure"
)]
use super::{Duration, FIRST_BACKOFF, MAX_BACKOFF, names_service, next_backoff};
#[test]
fn an_announcement_for_this_service_is_ours() {
assert!(names_service(
b"mymaster 127.0.0.1 6379 127.0.0.1 6380",
"mymaster"
));
}
#[test]
fn an_announcement_for_another_service_is_not() {
assert!(!names_service(
b"othermaster 127.0.0.1 6379 127.0.0.1 6380",
"mymaster"
));
}
#[test]
fn a_service_name_is_matched_whole() {
assert!(!names_service(
b"mymaster2 127.0.0.1 6379 127.0.0.1 6380",
"mymaster"
));
assert!(!names_service(
b"mymaster 127.0.0.1 6379 127.0.0.1 6380",
"mymaster2"
));
}
#[test]
fn an_empty_payload_announces_nothing() {
assert!(!names_service(b"", "mymaster"));
}
#[test]
fn the_first_reattempt_waits_the_floor_rather_than_nothing() {
assert_eq!(FIRST_BACKOFF, next_backoff(Duration::ZERO));
}
#[test]
fn a_repeated_failure_doubles_its_wait_up_to_the_cap() {
assert_eq!(FIRST_BACKOFF * 2, next_backoff(FIRST_BACKOFF));
assert_eq!(MAX_BACKOFF, next_backoff(MAX_BACKOFF));
assert_eq!(MAX_BACKOFF, next_backoff(Duration::from_secs(3)));
}
}