1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-23 23:51:06 +01:00

[actix-session/redis-rs] Catch server-driven disconnections and prevent a user-visible issue via a retry (#235)

Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
Luca Palmieri 2022-03-25 19:08:09 +00:00 committed by GitHub
parent 4d77e26e1e
commit 04f4934001
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 59 additions and 25 deletions

View File

@ -2,10 +2,12 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
- Implement `SessionExt` for `GuardContext`. [#234] - Implement `SessionExt` for `GuardContext`. [#234]
- `RedisSessionStore` will prevent connection timeouts from causing user-visible errors. [#235]
- Do not leak internal implementation details to callers when errors occur. [#236] - Do not leak internal implementation details to callers when errors occur. [#236]
[#234]: https://github.com/actix/actix-extras/pull/234 [#234]: https://github.com/actix/actix-extras/pull/234
[#236]: https://github.com/actix/actix-extras/pull/236 [#236]: https://github.com/actix/actix-extras/pull/236
[#235]: https://github.com/actix/actix-extras/pull/235
## 0.6.1 - 2022-03-21 ## 0.6.1 - 2022-03-21

View File

@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use redis::{aio::ConnectionManager, AsyncCommands, Value}; use redis::{aio::ConnectionManager, Cmd, FromRedisValue, RedisResult, Value};
use time::{self, Duration}; use time::{self, Duration};
use super::SessionKey; use super::SessionKey;
@ -136,10 +136,9 @@ impl RedisSessionStoreBuilder {
impl SessionStore for RedisSessionStore { impl SessionStore for RedisSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> { async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let value: Option<String> = self let value: Option<String> = self
.client .execute_command(redis::cmd("GET").arg(&[&cache_key]))
.clone()
.get(cache_key)
.await .await
.map_err(Into::into) .map_err(Into::into)
.map_err(LoadError::Other)?; .map_err(LoadError::Other)?;
@ -163,18 +162,16 @@ impl SessionStore for RedisSessionStore {
let session_key = generate_session_key(); let session_key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
redis::cmd("SET") self.execute_command(redis::cmd("SET").arg(&[
.arg(&[ &cache_key,
&cache_key, &body,
&body, "NX", // NX: only set the key if it does not already exist
"NX", // NX: only set the key if it does not already exist "EX", // EX: set expiry
"EX", // EX: set expiry &format!("{}", ttl.whole_seconds()),
&format!("{}", ttl.whole_seconds()), ]))
]) .await
.query_async(&mut self.client.clone()) .map_err(Into::into)
.await .map_err(SaveError::Other)?;
.map_err(Into::into)
.map_err(SaveError::Other)?;
Ok(session_key) Ok(session_key)
} }
@ -191,15 +188,14 @@ impl SessionStore for RedisSessionStore {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let v: redis::Value = redis::cmd("SET") let v: redis::Value = self
.arg(&[ .execute_command(redis::cmd("SET").arg(&[
&cache_key, &cache_key,
&body, &body,
"XX", // XX: Only set the key if it already exist. "XX", // XX: Only set the key if it already exist.
"EX", // EX: set expiry "EX", // EX: set expiry
&format!("{}", ttl.whole_seconds()), &format!("{}", ttl.whole_seconds()),
]) ]))
.query_async(&mut self.client.clone())
.await .await
.map_err(Into::into) .map_err(Into::into)
.map_err(UpdateError::Other)?; .map_err(UpdateError::Other)?;
@ -227,10 +223,7 @@ impl SessionStore for RedisSessionStore {
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> { async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.execute_command(redis::cmd("DEL").arg(&[&cache_key]))
self.client
.clone()
.del(&cache_key)
.await .await
.map_err(Into::into) .map_err(Into::into)
.map_err(UpdateError::Other)?; .map_err(UpdateError::Other)?;
@ -239,6 +232,45 @@ impl SessionStore for RedisSessionStore {
} }
} }
impl RedisSessionStore {
/// Execute Redis command and retry once in certain cases.
///
/// `ConnectionManager` automatically reconnects when it encounters an error talking to Redis.
/// The request that bumped into the error, though, fails.
///
/// This is generally OK, but there is an unpleasant edge case: Redis client timeouts. The
/// server is configured to drop connections who have been active longer than a pre-determined
/// threshold. `redis-rs` does not proactively detect that the connection has been dropped - you
/// only find out when you try to use it.
///
/// This helper method catches this case (`.is_connection_dropped`) to execute a retry. The
/// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for
/// a different more meaningful reason).
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
let mut can_retry = true;
loop {
match cmd.query_async(&mut self.client.clone()).await {
Ok(value) => return Ok(value),
Err(err) => {
if can_retry && err.is_connection_dropped() {
tracing::debug!(
"Connection dropped while trying to talk to Redis. Retrying."
);
// Retry at most once
can_retry = false;
continue;
} else {
return Err(err);
}
}
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::collections::HashMap; use std::collections::HashMap;