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

ref(relay): migrate redis to async for projectconfig #4284

Merged
merged 14 commits into from
Nov 27, 2024

Conversation

Litarnus
Copy link
Contributor

@Litarnus Litarnus commented Nov 22, 2024

Replaces all sync redis calls for projectconfig with the async equivalent.

@Litarnus Litarnus marked this pull request as ready for review November 25, 2024 14:30
@Litarnus Litarnus requested a review from a team as a code owner November 25, 2024 14:30
relay-redis/src/real.rs Show resolved Hide resolved
relay-redis/src/real.rs Show resolved Hide resolved
relay-redis/src/real.rs Show resolved Hide resolved
@@ -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"
Copy link
Member

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

Copy link
Contributor Author

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

@Litarnus Litarnus requested a review from loewenheim November 26, 2024 08:19
Comment on lines 460 to 463
timeout(
Duration::from_secs(options.read_timeout),
cmd.query_async(&mut *conn),
)
Copy link
Contributor Author

@Litarnus Litarnus Nov 26, 2024

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

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

type Connection = ClusterConnection;
type Error = redis::RedisError;

fn connect<'life0, 'async_trait>(
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

'life0: 'async_trait,
'life1: 'async_trait,
{
Box::pin(async move { redis::cmd("PING").query_async(conn).await })
Copy link
Member

@Dav1dde Dav1dde Nov 26, 2024

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

Comment on lines 460 to 463
timeout(
Duration::from_secs(options.read_timeout),
cmd.query_async(&mut *conn),
)
Copy link
Member

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.

Comment on lines 481 to 502
/// 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)
}
}
}
Copy link
Member

@Dav1dde Dav1dde Nov 26, 2024

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.

Copy link
Contributor Author

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


/// 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)?;
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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!

}
}

#[axum::async_trait]
Copy link
Member

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.

Comment on lines 488 to 491
// 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
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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

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));
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

match configs {
RedisPoolConfigs::Unified(pool) => {
let async_pool = create_async_pool(&pool).await?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let async_pool = create_async_pool(&pool).await?;
let project_configs = create_async_pool(&pool).await?;

let pool = create_redis_pool(pool)?;
Ok(RedisPools {
project_configs: pool.clone(),
project_configs: async_pool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
project_configs: async_pool,
project_configs,

relay-server/src/service.rs Outdated Show resolved Hide resolved
Comment on lines 515 to 517
/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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.

Comment on lines 496 to 498
/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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.

Comment on lines 488 to 491
// 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
Copy link
Member

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.

@Litarnus Litarnus merged commit 6046f47 into master Nov 27, 2024
23 checks passed
@Litarnus Litarnus deleted the martinl/project-config-async-redis branch November 27, 2024 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants