-
Notifications
You must be signed in to change notification settings - Fork 94
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
ref(relay): migrate redis to async for projectconfig #4284
Conversation
@@ -68,6 +68,7 @@ axum-extra = "0.9.3" | |||
axum-server = "0.7.1" | |||
arc-swap = "1.7.1" | |||
backoff = "0.4.0" | |||
bb8-redis = "0.17.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't done a full review, but what's the new dependency for? Doesn't redis-rs
support async out of the box? https://github.com/redis-rs/redis-rs?tab=readme-ov-file#async-support
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the connection pool, the async equivalent of r2d2
relay-redis/src/real.rs
Outdated
timeout( | ||
Duration::from_secs(options.read_timeout), | ||
cmd.query_async(&mut *conn), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since async redis doesn't seem to have set_read_timeout
/set_write_timeout
anymore, I wrapped the query call in a tokio timeout. From what I understand both write and read timeouts are applied for every command while sending the command to redis and waiting for the reply respectively.
If that's true I think it would be better to use timeout(read_timeout + write_timeout, ...)
to allow for the same total waiting as before
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the same as set_response_timeout
on the multiplexed connection or better, set connection and response timeouts on AsyncConnectionConfig
and with get_multiplexed_async_connection_with_config
this should do the right thing? I think we should just rely on that.
If that's true I think it would be better to use timeout(read_timeout + write_timeout, ...) to allow for the same total waiting as before.
This still has different semantics. Socket read/write is independent from total duration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, set_response_timeout
exists for the multiplexed connection obtained from the pool for redis in single server mode. The cluster connection does not expose this, however, I noticed that the ClusterClientBuilder
allows us to set the response timeout.
connection_timeout
is already handled by the pool builder.
The original thought still remains unclear for me, does it make sense to set the timeout to read + write
? Or what would be the proper timeout in this case without changing the config structure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original thought still remains unclear for me, does it make sense to set the timeout to read + write?
I think just limiting it to read
is fine. Let's just document the behaviour and change the config once all clients are async.
relay-redis/src/real.rs
Outdated
type Connection = ClusterConnection; | ||
type Error = redis::RedisError; | ||
|
||
fn connect<'life0, 'async_trait>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can just use async_trait
here, it's in the dependency tree anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like I misunderstood what the dev guide says about async_trait
. I thought it suggests to not use the crate, but after reading it again it does not mention this at all.
I will update accordingly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, that link is also slightly outdated, Rust traits can now be async fn
.
relay-redis/src/real.rs
Outdated
'life0: 'async_trait, | ||
'life1: 'async_trait, | ||
{ | ||
Box::pin(async move { redis::cmd("PING").query_async(conn).await }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also check the response if it is PONG
.
See also: https://docs.rs/bb8-redis/0.17.0/src/bb8_redis/lib.rs.html#72-75
relay-redis/src/real.rs
Outdated
timeout( | ||
Duration::from_secs(options.read_timeout), | ||
cmd.query_async(&mut *conn), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the same as set_response_timeout
on the multiplexed connection or better, set connection and response timeouts on AsyncConnectionConfig
and with get_multiplexed_async_connection_with_config
this should do the right thing? I think we should just rely on that.
If that's true I think it would be better to use timeout(read_timeout + write_timeout, ...) to allow for the same total waiting as before.
This still has different semantics. Socket read/write is independent from total duration.
relay-redis/src/real.rs
Outdated
/// Forwards the [`Script`] to the pool and loads it asynchronously. | ||
/// Returns the SHA1 hash of it. | ||
pub async fn load_async(&self, script: &Script) -> Result<String, RedisError> { | ||
match self { | ||
Self::Cluster(pool, _options) => { | ||
let mut conn = pool.get().await.map_err(RedisError::AsyncPool)?; | ||
script | ||
.prepare_invoke() | ||
.load_async(&mut *conn) | ||
.await | ||
.map_err(RedisError::Redis) | ||
} | ||
Self::Single(pool, _options) => { | ||
let mut conn = pool.get().await.map_err(RedisError::AsyncPool)?; | ||
script | ||
.prepare_invoke() | ||
.load_async(&mut *conn) | ||
.await | ||
.map_err(RedisError::Redis) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only necessary for the multi-write connection stuff, we don't support it for Async, might as well kill all of it.
This should also make your create_redis_pools
fn sync again making a bunch of stuff easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I'm happy to delete it
relay-redis/src/real.rs
Outdated
|
||
/// Creates a new [`AsyncRedisPool`] backed by a single redis server. | ||
pub async fn single(server: &str, opts: &RedisConfigOptions) -> Result<Self, RedisError> { | ||
let manager = bb8_redis::RedisConnectionManager::new(server).map_err(RedisError::Redis)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want a custom connection config, I don't think the manager allows us to use get_multiplexed_async_connection_with_config
. Might need our own one and can think about contributing that upstream separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will look into this, seems like a good idea to make the connection manager set the proper config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added another custom ConnectionManager that allows us to use the connection with custom config. It feels more consistent between cluster and non cluster pools because the timeout is set once on pool creation instead of setting it on the connection directly. Thank you for the idea!
relay-redis/src/real.rs
Outdated
} | ||
} | ||
|
||
#[axum::async_trait] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add the async-trait
crate directly and not use a re-export, also means the Redis crate does not depend on a web framework.
relay-server/src/service.rs
Outdated
// Based on the assumption that the first config is the | ||
// primary config for MultiWrite configurations | ||
let primary_config = configs.iter().next().ok_or(RedisError::Configuration)?; | ||
Box::pin(create_async_pool(primary_config)).await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't support it we should return an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm inclined to leave this in because projectconfigs used to work before with a multiwrite config and could now produce errors, especially when using the Unified
config. So I think technically that would be a breaking change to previous versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it's a breaking change which is silent. The config may not fail, but it does something different.
On that note, this configuration before already did not make sense and was always experimental with a limited lifetime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's doing something different to an outside observer. It would always perform the commands for secondary configs in the background ignoring the result. Since it doesn't perform write operations for projectconfigs this just means that less work is done.
On that note, this configuration before already did not make sense and was always experimental with a limited lifetime.
I see, in this case I agree it is better to return an error and fix the config where needed
relay-redis/src/real.rs
Outdated
pub async fn single(server: &str, opts: &RedisConfigOptions) -> Result<Self, RedisError> { | ||
let client = Client::open(server).map_err(RedisError::Redis)?; | ||
let config = AsyncConnectionConfig::new() | ||
.set_response_timeout(Duration::from_secs(opts.read_timeout)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason you don't set the connect timeout?
If this is because it's redundant with the timeout specified on the pool, then we can probably use CustomizeConnection
to set the remaining response timeout (we don't need a custom manager either).
Would also be good to have this as a comment somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it's because of redundancy. I wanted to have as much config as possible there because it works the same for single and cluster redis.
I can switch to CustomizeConnection
though and document the decision
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Up to you, from just glancing over code and docs, it seems like CustomizeConnection
is the easier way to do it.
relay-server/src/service.rs
Outdated
match configs { | ||
RedisPoolConfigs::Unified(pool) => { | ||
let async_pool = create_async_pool(&pool).await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let async_pool = create_async_pool(&pool).await?; | |
let project_configs = create_async_pool(&pool).await?; |
relay-server/src/service.rs
Outdated
let pool = create_redis_pool(pool)?; | ||
Ok(RedisPools { | ||
project_configs: pool.clone(), | ||
project_configs: async_pool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
project_configs: async_pool, | |
project_configs, |
relay-redis/src/real.rs
Outdated
/// Creates a new [`AsyncRedisPool`] backed by a single redis server. | ||
/// It will set the `response_timeout` to the provided `read_timeout`. | ||
/// `write_timeout` is not used at all in the async pool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Creates a new [`AsyncRedisPool`] backed by a single redis server. | |
/// It will set the `response_timeout` to the provided `read_timeout`. | |
/// `write_timeout` is not used at all in the async pool | |
/// Creates a new [`AsyncRedisPool`] connecting to a single Redis server. | |
/// | |
/// It will set the `response_timeout` to the provided `read_timeout`, | |
/// `write_timeout` is not used at all in the async pool. |
relay-redis/src/real.rs
Outdated
/// Creates a new cluster based [`AsyncRedisPool`]. | ||
/// It will set the `response_timeout` to the provided `read_timeout`. | ||
/// `write_timeout` is not used at all in the async pool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Creates a new cluster based [`AsyncRedisPool`]. | |
/// It will set the `response_timeout` to the provided `read_timeout`. | |
/// `write_timeout` is not used at all in the async pool | |
/// Creates a new cluster based [`AsyncRedisPool`]. | |
/// | |
/// It will set the `response_timeout` to the provided `read_timeout`, | |
/// `write_timeout` is not used at all in the async pool. |
relay-server/src/service.rs
Outdated
// Based on the assumption that the first config is the | ||
// primary config for MultiWrite configurations | ||
let primary_config = configs.iter().next().ok_or(RedisError::Configuration)?; | ||
Box::pin(create_async_pool(primary_config)).await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it's a breaking change which is silent. The config may not fail, but it does something different.
On that note, this configuration before already did not make sense and was always experimental with a limited lifetime.
Replaces all sync redis calls for projectconfig with the async equivalent.