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

Feat: Add redis backend #813

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open

Conversation

p1gp1g
Copy link

@p1gp1g p1gp1g commented Dec 19, 2024

Currently, autopush requires using Bigtable, there is no particular issue for a big, public server, but some may want to host their own server, or to do some tests locally. So this PR adds another kind of backend using Redis. It should work with redis forks too.

It mostly follow the behavior of bigtable with a few differences:

  • when saving a message with a topic, it simply removes the previous message for this topic. So topic messages and timestamp messages are processed the exact same way. Therefore the test (run_gauntlet) is a bit different for this: it adds 2 messages for the same topic and check only one is fetched.
  • Timestamps used and returned by fetch_timestamp_messages are the date of the record in ms, with bigtables it uses the message's sortkey_timestamp, in seconds.

I've also added a docker-compose file to run everything.

For the moment RedisClientImpl doesn't use connection pooling, but this should be easily implemented, it also provide the Command trait, returned by RedisClientImpl.connection

Fixes #774

Copy link
Member

@jrconlin jrconlin left a comment

Choose a reason for hiding this comment

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

Gotta admit, Redis is not really the DB that I would have picked for this, but sure, provided that the durable storage element is set, it should be able to handle light loads.

FWIW, I had done a version that used Postgres a long time ago. (If you go through the Changelog, you might find a reference to it.) Sadly, I burned that branch a while ago.

Still, thank you for this. There are a few things you'll need to do to fix it up.

The requirements are:

  • Please don't use .unwrap() (or if you do, please explain why.)
  • We actually do need to support topic messages.
  • (Make sure that all unit and integration tests pass)

autoendpoint/src/server.rs Outdated Show resolved Hide resolved
@@ -1,5 +1,5 @@
pub fn main() {
if !cfg!(feature = "bigtable") {
panic!("No database defined! Please compile with `features=bigtable`");
if !cfg!(feature = "bigtable") && !cfg!(feature = "redis") {
Copy link
Member

Choose a reason for hiding this comment

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

bigtable and redis are conflicting feature flags, no?

If you like, we do a conditional check in syncstorage-rs to prevent folk from accidentally specifying both flags.

Copy link
Author

Choose a reason for hiding this comment

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

They should not be conflicting, the server is selected with the dsn url

Copy link
Member

Choose a reason for hiding this comment

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

Right, the actual database selection is non-conflicting, but the compilation is not. I don't know of a situation where someone might want to have both Bigtable and Redis support enabled on the back end at the same time, considering that there's only one database you can specify or use.

Copy link
Author

Choose a reason for hiding this comment

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

I agree there is no use case when both are used, but I can see some examples for compiling both. For instance if a generic container image is published/used, or one do a build that can be used in different environments, (dev/tests with redis, integration/prod with bigtable)

autopush-common/src/db/redis/redis_client/mod.rs Outdated Show resolved Hide resolved
autopush-common/src/db/redis/redis_client/mod.rs Outdated Show resolved Hide resolved
autopush-common/src/db/redis/redis_client/mod.rs Outdated Show resolved Hide resolved
autopush-common/src/db/redis/redis_client/mod.rs Outdated Show resolved Hide resolved
autopush-common/src/db/redis/redis_client/mod.rs Outdated Show resolved Hide resolved
autopush-common/src/notification.rs Show resolved Hide resolved
docker-compose.yml Outdated Show resolved Hide resolved
@p1gp1g
Copy link
Author

p1gp1g commented Dec 21, 2024

Thank you for the review, I've addressed most of the comments.

I went to redis, because this is also what I used for Uppush, which also work as a webpush server. I've seen in the issue comments you had a version with Postgres.

Regarding the topics, this implementation supports topic, but in another way. See the tests: we store 2 messages with the same topic and fetch a single one (if I set topic to None, it fails)

        // We store 2 messages, with a single topic
        let test_notification_0 = crate::db::Notification {
            channel_id: topic_chid,
            version: "version0".to_owned(),
            ttl: 300,
            topic: Some("topic".to_owned()),
            timestamp,
            data: Some(test_data.clone()),
            sortkey_timestamp: Some(sort_key),
            ..Default::default()
        };
        assert!(client
            .save_message(&uaid, test_notification_0.clone())
            .await
            .is_ok());

        let test_notification = crate::db::Notification {
            timestamp: now(),
            version: "version1".to_owned(),
            sortkey_timestamp: Some(sort_key + 10),
            ..test_notification_0
        };

        assert!(client
            .save_message(&uaid, test_notification.clone())
            .await
            .is_ok());

        let mut fetched = client.fetch_timestamp_messages(&uaid, None, 999).await?;
        assert_eq!(fetched.messages.len(), 1);

By the way, using the current chidmessageid allows to do this automatically, the topic isn't necessary: ddc8a46

Regarding the tests, they all pass cargo test --no-default-features --features redis

@jrconlin
Copy link
Member

jrconlin commented Jan 7, 2025

Sigh, just like the other PR, I'm afraid we can't accept this PR with unsigned commits. Please feel free to resubmit this with signed commits.

(This is a policy put in place by our Security Operations team.)

@p1gp1g
Copy link
Author

p1gp1g commented Jan 7, 2025

All the commits are signed

@@ -1,5 +1,5 @@
pub fn main() {
if !cfg!(feature = "bigtable") {
panic!("No database defined! Please compile with `features=bigtable`");
if !cfg!(feature = "bigtable") && !cfg!(feature = "redis") {
Copy link
Member

Choose a reason for hiding this comment

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

Right, the actual database selection is non-conflicting, but the compilation is not. I don't know of a situation where someone might want to have both Bigtable and Redis support enabled on the back end at the same time, considering that there's only one database you can specify or use.


mod error;

use super::RedisDbSettings;
Copy link
Member

Choose a reason for hiding this comment

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

nit: (feel free to ignore) we don't tend to use super a lot because it can make refactoring surprisingly weird. Instead we usually root off of crate like above.

so this would be:

use crate::db::redis::RedisDbSettings;

autopush-common/src/db/redis/redis_client/mod.rs Outdated Show resolved Hide resolved
Ok(channels)
}

/// Delete the channel. Does not delete its associated pending messages.
Copy link
Member

Choose a reason for hiding this comment

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

As I note above you might be the exception here and actually want to delete data when the channel is dropped.

conn: Arc::new(Mutex::new(None)),
settings: db_settings,
metrics,
redis_opts: SetOptions::default().with_expiration(SetExpiry::EX(MAX_ROUTER_TTL)),
Copy link
Member

Choose a reason for hiding this comment

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

So, fun fact: Bigtable and dynamodb can make writes kind of expensive, so we instead rely on TTLs for data to get cleaned up. Redis doesn't really have that limit, so while having an expiration definitely makes sense, feel free to drop values as they're no longer required (e.g. once you get an ACK, when the ChannelID or UserID is dropped, etc.)
Honestly, this is probably a lot more important in the context of Redis, since memory is more a problem than disk space.

///
/// If [`limit`] = 0, we fetch all messages after [`timestamp`].
///
/// This can return expired messages, following bigtables behavior
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, but there's no real need to do that, and in fact, it's probably an anti-pattern we need to fix in bigtable. (There's a potential abuse vector that should be cut off.)

Ideally, messages should be dropped once they've been decisively handled (e.g. if they're ACK'd or if the NACK response specifies that the message is undeliverable. see #795 )

Copy link
Author

@p1gp1g p1gp1g Jan 11, 2025

Choose a reason for hiding this comment

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

Expired messages are removed from redis, so it doesn't use memory for expired messages.

But the message id is not removed from the message lists when it expires.

We can't just remove expired messages from the result, because if the messages vec is empty (so when there are 10 expired messages), check_storage_advance stops.

So there are 3 main solutions:

  • Change check_storage_advance to check if messages.is_empty() && timestamp_has_changed() // to write, but I didn't want to change logic on other components
  • Writing a loop in fetch_timestamp_messages but this is counter productive to do yet another loop
  • Returning dummy expired events, that check_sotrage_advance will remove (this is what it does now)

Ideally, messages should be dropped once they've been decisively handled (e.g. if they're ACK'd or if the NACK response specifies that the message is undeliverable. see #795 )

remove_message do remove the message, so this is already the case :)

autopush-common/src/db/redis/redis_client/mod.rs Outdated Show resolved Hide resolved
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.

Using autopush with local database (like sqlite)
2 participants