use super::pub_sub_push::PubSubPush;
use crate::{
ClientError, Connection, ConnectionState, Error, ErrorKind, JoinHandle, ReconnectionState,
RedisError, RedisErrorKind, Result, RetryReason,
client::{Config, Message, MessageKind, PreparedCommand},
commands::InternalPubSubCommands,
resp::{
ClientReplyMode, CommandKind, RespResponse, RespView, StateSlot, SubscriptionType, cmd,
},
spawn, timeout,
};
use bytes::Bytes;
use futures_util::{FutureExt, select};
use smallvec::SmallVec;
use std::{
collections::{HashMap, VecDeque},
future::poll_fn,
sync::Arc,
task::Poll,
time::Duration,
};
use tokio::{sync::broadcast, time::Instant};
use tracing::{Instrument, debug, error, info, info_span, trace, warn};
pub(crate) type MsgSender = tokio::sync::mpsc::UnboundedSender<Message>;
pub(crate) type MsgReceiver = tokio::sync::mpsc::UnboundedReceiver<Message>;
type WeakMsgSender = tokio::sync::mpsc::WeakUnboundedSender<Message>;
pub(crate) type ResultSender = tokio::sync::oneshot::Sender<Result<RespResponse>>;
pub(crate) type ResultReceiver = tokio::sync::oneshot::Receiver<Result<RespResponse>>;
pub(crate) type ResultsSender = tokio::sync::oneshot::Sender<Result<Vec<RespResponse>>>;
pub(crate) type ResultsReceiver = tokio::sync::oneshot::Receiver<Result<Vec<RespResponse>>>;
pub(crate) type PubSubSender = crate::client::BoundedSender;
pub(crate) type PubSubReceiver = crate::client::BoundedReceiver;
pub(crate) type PushSender = crate::client::BoundedSender;
pub(crate) type PushReceiver = crate::client::BoundedReceiver;
pub(crate) type ReconnectSender = broadcast::Sender<()>;
pub(crate) type ReconnectReceiver = broadcast::Receiver<()>;
#[cfg(test)]
#[derive(Clone, Default)]
pub(crate) struct SendBatchTestHook {
inject_first_message_reasons: Arc<std::sync::Mutex<VecDeque<Option<Vec<RetryReason>>>>>,
fed_retry_reasons: Arc<std::sync::Mutex<Vec<(String, usize)>>>,
kill_on_read_by_name: Arc<std::sync::Mutex<Option<(String, usize)>>>,
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "test-support code: a panic is how a test reports failure"
)]
impl SendBatchTestHook {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push_injection(&self, reasons: Option<Vec<RetryReason>>) {
self.inject_first_message_reasons
.lock()
.expect("send batch test hook mutex poisoned")
.push_back(reasons);
}
pub(crate) fn fed_retry_reasons(&self) -> Vec<(String, usize)> {
self.fed_retry_reasons
.lock()
.expect("send batch test hook mutex poisoned")
.clone()
}
fn take_injection(&self) -> Option<Vec<RetryReason>> {
self.inject_first_message_reasons
.lock()
.expect("send batch test hook mutex poisoned")
.pop_front()
.flatten()
}
fn record_fed(&self, command_name: String, num_reasons: usize) {
self.fed_retry_reasons
.lock()
.expect("send batch test hook mutex poisoned")
.push((command_name, num_reasons));
}
pub(crate) fn arm_kill_on_read_for(&self, command_name: &str, num_reads: usize) {
*self
.kill_on_read_by_name
.lock()
.expect("send batch test hook mutex poisoned") =
Some((command_name.to_owned(), num_reads));
}
fn take_kill_on_read_for(&self, command_name: &str) -> Option<usize> {
let mut guard = self
.kill_on_read_by_name
.lock()
.expect("send batch test hook mutex poisoned");
if guard.as_ref().is_some_and(|(name, _)| name == command_name) {
return guard.take().map(|(_, num_reads)| num_reads);
}
None
}
}
#[cfg(test)]
#[derive(Clone, Default)]
pub(crate) struct QueueMetricsTestHook {
messages_to_send_high_water: Arc<std::sync::atomic::AtomicUsize>,
messages_to_receive_high_water: Arc<std::sync::atomic::AtomicUsize>,
pub_sub_delivered: Arc<std::sync::atomic::AtomicUsize>,
pub_sub_delivery_failed: Arc<std::sync::atomic::AtomicUsize>,
pub_sub_delivered_bytes: Arc<std::sync::atomic::AtomicUsize>,
push_delivered: Arc<std::sync::atomic::AtomicUsize>,
push_delivery_failed: Arc<std::sync::atomic::AtomicUsize>,
push_delivered_bytes: Arc<std::sync::atomic::AtomicUsize>,
}
#[cfg(test)]
impl QueueMetricsTestHook {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn messages_to_send_high_water(&self) -> usize {
self.messages_to_send_high_water
.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn messages_to_receive_high_water(&self) -> usize {
self.messages_to_receive_high_water
.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn pub_sub_delivered(&self) -> usize {
self.pub_sub_delivered
.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn pub_sub_delivery_failed(&self) -> usize {
self.pub_sub_delivery_failed
.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn pub_sub_delivered_bytes(&self) -> usize {
self.pub_sub_delivered_bytes
.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn push_delivered(&self) -> usize {
self.push_delivered
.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn push_delivery_failed(&self) -> usize {
self.push_delivery_failed
.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn push_delivered_bytes(&self) -> usize {
self.push_delivered_bytes
.load(std::sync::atomic::Ordering::Relaxed)
}
fn record_queue_depths(&self, to_send: usize, to_receive: usize) {
self.messages_to_send_high_water
.fetch_max(to_send, std::sync::atomic::Ordering::Relaxed);
self.messages_to_receive_high_water
.fetch_max(to_receive, std::sync::atomic::Ordering::Relaxed);
}
fn record_pub_sub_delivered(&self, bytes: usize) {
self.pub_sub_delivered
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.pub_sub_delivered_bytes
.fetch_add(bytes, std::sync::atomic::Ordering::Relaxed);
}
fn record_pub_sub_delivery_failed(&self) {
self.pub_sub_delivery_failed
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
fn record_push_delivered(&self, bytes: usize) {
self.push_delivered
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.push_delivered_bytes
.fetch_add(bytes, std::sync::atomic::Ordering::Relaxed);
}
fn record_push_delivery_failed(&self) {
self.push_delivery_failed
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Status {
Disconnected,
Connected,
EnteringMonitor,
Monitor,
LeavingMonitor,
}
struct MessageToSend {
pub message: Message,
}
impl MessageToSend {
pub(crate) fn new(message: Message) -> Self {
Self { message }
}
}
#[derive(Debug)]
struct MessageToReceive {
pub message: Message,
pub num_commands: usize,
pub pending_responses: Vec<RespResponse>,
}
impl MessageToReceive {
pub(crate) fn new(message: Message, num_commands: usize) -> Self {
Self {
message,
num_commands,
pending_responses: Vec::with_capacity(num_commands),
}
}
}
struct PendingSubscription {
pub channel_or_pattern: Bytes,
pub subscription_type: SubscriptionType,
pub sender: PubSubSender,
pub more_to_come: bool,
}
pub(crate) struct NetworkHandler {
status: Status,
connection: Connection,
msg_sender: WeakMsgSender,
msg_receiver: MsgReceiver,
messages_to_send: VecDeque<MessageToSend>,
messages_to_receive: VecDeque<MessageToReceive>,
pending_subscriptions: VecDeque<PendingSubscription>,
pending_unsubscriptions: VecDeque<HashMap<Bytes, SubscriptionType>>,
subscriptions: HashMap<Bytes, (SubscriptionType, PubSubSender)>,
orphaned_subscriptions: Vec<(Bytes, SubscriptionType)>,
is_reply_on: bool,
skip_next_reply: bool,
connection_state: ConnectionState,
invalidation_sender: Option<PushSender>,
monitor_sender: Option<PushSender>,
reconnect_sender: ReconnectSender,
auto_resubscribe: bool,
auto_remonitor: bool,
reconnection_state: ReconnectionState,
max_command_attempts: usize,
max_messages_per_wave: usize,
max_queued_bytes: usize,
queued_bytes: usize,
results_to_discard: usize,
#[cfg(test)]
send_batch_test_hook: Option<SendBatchTestHook>,
#[cfg(test)]
queue_metrics_test_hook: Option<QueueMetricsTestHook>,
}
impl NetworkHandler {
pub(crate) async fn connect(
config: Config,
) -> Result<(MsgSender, JoinHandle<()>, ReconnectSender, Arc<str>)> {
config.validate()?;
let auto_resubscribe = config.auto_resubscribe;
let auto_remonitor = config.auto_remonitor;
let max_command_attempts = config.max_command_attempts;
let max_messages_per_wave = config.max_messages_per_wave;
let max_queued_bytes = config.backpressure.max_queued_bytes;
let reconnection_config = config.reconnection.clone();
#[cfg(test)]
let send_batch_test_hook = config.send_batch_test_hook.clone();
#[cfg(test)]
let queue_metrics_test_hook = config.queue_metrics_test_hook.clone();
let mut connection_state = ConnectionState::default();
let connection = Connection::connect(config, &mut connection_state).await?;
let (msg_sender, msg_receiver): (MsgSender, MsgReceiver) =
tokio::sync::mpsc::unbounded_channel();
let (reconnect_sender, _): (ReconnectSender, ReconnectReceiver) = broadcast::channel(32);
let tag = connection.tag().to_owned();
let mut network_handler = NetworkHandler {
status: Status::Connected,
connection,
msg_sender: msg_sender.downgrade(),
msg_receiver,
messages_to_send: VecDeque::new(),
messages_to_receive: VecDeque::new(),
pending_subscriptions: VecDeque::new(),
pending_unsubscriptions: VecDeque::new(),
subscriptions: HashMap::new(),
orphaned_subscriptions: Vec::new(),
is_reply_on: true,
skip_next_reply: false,
connection_state,
invalidation_sender: None,
monitor_sender: None,
reconnect_sender: reconnect_sender.clone(),
auto_resubscribe,
auto_remonitor,
reconnection_state: ReconnectionState::new(reconnection_config),
max_command_attempts,
max_messages_per_wave,
max_queued_bytes,
queued_bytes: 0,
results_to_discard: 0,
#[cfg(test)]
send_batch_test_hook,
#[cfg(test)]
queue_metrics_test_hook,
};
let span = info_span!("connection", tag = %tag);
let join_handle = spawn(
async move {
if let Err(e) = network_handler.network_loop().await {
error!("network loop ended in error: {e}");
}
}
.instrument(span),
);
Ok((msg_sender, join_handle, reconnect_sender, tag))
}
async fn network_loop(&mut self) -> Result<()> {
loop {
select! {
msg = poll_fn(|cx| self.msg_receiver.poll_recv(cx)).fuse() => {
if !self.try_handle_message(msg).await { break; }
},
result = self.connection.read().fuse() => {
if !self.try_handle_result(result).await { break; }
}
}
}
debug!("end of network loop");
Ok(())
}
#[expect(
clippy::arithmetic_side_effects,
reason = "one increment per message taken from the channel in this wave, \
and the wave is cut at `max_messages_per_wave` below."
)]
async fn try_handle_message(&mut self, mut msg: Option<Message>) -> bool {
let mut is_channel_closed = false;
let mut queued: usize = 0;
loop {
if let Some(msg) = msg {
self.handle_message(msg);
queued += 1;
} else {
is_channel_closed = true;
break;
}
if queued >= self.max_messages_per_wave {
if self.status != Status::Disconnected {
self.send_messages().await;
}
queued = 0;
}
match self.msg_receiver.try_recv() {
Ok(m) => msg = Some(m),
Err(_) => {
break;
}
}
}
if self.status != Status::Disconnected {
self.send_messages().await
}
!is_channel_closed
}
#[cfg(test)]
fn record_queue_depths(&self) {
if let Some(hook) = &self.queue_metrics_test_hook {
hook.record_queue_depths(self.messages_to_send.len(), self.messages_to_receive.len());
}
}
fn would_exceed_queue_budget(&self, cost: usize) -> bool {
self.max_queued_bytes != 0
&& self.queued_bytes != 0
&& self.queued_bytes.saturating_add(cost) > self.max_queued_bytes
}
#[expect(
clippy::arithmetic_side_effects,
reason = "the running total counts bytes of buffers that are actually \
allocated and still queued, so it is bounded by the memory \
holding them. Saturating instead would silently desynchronise \
the backpressure accounting from what is really queued."
)]
fn queue_message(&mut self, msg: Message) {
self.queued_bytes += msg.queued_bytes();
self.messages_to_send.push_back(MessageToSend::new(msg));
}
fn handle_message(&mut self, mut msg: Message) {
trace!("[{:?}] Will handle message: {msg:?}", self.status);
let will_be_queued = self.status != Status::Disconnected || msg.retry_on_error;
if will_be_queued
&& msg.attempts == 0
&& !matches!(msg.kind, MessageKind::Invalidation { .. })
&& self.would_exceed_queue_budget(msg.queued_bytes())
{
debug!(
"send queue is full ({} bytes), shedding command: {:?}",
self.queued_bytes,
msg.commands()
);
msg.send_error(Error::from(ClientError::SendQueueFull));
return;
}
let mut collision_error = None;
match &self.status {
Status::Connected => {
match &mut msg.kind {
MessageKind::PubSub {
subscription_type,
subscriptions,
..
} => {
for (channel_or_pattern, _sender) in subscriptions.iter() {
if self.subscriptions.contains_key(channel_or_pattern) {
debug!(
"[{:?}] There is already a subscription on channel `{}`",
self.status,
String::from_utf8_lossy(channel_or_pattern)
);
collision_error = Some(Error::from(ClientError::AlreadySubscribed));
break;
}
}
if collision_error.is_none() {
let subscriptions = std::mem::take(subscriptions);
let last_subscription_index = subscriptions.len().saturating_sub(1);
let pending_subscriptions = subscriptions.into_iter().enumerate().map(
|(index, (channel_or_pattern, sender))| PendingSubscription {
channel_or_pattern,
subscription_type: *subscription_type,
sender,
more_to_come: index < last_subscription_index,
},
);
self.pending_subscriptions.extend(pending_subscriptions);
}
}
MessageKind::Monitor { push_sender, .. } => {
self.status = Status::EnteringMonitor;
let push_sender = push_sender.take();
if let Some(push_sender) = push_sender {
debug!("Registering MONITOR push_sender");
self.monitor_sender = Some(push_sender);
}
}
MessageKind::Invalidation { push_sender } => {
let push_sender = push_sender.take();
if let Some(push_sender) = push_sender {
debug!("Registering Invalidation push_sender");
self.invalidation_sender = Some(push_sender);
}
return; }
MessageKind::Single { command, .. } => {
if let CommandKind::Unsbuscribe(subscription_type) = command.kind() {
self.pending_unsubscriptions.push_back(
command.args().map(|a| (a, *subscription_type)).collect(),
);
}
}
_ => (),
}
if let Some(err) = collision_error {
msg.send_error(err);
} else {
self.queue_message(msg);
}
}
Status::Disconnected => {
if msg.retry_on_error {
debug!(
"network disconnected, queuing command: {:?}",
msg.commands()
);
self.queue_message(msg);
} else {
debug!(
"network disconnected, sending command in error: {:?}",
msg.commands()
);
msg.send_error(Error::from(ErrorKind::DisconnectedByPeer));
}
}
Status::EnteringMonitor => self.queue_message(msg),
Status::Monitor => {
for command in msg.commands() {
if matches!(command.kind(), CommandKind::Reset) {
self.status = Status::LeavingMonitor;
}
}
self.queue_message(msg);
}
Status::LeavingMonitor => {
self.queue_message(msg);
}
}
#[cfg(test)]
self.record_queue_depths();
}
#[expect(
clippy::arithmetic_side_effects,
reason = "both counters add one per command in the send queue: the queue is \
bounded by `max_messages_per_wave` and each command owns an \
allocated buffer, so neither total can approach `usize::MAX`."
)]
async fn send_messages(&mut self) {
#[cfg(test)]
self.record_queue_depths();
if !self.messages_to_send.is_empty() {
let num_commands = self
.messages_to_send
.iter()
.fold(0, |sum, msg| sum + msg.message.num_commands());
if num_commands > 1 {
debug!("sending batch of {num_commands} commands");
}
}
#[cfg(test)]
if let Some(hook) = &self.send_batch_test_hook
&& !self.messages_to_send.is_empty()
&& let Some(reasons) = hook.take_injection()
&& let Some(front) = self.messages_to_send.front_mut()
{
front.message.retry_reasons = Some(reasons);
}
let start_idx = self.messages_to_receive.len();
while let Some(message_to_send) = self.messages_to_send.pop_front() {
let mut msg = message_to_send.message;
self.queued_bytes = self.queued_bytes.saturating_sub(msg.queued_bytes());
let mut retry_reasons = SmallVec::<[RetryReason; 10]>::new();
let reasons = msg.retry_reasons.take();
if let Some(reasons) = reasons {
retry_reasons.extend(reasons);
}
let mut num_commands_to_receive: usize = 0;
for command in msg.commands_mut() {
let kind = *command.kind();
match kind {
CommandKind::ClientReply(ClientReplyMode::On) => {
self.is_reply_on = true;
self.skip_next_reply = false;
self.connection_state.record(StateSlot::ReplyMode, command);
}
CommandKind::ClientReply(ClientReplyMode::Off) => {
self.is_reply_on = false;
self.skip_next_reply = false;
self.connection_state.record(StateSlot::ReplyMode, command);
}
CommandKind::ClientReply(ClientReplyMode::Skip) => {
self.skip_next_reply = true;
}
CommandKind::ConnectionState(slot) => {
self.connection_state.record(slot, command);
}
CommandKind::Reset => {
self.connection_state.clear();
self.is_reply_on = true;
self.skip_next_reply = false;
self.subscriptions.clear();
}
_ => (),
}
if matches!(
kind,
CommandKind::ConnectionState(_)
| CommandKind::ClientReply(_)
| CommandKind::Reset
) {
self.connection
.sync_connection_state(&self.connection_state);
}
let expects_reply = if !self.is_reply_on {
false
} else if matches!(kind, CommandKind::ClientReply(ClientReplyMode::Skip)) {
false
} else if self.skip_next_reply {
self.skip_next_reply = false;
false
} else {
true
};
if expects_reply {
num_commands_to_receive += 1;
}
#[cfg(test)]
if let Some(hook) = &self.send_batch_test_hook {
let command_name = String::from_utf8_lossy(command.name()).into_owned();
hook.record_fed(command_name.clone(), retry_reasons.len());
if let Some(num_reads) = hook.take_kill_on_read_for(&command_name) {
command
.kill_connection_on_read
.store(num_reads, std::sync::atomic::Ordering::SeqCst);
}
}
if let Err(e) = self.connection.feed(command, &retry_reasons).await {
error!("Feed error: {e}");
msg.send_error(e);
return;
}
}
if num_commands_to_receive > 0 {
self.messages_to_receive
.push_back(MessageToReceive::new(msg, num_commands_to_receive));
}
}
if let Err(e) = self.connection.flush().await {
error!("Flush error: {e}");
while self.messages_to_receive.len() > start_idx {
if let Some(msg_to_receive) = self.messages_to_receive.pop_back() {
msg_to_receive.message.send_error(e.clone());
}
}
}
}
async fn try_handle_result(&mut self, result: Option<Result<RespResponse>>) -> bool {
let Some(result) = result else {
return self.reconnect().await;
};
if let Err(e) = &result
&& is_connection_level_error(e)
{
debug!("Connection-level read error, reconnecting: {e}");
return self.reconnect().await;
}
let mut master_demoted = self.master_demoted(&result);
self.handle_result(result);
while let Poll::Ready(result) = self.connection.try_read() {
let Some(result) = result else {
return self.reconnect().await;
};
if let Err(e) = &result
&& is_connection_level_error(e)
{
debug!("Connection-level read error, reconnecting: {e}");
return self.reconnect().await;
}
master_demoted |= self.master_demoted(&result);
self.handle_result(result);
}
if master_demoted {
debug!("The master was demoted to replica, rediscovering it");
return self.reconnect().await;
}
self.unsubscribe_orphaned_subscriptions().await;
true
}
fn master_demoted(&self, result: &Result<RespResponse>) -> bool {
indicates_demoted_master(result) && self.connection.rediscovers_master_on_reconnect()
}
fn dispatch_result<T>(&self, sender: tokio::sync::oneshot::Sender<T>, value: T) {
if sender.send(value).is_err() {
warn!("Cannot send value to caller because receiver is not there anymore");
}
}
fn handle_result(&mut self, result: Result<RespResponse>) {
match self.status {
Status::Disconnected => (),
Status::Connected => match &result {
Ok(response) if response.is_push() => {
if let Some(response) = self.try_match_pubsub_message(result) {
if response.is_err() {
self.receive_result(response);
} else {
match &mut self.invalidation_sender {
Some(push_sender) => {
#[cfg(test)]
let delivered_bytes = response
.as_ref()
.map(|response| response.retained_bytes())
.unwrap_or(0);
let sent = push_sender.send(response);
#[cfg(test)]
if let Some(hook) = &self.queue_metrics_test_hook {
if sent.is_ok() {
hook.record_push_delivered(delivered_bytes);
} else {
hook.record_push_delivery_failed();
}
}
if let Err(e) = sent {
warn!("Cannot send push message result to caller: {e}");
}
}
None => {
warn!(
"Received a push message with no sender configured: {response:?}"
)
}
}
}
}
}
_ => {
self.receive_result(result);
}
},
Status::EnteringMonitor => {
self.receive_result(result);
self.status = Status::Monitor;
}
Status::Monitor => match &result {
Ok(response) if response.is_monitor() => {
self.deliver_monitor_result(result);
}
_ => self.receive_result(result),
},
Status::LeavingMonitor => match &result {
Ok(response) if response.is_monitor() => {
self.deliver_monitor_result(result);
}
_ => {
self.receive_result(result);
self.status = Status::Connected;
}
},
}
}
fn deliver_monitor_result(&self, result: Result<RespResponse>) {
#[cfg(test)]
let delivered_bytes = result
.as_ref()
.map(|response| response.retained_bytes())
.unwrap_or(0);
let Some(push_sender) = &self.monitor_sender else {
return;
};
let sent = push_sender.send(result);
#[cfg(test)]
if let Some(hook) = &self.queue_metrics_test_hook {
if sent.is_ok() {
hook.record_push_delivered(delivered_bytes);
} else {
hook.record_push_delivery_failed();
}
}
if let Err(e) = sent {
warn!("Cannot send monitor result to caller: {e}");
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "each subtraction has its guard on the branch above it: \
`results_to_discard` is only decremented inside `> 0`, and \
`num_commands` is only decremented in the arm where it is neither \
1 nor being resolved — and it never starts at 0, because \
`send_messages` only enqueues a message to receive when it wrote \
at least one command expecting a reply. The retry counter is \
compared against `max_command_attempts` on the next line."
)]
fn receive_result(&mut self, result: Result<RespResponse>) {
if self.results_to_discard > 0 {
self.results_to_discard -= 1;
debug!("discarding response of an already resolved message: {result:?}");
return;
}
match self.messages_to_receive.front_mut() {
Some(message_to_receive) => {
trace!("message_to_receive: {:?}", message_to_receive);
if message_to_receive.num_commands == 1 || result.is_err() {
if let Some(mut message_to_receive) = self.messages_to_receive.pop_front() {
if message_to_receive.num_commands > 1 {
self.results_to_discard += message_to_receive.num_commands - 1;
}
let mut should_retry = false;
if let Err(e) = &result
&& matches!(e.kind(), ErrorKind::Retry(_))
{
should_retry = true;
} else if message_to_receive.message.retry_reasons.is_some() {
should_retry = true;
}
if should_retry {
if let Err(ErrorKind::Retry(reasons)) = result.map_err(Error::into_kind)
{
if let Some(retry_reasons) =
&mut message_to_receive.message.retry_reasons
{
retry_reasons.extend(reasons);
} else {
message_to_receive.message.retry_reasons =
Some(Vec::from_iter(reasons));
}
}
message_to_receive.message.attempts += 1;
if max_attempts_reached(
message_to_receive.message.attempts,
self.max_command_attempts,
) {
debug!(
"Message reached the maximum number of attempts, failing it"
);
message_to_receive.message.send_error(Error::from(
ClientError::MaxCommandAttemptsReached,
));
}
else if let Some(msg_sender) = self.msg_sender.upgrade() {
if let Err(e) = msg_sender.send(message_to_receive.message) {
error!("Cannot retry message: {e}");
}
} else {
debug!("Cannot retry message: channel closed");
}
} else {
trace!("Will respond to: {:?}", message_to_receive.message);
let result = match (result, message_to_receive.message.command_name()) {
(Err(e), Some(command)) => Err(e.with_command(command)),
(result, _) => result,
};
match message_to_receive.message.kind {
MessageKind::Single {
result_sender: Some(result_sender),
..
}
| MessageKind::PubSub { result_sender, .. }
| MessageKind::Monitor { result_sender, .. } => {
self.dispatch_result(result_sender, result);
}
MessageKind::Batch { results_sender, .. } => match result {
Ok(resp_buf) => {
message_to_receive.pending_responses.push(resp_buf);
self.dispatch_result(
results_sender,
Ok(message_to_receive.pending_responses),
);
}
Err(e) => {
self.dispatch_result(results_sender, Err(e));
}
},
MessageKind::Invalidation { .. }
| MessageKind::Single {
result_sender: None,
..
} => {
debug!("forget value {result:?}")
}
}
}
}
} else {
match result {
Ok(value) => {
message_to_receive.pending_responses.push(value);
message_to_receive.num_commands -= 1;
}
Err(e) => {
if let ErrorKind::Retry(reasons) = e.into_kind() {
if let Some(retry_reasons) =
&mut message_to_receive.message.retry_reasons
{
retry_reasons.extend(reasons);
} else {
message_to_receive.message.retry_reasons =
Some(Vec::from_iter(reasons));
}
}
}
}
}
}
None => {
if result.is_ok() {
warn!(
"Dropping an unexpected response with no message awaiting it: {result:?}"
);
}
}
}
}
fn orphan_subscription(&mut self, orphaned: Option<(Bytes, SubscriptionType)>) {
let Some((channel_or_pattern, subscription_type)) = orphaned else {
return;
};
self.subscriptions.remove(&channel_or_pattern);
self.orphaned_subscriptions
.push((channel_or_pattern, subscription_type));
}
async fn unsubscribe_orphaned_subscriptions(&mut self) {
if self.orphaned_subscriptions.is_empty() {
return;
}
for (channel_or_pattern, subscription_type) in
std::mem::take(&mut self.orphaned_subscriptions)
{
let PreparedCommand { mut command, .. } = match subscription_type {
SubscriptionType::Channel => self.connection.unsubscribe(channel_or_pattern),
SubscriptionType::Pattern => self.connection.punsubscribe(channel_or_pattern),
SubscriptionType::ShardChannel => self.connection.sunsubscribe(channel_or_pattern),
};
if self.connection.is_cluster() {
command.compute_slots();
}
self.handle_message(Message::single_forget(command, true));
}
if self.status != Status::Disconnected {
self.send_messages().await;
}
}
fn try_match_pubsub_message(
&mut self,
value: Result<RespResponse>,
) -> Option<Result<RespResponse>> {
if let Ok(ref_value) = &value {
if let Ok(pub_sub_message) = PubSubPush::try_from(ref_value) {
match pub_sub_message {
PubSubPush::Message(channel_or_pattern, _)
| PubSubPush::SMessage(channel_or_pattern, _) => {
#[cfg(test)]
let delivered_bytes = ref_value.retained_bytes();
let orphaned = match self.subscriptions.get_key_value(channel_or_pattern) {
Some((key, (subscription_type, pub_sub_sender))) => {
let sent = pub_sub_sender.send(value);
#[cfg(test)]
if let Some(hook) = &self.queue_metrics_test_hook {
if sent.is_ok() {
hook.record_pub_sub_delivered(delivered_bytes);
} else {
hook.record_pub_sub_delivery_failed();
}
}
match sent {
Ok(()) => None,
Err(e) => {
warn!(
"Cannot send pub/sub message to caller from channel `{}`: {e}",
String::from_utf8_lossy(key)
);
Some((key.clone(), *subscription_type))
}
}
}
None => {
error!(
"Unexpected message on channel `{}`",
String::from_utf8_lossy(channel_or_pattern)
);
None
}
};
self.orphan_subscription(orphaned);
None
}
PubSubPush::Subscribe(channel_or_pattern)
| PubSubPush::PSubscribe(channel_or_pattern)
| PubSubPush::SSubscribe(channel_or_pattern) => {
let matches = self
.pending_subscriptions
.front()
.is_some_and(|p| p.channel_or_pattern == channel_or_pattern);
if matches && let Some(pending_sub) = self.pending_subscriptions.pop_front()
{
if self
.subscriptions
.insert(
pending_sub.channel_or_pattern,
(pending_sub.subscription_type, pending_sub.sender),
)
.is_some()
{
return Some(Err(Error::from(ClientError::AlreadySubscribed)));
}
if pending_sub.more_to_come {
return None;
}
self.receive_result(Ok(RespResponse::ok()));
} else {
error!(
"Unexpected subscription confirmation on channel `{}`",
String::from_utf8_lossy(channel_or_pattern)
);
self.receive_result(Err(Error::from(
ClientError::UnexpectedSubscriptionConfirmation,
)));
}
None
}
PubSubPush::Unsubscribe(channel_or_pattern)
| PubSubPush::PUnsubscribe(channel_or_pattern)
| PubSubPush::SUnsubscribe(channel_or_pattern) => {
self.subscriptions.remove(channel_or_pattern);
if let Some(remaining) = self.pending_unsubscriptions.front_mut() {
if remaining.len() > 1 {
if remaining.remove(channel_or_pattern).is_none() {
error!(
"Cannot find channel or pattern to remove: `{}`",
String::from_utf8_lossy(channel_or_pattern)
);
}
None
} else {
let Some(mut remaining) = self.pending_unsubscriptions.pop_front()
else {
error!(
"Cannot find channel or pattern to remove: `{}`",
String::from_utf8_lossy(channel_or_pattern)
);
return None;
};
if remaining.remove(channel_or_pattern).is_none() {
error!(
"Cannot find channel or pattern to remove: `{}`",
String::from_utf8_lossy(channel_or_pattern)
);
return None;
}
self.receive_result(Ok(RespResponse::ok()));
None
}
} else {
Some(value)
}
}
PubSubPush::PMessage(pattern, channel, _) => {
#[cfg(test)]
let delivered_bytes = ref_value.retained_bytes();
let orphaned = match self.subscriptions.get_key_value(pattern) {
Some((key, (subscription_type, pub_sub_sender))) => {
let sent = pub_sub_sender.send(value);
#[cfg(test)]
if let Some(hook) = &self.queue_metrics_test_hook {
if sent.is_ok() {
hook.record_pub_sub_delivered(delivered_bytes);
} else {
hook.record_pub_sub_delivery_failed();
}
}
match sent {
Ok(()) => None,
Err(e) => {
warn!(
"Cannot send pub/sub message to caller for pattern `{}`: {e}",
String::from_utf8_lossy(key)
);
Some((key.clone(), *subscription_type))
}
}
}
None => {
error!(
"Unexpected message on channel `{}` for pattern `{}`",
String::from_utf8_lossy(channel),
String::from_utf8_lossy(pattern)
);
None
}
};
self.orphan_subscription(orphaned);
None
}
}
} else {
Some(value)
}
} else {
Some(value)
}
}
#[tracing::instrument(name = "reconnect", skip_all)]
#[expect(
clippy::arithmetic_side_effects,
reason = "the retry counter is compared against `max_command_attempts` \
immediately after each increment, and `queued_bytes` accounts \
bytes of buffers that are allocated and requeued — see \
`queue_message`."
)]
async fn reconnect(&mut self) -> bool {
debug!("reconnecting...");
let old_status = self.status;
self.status = Status::Disconnected;
self.results_to_discard = 0;
self.skip_next_reply = false;
self.orphaned_subscriptions.clear();
let max_command_attempts = self.max_command_attempts;
#[cfg(test)]
self.record_queue_depths();
let mut retained_to_receive = VecDeque::with_capacity(self.messages_to_receive.len());
while let Some(mut message_to_receive) = self.messages_to_receive.pop_front() {
if !message_to_receive.message.retry_on_error {
message_to_receive
.message
.send_error(Error::from(ErrorKind::DisconnectedByPeer));
} else {
message_to_receive.message.attempts += 1;
if max_attempts_reached(message_to_receive.message.attempts, max_command_attempts) {
message_to_receive
.message
.send_error(Error::from(ClientError::MaxCommandAttemptsReached));
} else {
retained_to_receive.push_back(message_to_receive);
}
}
}
self.messages_to_receive = retained_to_receive;
let mut retained_to_send = VecDeque::with_capacity(self.messages_to_send.len());
self.queued_bytes = 0;
while let Some(mut message_to_send) = self.messages_to_send.pop_front() {
if !message_to_send.message.retry_on_error {
message_to_send
.message
.send_error(Error::from(ErrorKind::DisconnectedByPeer));
} else {
message_to_send.message.attempts += 1;
if max_attempts_reached(message_to_send.message.attempts, max_command_attempts) {
message_to_send
.message
.send_error(Error::from(ClientError::MaxCommandAttemptsReached));
} else {
self.queued_bytes += message_to_send.message.queued_bytes();
retained_to_send.push_back(message_to_send);
}
}
}
self.messages_to_send = retained_to_send;
loop {
if let Some(delay) = self.reconnection_state.next_delay() {
debug!("Waiting {delay} ms before reconnection");
let start = Instant::now();
let end = start
.checked_add(Duration::from_millis(delay))
.or_else(|| start.checked_add(Duration::from_secs(3600)))
.unwrap_or(start);
loop {
let delay = end.duration_since(Instant::now());
let result =
timeout(delay, poll_fn(|cx| self.msg_receiver.poll_recv(cx))).await;
if let Ok(msg) = result {
if !self.try_handle_message(msg).await {
return false;
}
} else {
break;
}
}
} else {
warn!("Max reconnection attempts reached");
while let Some(message_to_receive) = self.messages_to_receive.pop_front() {
message_to_receive
.message
.send_error(Error::from(ErrorKind::DisconnectedByPeer));
}
while let Some(message_to_send) = self.messages_to_send.pop_front() {
message_to_send
.message
.send_error(Error::from(ErrorKind::DisconnectedByPeer));
}
self.queued_bytes = 0;
return false;
}
if let Err(e) = self.connection.reconnect(&mut self.connection_state).await {
error!("Failed to reconnect: {e:?}");
continue;
}
self.is_reply_on = self.connection_state.is_reply_on();
if self.auto_resubscribe
&& let Err(e) = self.auto_resubscribe().await
{
error!("Failed to reconnect: {e:?}");
continue;
}
if self.auto_remonitor
&& let Err(e) = self.auto_remonitor(old_status).await
{
error!("Failed to reconnect: {e:?}");
continue;
}
if let Err(e) = self.reconnect_sender.send(()) {
debug!("Cannot send reconnect notification to clients: {e}");
}
if let Status::Monitor | Status::EnteringMonitor = old_status {
if self.monitor_sender.is_some() {
self.status = Status::Monitor;
} else {
self.status = Status::Connected;
}
} else {
self.status = Status::Connected;
}
let to_replay: Vec<Message> = std::mem::take(&mut self.messages_to_receive)
.into_iter()
.map(|message_to_receive| message_to_receive.message)
.chain(
std::mem::take(&mut self.messages_to_send)
.into_iter()
.map(|message_to_send| message_to_send.message),
)
.collect();
self.queued_bytes = 0;
for message in to_replay {
self.handle_message(message);
}
self.send_messages().await;
info!("reconnected!");
self.reconnection_state.reset_attempts();
return true;
}
}
async fn auto_resubscribe(&mut self) -> Result<()> {
for map in self.pending_unsubscriptions.drain(..) {
for channel_or_pattern in map.into_keys() {
self.subscriptions.remove(&channel_or_pattern);
}
}
if !self.subscriptions.is_empty() {
for (channel_or_pattern, (subscription_type, _)) in &self.subscriptions {
match subscription_type {
SubscriptionType::Channel => {
self.connection.subscribe(channel_or_pattern).await?;
}
SubscriptionType::Pattern => {
self.connection.psubscribe(channel_or_pattern).await?;
}
SubscriptionType::ShardChannel => {
self.connection.ssubscribe(channel_or_pattern).await?;
}
}
}
}
if !self.pending_subscriptions.is_empty() {
for pending_sub in self.pending_subscriptions.drain(..) {
match pending_sub.subscription_type {
SubscriptionType::Channel => {
self.connection
.subscribe(pending_sub.channel_or_pattern.clone())
.await?;
}
SubscriptionType::Pattern => {
self.connection
.psubscribe(pending_sub.channel_or_pattern.clone())
.await?;
}
SubscriptionType::ShardChannel => {
self.connection
.ssubscribe(pending_sub.channel_or_pattern.clone())
.await?;
}
}
self.subscriptions.insert(
pending_sub.channel_or_pattern,
(pending_sub.subscription_type, pending_sub.sender),
);
}
}
Ok(())
}
async fn auto_remonitor(&mut self, old_status: Status) -> Result<()> {
if let Status::Monitor | Status::EnteringMonitor = old_status {
self.connection.send(&cmd("MONITOR").into()).await?;
}
Ok(())
}
}
#[inline]
fn is_connection_level_error(error: &Error) -> bool {
match error.kind() {
ErrorKind::IO(_) | ErrorKind::EOF => true,
ErrorKind::Client(client_error) => client_error.is_framing_error(),
_ => false,
}
}
#[inline]
fn indicates_demoted_master(result: &Result<RespResponse>) -> bool {
match result {
Ok(response) => {
response.is_error()
&& matches!(response.view(), Ok(RespView::Error(message))
if matches!(RedisError::try_from(message),
Ok(error) if error.kind == RedisErrorKind::Readonly))
}
Err(e) => {
matches!(e.kind(), ErrorKind::Redis(error) if error.kind == RedisErrorKind::Readonly)
}
}
}
#[inline]
fn max_attempts_reached(attempts: usize, cap: usize) -> bool {
cap != 0 && attempts >= cap
}
#[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::{indicates_demoted_master, is_connection_level_error, max_attempts_reached};
#[test]
fn zero_cap_is_unlimited() {
assert!(!max_attempts_reached(1, 0));
assert!(!max_attempts_reached(1_000_000, 0));
}
#[test]
fn cap_reached_at_or_above_limit() {
assert!(!max_attempts_reached(2, 3));
assert!(max_attempts_reached(3, 3));
assert!(max_attempts_reached(4, 3));
}
use crate::{ClientError, Error, ErrorKind, RedisError, RedisErrorKind};
#[test]
fn per_message_errors_are_not_connection_level() {
assert!(!is_connection_level_error(&Error::from(ErrorKind::Retry(
Default::default()
))));
assert!(!is_connection_level_error(&Error::from(ErrorKind::Redis(
RedisError {
kind: RedisErrorKind::NoPerm,
description: "no permission".to_owned(),
}
))));
assert!(!is_connection_level_error(&Error::from(
ClientError::CrossSlot
)));
}
#[test]
fn decode_and_transport_errors_are_connection_level() {
assert!(is_connection_level_error(&Error::from(
ClientError::CannotParseInteger
)));
assert!(is_connection_level_error(&Error::from(
ClientError::UnknownRespTag('?')
)));
assert!(is_connection_level_error(&Error::from(
ClientError::MaxNestingDepthExceeded
)));
assert!(is_connection_level_error(&Error::from(ErrorKind::EOF)));
}
#[test]
fn the_public_predicate_agrees_on_every_connection_level_error() {
for error in [
Error::from(ClientError::CannotParseInteger),
Error::from(ClientError::UnknownRespTag('?')),
Error::from(ClientError::MaxNestingDepthExceeded),
Error::from(ClientError::VerbatimStringTooShort),
Error::from(ClientError::BulkLengthTooLarge),
Error::from(ErrorKind::EOF),
Error::from(ErrorKind::IO(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"reset",
)))),
] {
assert!(is_connection_level_error(&error));
assert!(
error.is_connection_error(),
"the handler reconnects on {error:?} but the user is told the connection is fine"
);
}
}
#[test]
fn readonly_is_the_only_demotion_signal() {
assert!(indicates_demoted_master(&Ok(decode_one(
"-READONLY You can't write against a read only replica.\r\n"
))));
assert!(!indicates_demoted_master(&Ok(decode_one(
"-NOPERM no permission\r\n"
))));
assert!(!indicates_demoted_master(&Ok(decode_one("+OK\r\n"))));
assert!(!indicates_demoted_master(&Ok(decode_one(":12\r\n"))));
assert!(indicates_demoted_master(&Err(Error::from(
ErrorKind::Redis(RedisError {
kind: RedisErrorKind::Readonly,
description: "You can't write against a read only replica.".to_owned(),
})
))));
assert!(!indicates_demoted_master(&Err(Error::from(
ErrorKind::Retry(Default::default())
))));
assert!(!indicates_demoted_master(&Err(Error::from(ErrorKind::EOF))));
}
fn decode_one(str: &str) -> crate::resp::RespResponse {
use tokio_util::codec::Decoder;
let mut buf: bytes::BytesMut = str.into();
crate::resp::BufferDecoder::new()
.decode(&mut buf)
.unwrap()
.expect("one complete frame")
}
#[test]
fn readonly_stays_a_per_message_error() {
assert!(!is_connection_level_error(&Error::from(ErrorKind::Redis(
RedisError {
kind: RedisErrorKind::Readonly,
description: "You can't write against a read only replica.".to_owned(),
}
))));
}
}