Skip to content
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

feature(session): add deadpool-redis compatibility #381

Merged
merged 39 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
20ca707
Add compatibility of `deadpool-redis` for the storage `redis_rs`.
0rangeFox Jan 8, 2024
ab41ca6
Keep up-to-date the `actix-redis` version.
0rangeFox Jan 8, 2024
69195e6
Format the project issued by command `cargo +nightly fmt`.
0rangeFox Jan 8, 2024
99fb626
Add `deadpool-redis` into the documentation and tests.
0rangeFox Jan 8, 2024
8834e3a
Merge branch 'actix:master' into session-deadpool_redis-compatibility
0rangeFox Jan 8, 2024
73348c6
Update CHANGES.md.
0rangeFox Jan 8, 2024
6ef48e0
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jan 12, 2024
3eb98ef
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jan 15, 2024
b8b2dee
Update the documentation of `Deadpool Redis` section on `redis_rs`.
0rangeFox Jan 22, 2024
30b7402
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jan 22, 2024
d4b684c
Replace `no_run` with `ignore` attribute on "Deadpool Redis" example …
0rangeFox Jan 26, 2024
69924bf
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jan 26, 2024
c97e239
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jan 29, 2024
d2f029e
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jan 31, 2024
1b86114
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Feb 5, 2024
90328ab
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Feb 15, 2024
b75c4e1
Rollback the renaming `redis::cmd` to `cmd` for better reading and av…
0rangeFox Feb 15, 2024
e974a2c
Format the project issued by command `cargo +nightly fmt`.
0rangeFox Feb 15, 2024
870cf7e
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Feb 21, 2024
7a391bb
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Mar 5, 2024
2a56691
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Mar 11, 2024
ce0caa3
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Mar 14, 2024
2a83eb3
Format.
0rangeFox Mar 14, 2024
a269380
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Apr 4, 2024
acc99ad
Merge branch 'master' into session-deadpool_redis-compatibility
robjtede Jun 9, 2024
5cb52c8
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jun 10, 2024
201d8e7
Fix feature naming from the last merge.
0rangeFox Jun 10, 2024
e272290
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jun 16, 2024
b396047
Merge branch 'master' into session-deadpool_redis-compatibility
0rangeFox Jul 5, 2024
83f7235
Merge branch 'master' into session-deadpool_redis-compatibility
robjtede Aug 1, 2024
eaba963
Fix feature missing from the last merge.
0rangeFox Aug 1, 2024
256b442
Format the project issued by command `cargo +nightly fmt`.
0rangeFox Aug 1, 2024
307299d
Re-import `cookie-session` feature. (Maybe was removed accidentally f…
0rangeFox Aug 1, 2024
a8d77af
tmp
robjtede Aug 3, 2024
4f08499
chore: bump deadpool-redis to 0.16
robjtede Aug 6, 2024
8ebe7a7
chore: fixup rest of redis code for pool
robjtede Aug 6, 2024
563ac73
fix: add missing cfg guard
robjtede Aug 6, 2024
33d65a2
Merge branch 'master' into session-deadpool_redis-compatibility
robjtede Aug 6, 2024
8ca14e4
docs: fix pool docs
robjtede Aug 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions actix-session/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Add `redis-session-rustls` crate feature that enables `rustls`-secured Redis sessions.
- Add `redis-pool` crate feature (off-by-default) which enables `RedisSessionStore::{new, builder}_pooled()` constructors.
- Rename `redis-rs-session` crate feature to `redis-session`.
- Rename `redis-rs-tls-session` crate feature to `redis-session-native-tls`.
- Remove `redis-actor-session` crate feature (and, therefore, the `actix-redis` based storage backend).
Expand Down
2 changes: 2 additions & 0 deletions actix-session/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ cookie-session = []
redis-session = ["dep:redis", "dep:rand"]
redis-session-native-tls = ["redis-session", "redis/tokio-native-tls-comp"]
redis-session-rustls = ["redis-session", "redis/tokio-rustls-comp"]
redis-pool = ["dep:deadpool-redis"]

[dependencies]
actix-service = "2"
Expand All @@ -38,6 +39,7 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }

# redis-session
redis = { version = "0.26", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
deadpool-redis = { version = "0.16", optional = true }

[dev-dependencies]
actix-session = { path = ".", features = ["cookie-session", "redis-session"] }
Expand Down
9 changes: 5 additions & 4 deletions actix-session/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ mod utils;

#[cfg(feature = "cookie-session")]
pub use self::cookie::CookieSessionStore;
#[cfg(feature = "redis-session")]
pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
#[cfg(feature = "redis-session")]
pub use self::utils::generate_session_key;
pub use self::{
interface::{LoadError, SaveError, SessionStore, UpdateError},
session_key::SessionKey,
};
#[cfg(feature = "redis-session")]
pub use self::{
redis_rs::{RedisSessionStore, RedisSessionStoreBuilder},
utils::generate_session_key,
};
242 changes: 189 additions & 53 deletions actix-session/src/storage/redis_rs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::Arc;

use actix_web::cookie::time::Duration;
use anyhow::Error;
use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
use redis::{aio::ConnectionManager, AsyncCommands, Client, Cmd, FromRedisValue, Value};

use super::SessionKey;
use crate::storage::{
Expand Down Expand Up @@ -56,14 +56,38 @@ use crate::storage::{
/// # })
/// ```
///
/// # Pooled Redis Connections
///
/// When the `redis-pool` crate feature is enabled, a pre-existing pool from [`deadpool_redis`] can
/// be provided.
///
/// ```no_run
/// use actix_session::storage::RedisSessionStore;
/// use deadpool_redis::{Config, Runtime};
///
/// let redis_cfg = Config::from_url("redis://127.0.0.1:6379");
/// let redis_pool = redis_cfg.create_pool(Some(Runtime::Tokio1)).unwrap();
///
/// let store = RedisSessionStore::new_pooled(redis_pool);
/// ```
///
/// # Implementation notes
/// `RedisSessionStore` leverages [`redis-rs`] as Redis client.
///
/// [`redis-rs`]: https://github.com/mitsuhiko/redis-rs
/// `RedisSessionStore` leverages the [`redis`] crate as the underlying Redis client.
#[derive(Clone)]
pub struct RedisSessionStore {
configuration: CacheConfiguration,
client: ConnectionManager,
client: RedisSessionConn,
}

#[derive(Clone)]
enum RedisSessionConn {
/// Single connection.
Single(ConnectionManager),

/// Connection pool.
#[cfg(feature = "redis-pool")]
Pool(deadpool_redis::Pool),
}

#[derive(Clone)]
Expand All @@ -80,34 +104,77 @@ impl Default for CacheConfiguration {
}

impl RedisSessionStore {
/// A fluent API to configure [`RedisSessionStore`].
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] - a
/// connection string for Redis.
pub fn builder<S: Into<String>>(connection_string: S) -> RedisSessionStoreBuilder {
/// Returns a fluent API builder to configure [`RedisSessionStore`].
///
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
/// - a connection string for Redis.
pub fn builder(connection_string: impl Into<String>) -> RedisSessionStoreBuilder {
RedisSessionStoreBuilder {
configuration: CacheConfiguration::default(),
connection_string: connection_string.into(),
conn_builder: RedisSessionConnBuilder::Single(connection_string.into()),
}
}

/// Create a new instance of [`RedisSessionStore`] using the default configuration.
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] - a
/// connection string for Redis.
pub async fn new<S: Into<String>>(
connection_string: S,
) -> Result<RedisSessionStore, anyhow::Error> {
/// Returns a fluent API builder to configure [`RedisSessionStore`].
///
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
/// - a pool object for Redis.
#[cfg(feature = "redis-pool")]
pub fn builder_pooled(pool: impl Into<deadpool_redis::Pool>) -> RedisSessionStoreBuilder {
RedisSessionStoreBuilder {
configuration: CacheConfiguration::default(),
conn_builder: RedisSessionConnBuilder::Pool(pool.into()),
}
}

/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
///
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
/// - a connection string for Redis.
pub async fn new(connection_string: impl Into<String>) -> Result<RedisSessionStore, Error> {
Self::builder(connection_string).build().await
}

/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
///
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
/// - a pool object for Redis.
#[cfg(feature = "redis-pool")]
pub async fn new_pooled(
pool: impl Into<deadpool_redis::Pool>,
) -> anyhow::Result<RedisSessionStore> {
Self::builder_pooled(pool).build().await
}
}

/// A fluent builder to construct a [`RedisSessionStore`] instance with custom configuration
/// parameters.
///
/// [`RedisSessionStore`]: crate::storage::RedisSessionStore
#[must_use]
pub struct RedisSessionStoreBuilder {
connection_string: String,
configuration: CacheConfiguration,
conn_builder: RedisSessionConnBuilder,
}

enum RedisSessionConnBuilder {
/// Single connection string.
Single(String),

/// Pre-built connection pool.
#[cfg(feature = "redis-pool")]
Pool(deadpool_redis::Pool),
}

impl RedisSessionConnBuilder {
async fn into_client(self) -> anyhow::Result<RedisSessionConn> {
Ok(match self {
RedisSessionConnBuilder::Single(conn_string) => {
RedisSessionConn::Single(ConnectionManager::new(Client::open(conn_string)?).await?)
}

#[cfg(feature = "redis-pool")]
RedisSessionConnBuilder::Pool(pool) => RedisSessionConn::Pool(pool),
})
}
}

impl RedisSessionStoreBuilder {
Expand All @@ -120,9 +187,10 @@ impl RedisSessionStoreBuilder {
self
}

/// Finalise the builder and return a [`RedisSessionStore`] instance.
pub async fn build(self) -> Result<RedisSessionStore, anyhow::Error> {
let client = ConnectionManager::new(redis::Client::open(self.connection_string)?).await?;
/// Finalises builder and returns a [`RedisSessionStore`] instance.
pub async fn build(self) -> anyhow::Result<RedisSessionStore> {
let client = self.conn_builder.into_client().await?;

Ok(RedisSessionStore {
configuration: self.configuration,
client,
Expand Down Expand Up @@ -190,7 +258,7 @@ impl SessionStore for RedisSessionStore {

let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());

let v: redis::Value = self
let v: Value = self
.execute_command(redis::cmd("SET").arg(&[
&cache_key,
&body,
Expand Down Expand Up @@ -223,18 +291,29 @@ impl SessionStore for RedisSessionStore {
}
}

async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> {
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> anyhow::Result<()> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());

self.client
.clone()
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
.await?;
match self.client {
RedisSessionConn::Single(ref conn) => {
conn.clone()
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
.await?;
}

#[cfg(feature = "redis-pool")]
RedisSessionConn::Pool(ref pool) => {
pool.get()
.await?
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
.await?;
}
}

Ok(())
}

async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
async fn delete(&self, session_key: &SessionKey) -> Result<(), Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());

self.execute_command::<()>(redis::cmd("DEL").arg(&[&cache_key]))
Expand All @@ -261,24 +340,55 @@ impl RedisSessionStore {
/// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for
/// a different more meaningful reason).
#[allow(clippy::needless_pass_by_ref_mut)]
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> anyhow::Result<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);
match self.client {
RedisSessionConn::Single(ref conn) => {
let mut conn = conn.clone();

loop {
match cmd.query_async(&mut conn).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.into());
}
}
}
}
}

#[cfg(feature = "redis-pool")]
RedisSessionConn::Pool(ref pool) => {
let mut conn = pool.get().await?;

loop {
match cmd.query_async(&mut conn).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.into());
}
}
}
}
}
Expand All @@ -291,14 +401,27 @@ mod tests {
use std::collections::HashMap;

use actix_web::cookie::time;
#[cfg(not(feature = "redis-session"))]
use deadpool_redis::{Config, Runtime};

use super::*;
use crate::test_helpers::acceptance_test_suite;

async fn redis_store() -> RedisSessionStore {
RedisSessionStore::new("redis://127.0.0.1:6379")
.await
.unwrap()
#[cfg(feature = "redis-session")]
{
RedisSessionStore::new("redis://127.0.0.1:6379")
.await
.unwrap()
}

#[cfg(not(feature = "redis-session"))]
{
let redis_pool = Config::from_url("redis://127.0.0.1:6379")
.create_pool(Some(Runtime::Tokio1))
.unwrap();
RedisSessionStore::new(redis_pool.clone())
}
}

#[actix_web::test]
Expand All @@ -318,12 +441,25 @@ mod tests {
async fn loading_an_invalid_session_state_returns_deserialization_error() {
let store = redis_store().await;
let session_key = generate_session_key();
store
.client
.clone()
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
.await
.unwrap();

match store.client {
RedisSessionConn::Single(ref conn) => conn
.clone()
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
.await
.unwrap(),

#[cfg(feature = "redis-pool")]
RedisSessionConn::Pool(ref pool) => {
pool.get()
.await
.unwrap()
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
.await
.unwrap();
}
}

assert!(matches!(
store.load(&session_key).await.unwrap_err(),
LoadError::Deserialization(_),
Expand Down
Loading