Repository navigation
fix(node): close() detaches a standalone client with a blocking command in flight #7244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d99d79a
b52f0f5
776581c
e7fd09c
5acf57d
ab4f908
b7e93f9
7a2890e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,6 +121,9 @@ struct ConnectionBackend { | |
| connection_info: RwLock<redis::Client>, | ||
| /// Once this flag is set, the internal connection needs no longer try to reconnect to the server, because all the outer clients were dropped. | ||
| client_dropped_flagged: AtomicBool, | ||
| /// Notified by [`ReconnectingConnection::kill`]. The reconnect task races its probe against | ||
| /// this so a connection opened during a reconnect is closed as soon as the client is killed. | ||
| kill_notify: Notify, | ||
| /// Optional handle to the IAM token cache for refreshing the password before reconnection. | ||
| iam_token_handle: Option<IAMTokenHandle>, | ||
| /// Optional handle to the reloaded mTLS certificate cache for refreshing the | ||
|
|
@@ -372,6 +375,7 @@ impl ReconnectingConnection { | |
| connection_info: RwLock::new(connection_info), | ||
| connection_available_signal: ManualResetEvent::new(true), | ||
| client_dropped_flagged: AtomicBool::new(false), | ||
| kill_notify: Notify::new(), | ||
| iam_token_handle, | ||
| cert_material_handle, | ||
| }; | ||
|
|
@@ -404,13 +408,39 @@ impl ReconnectingConnection { | |
| } | ||
|
|
||
| pub(super) fn mark_as_dropped(&self) { | ||
| // Update the telemetry for each connection that is dropped. A dropped connection | ||
| // will not be re-connected, so update the telemetry here | ||
| Telemetry::decr_total_connections(1); | ||
| self.inner | ||
| let was_dropped = self | ||
| .inner | ||
| .backend | ||
| .client_dropped_flagged | ||
| .store(true, Ordering::Relaxed) | ||
| .swap(true, Ordering::Relaxed); | ||
|
|
||
| if !was_dropped { | ||
| // Update the telemetry for each connection that is dropped. A dropped connection | ||
| // will not be re-connected, so update the telemetry here | ||
| Telemetry::decr_total_connections(1); | ||
| } | ||
| } | ||
|
|
||
| /// Marks the connection as dropped and closes the socket right away. | ||
| /// | ||
| /// `mark_as_dropped` alone only stops future reconnects; the socket stays open until the | ||
| /// last `MultiplexedConnection` clone is gone and every in-flight request has been answered. | ||
| /// A blocking command in flight therefore keeps a closed client attached to the server. | ||
| /// Killing the connection makes the server drop the client immediately. | ||
| pub(super) fn kill(&self) { | ||
| self.mark_as_dropped(); | ||
| { | ||
| let guard = self.inner.state.lock().unwrap(); | ||
| if let ConnectionState::Connected(connection) = &*guard { | ||
| connection.kill(); | ||
| } | ||
| } | ||
| // A reconnect in progress owns its new connection until the probe completes; wake it | ||
| // so it closes that connection instead of installing it. | ||
| self.inner.backend.kill_notify.notify_one(); | ||
| // Requests parked in get_connection wait for the reconnect that will never finish. | ||
| // Wake them so they fail instead of hanging. | ||
| self.inner.backend.connection_available_signal.set(); | ||
| } | ||
|
|
||
| pub(super) async fn try_get_connection(&self) -> Option<MultiplexedConnection> { | ||
|
|
@@ -428,13 +458,23 @@ impl ReconnectingConnection { | |
| if let Some(connection) = self.try_get_connection().await { | ||
| return Ok(connection); | ||
| } | ||
| if self.is_dropped() { | ||
| return Err(RedisError::from(( | ||
| redis::ErrorKind::ClientError, | ||
| "Client was closed while waiting for a connection", | ||
| ))); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Attempt to re-connect the connection. | ||
| /// | ||
| /// This function spawns a task to perform the reconnection in the background | ||
| pub(super) fn reconnect(&self, reason: ReconnectReason) { | ||
| if self.is_dropped() { | ||
| log_debug("reconnect", "skipped, client was dropped"); | ||
| return; | ||
| } | ||
| { | ||
| let mut guard = self.inner.state.lock().unwrap(); | ||
| if matches!(*guard, ConnectionState::Reconnecting) { | ||
|
|
@@ -491,7 +531,13 @@ impl ReconnectingConnection { | |
| "ReconnectingConnection", | ||
| "reconnect stopped after client was dropped", | ||
| ); | ||
| // Client was dropped, reconnection attempts can stop | ||
| // Client was dropped, reconnection attempts can stop. Wake requests | ||
| // waiting for this reconnect so they fail instead of hanging. | ||
| connection_clone | ||
| .inner | ||
| .backend | ||
| .connection_available_signal | ||
| .set(); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -545,16 +591,57 @@ impl ReconnectingConnection { | |
| .await | ||
| { | ||
| Ok(mut connection) => { | ||
| if connection | ||
| .send_packed_command(&redis::cmd("PING")) | ||
| .await | ||
| .is_err() | ||
| { | ||
| tokio::time::sleep(sleep_duration).await; | ||
| continue; | ||
| // Until the probe completes, this task is the only owner of the new | ||
| // connection and kill() cannot reach it. Race the probe against the | ||
| // kill signal so a close during the probe closes the socket right away | ||
| // instead of after the server answers the PING. | ||
| let ping = redis::cmd("PING"); | ||
| let probe = tokio::select! { | ||
| biased; | ||
| _ = connection_clone.inner.backend.kill_notify.notified() => None, | ||
| result = connection.send_packed_command(&ping) => Some(result), | ||
| }; | ||
|
Comment on lines
+599
to
+603
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: ast-grep outline glide-core/redis-rs/redis/src/aio --items all \
--type function,method --match 'get_multiplexed_async_connection|setup_connection|new|kill'
rg -n -C 8 \
'get_multiplexed_async_connection|setup_connection|AbortHandle|abort_handle|spawn|impl Drop' \
glide-core/redis-rs/redis/src/aioRepository: valkey-io/valkey-glide Length of output: 42023 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- reconnecting_connection.rs ---'
sed -n '550,635p' glide-core/src/client/reconnecting_connection.rs
printf '%s\n' '--- connection_manager.rs setup caller ---'
sed -n '175,215p' glide-core/redis-rs/redis/src/aio/connection_manager.rs
printf '%s\n' '--- multiplexed setup and constructor ---'
sed -n '700,840p' glide-core/redis-rs/redis/src/aio/multiplexed_connection.rs
sed -n '1020,1115p' glide-core/redis-rs/redis/src/aio/multiplexed_connection.rs
printf '%s\n' '--- pipeline kill and relevant ownership docs ---'
sed -n '950,995p' glide-core/redis-rs/redis/src/aio/multiplexed_connection.rs
printf '%s\n' '--- setup_connection implementation ---'
sed -n '145,205p' glide-core/redis-rs/redis/src/aio/mod.rsRepository: valkey-io/valkey-glide Length of output: 19789 🏁 Script executed: sed -n '570,620p' glide-core/src/client/reconnecting_connection.rs
sed -n '1035,1105p' glide-core/redis-rs/redis/src/aio/multiplexed_connection.rs
sed -n '960,986p' glide-core/redis-rs/redis/src/aio/multiplexed_connection.rsRepository: valkey-io/valkey-glide Length of output: 6689 🏁 Script executed: rg -n -C 14 'get_multiplexed_async_connection_with_timeouts|get_multiplexed_async_connection|get_multiplexed_connection' glide-core/redis-rs/redis/src glide-core/src/client/reconnecting_connection.rsRepository: valkey-io/valkey-glide Length of output: 33420 Cancel reconnect setup on shutdown.
Race shutdown with the full connection-creation future. Dropping that future drops the local driver and stream. If a driver has already been handed off, abort it explicitly. 🤖 Prompt for AI AgentsSource: Learnings |
||
| match probe { | ||
| None => { | ||
| log_debug( | ||
| "reconnect", | ||
| "client was dropped during the probe, closing the new connection", | ||
| ); | ||
| connection.kill(); | ||
| connection_clone | ||
| .inner | ||
| .backend | ||
| .connection_available_signal | ||
| .set(); | ||
| return; | ||
| } | ||
| Some(Err(_)) => { | ||
| // The connection is discarded; abort its driver instead of | ||
| // leaving the socket open until the unanswered PING returns. | ||
| connection.kill(); | ||
| tokio::time::sleep(sleep_duration).await; | ||
| continue; | ||
| } | ||
| Some(Ok(_)) => {} | ||
| } | ||
| { | ||
| let mut guard = connection_clone.inner.state.lock().unwrap(); | ||
| // kill() may have run after the probe completed. It found the | ||
| // state Reconnecting and had nothing to close, so close the new | ||
| // connection here instead of installing it. | ||
| if connection_clone.is_dropped() { | ||
| log_debug( | ||
| "reconnect", | ||
| "client was dropped during reconnect, closing the new connection", | ||
| ); | ||
| connection.kill(); | ||
| connection_clone | ||
| .inner | ||
| .backend | ||
| .connection_available_signal | ||
| .set(); | ||
| return; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
| log_debug("reconnect", "completed successfully"); | ||
| connection_clone | ||
| .inner | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.