Skip to main content

Redis SET or HASH

· 4 min read
Ang Xuan Ze

Introduction

So I was working on SeaHungerGamesBot, and one major pain point was: when a certain user started the conversation, but did not proceed from there. (probably due to the hassle of copy-pasting the API key?)

Since my current goal is to have as many active users as possible, that was a wasted opportunity. So my objective was to keep track of the users that started pressing some commands, but backed off and disappear afterwards. I call them the potential users.

What's the Plan

The initial plan is:

Remind users to use the Bot if:
- They have chatted with it before
- It happened in the past month
- They still did not provide their key in the past month
  • For such users, store their user_id and the time they call the commands. This can be achieved easily since all anonymous users (aka. those that did not provide their keys) will get routed through a CheckKey function, and if it does not exist in DB, they will be urged to provide their key in the next step in order to proceed with other features.

  • The reason we store the time is because we want to have a "cool down" period before we annoy them again. E.g. A user chatted on 1st Jan, we will not send him the cold message until at least 1st Feb.

  • If we did send him on 1st Feb, we reset the time to 1st Feb and the next message will be sent on 1st March. So on and so forth.

  • And if the user provide their key between 1st Feb and 1st March, we remove them from the pool and they will not receive this anymore.

Cool, Now What?

Technical Implementation

1. Naive

The naive solution is to have a cache key, something like potential_user:<user_id> with the value as time. Since it is pretty straight forward, and we can GET or SET the value easily.

Pros

  • Easy peasy

Cons

  • Kinda messy

2. Slightly more elegant?

Then I thought of Redis Sets, and it is kinda what I prefer it to be. We can have a set called potential_user, and inside the set, we have list of keys <user_id>:<time>.

Pros

  • Very organized, i love it

Cons

  • Need to store all the information in the key, hence delimiter is needed.
  • Have to parse the key to get the relevant information.
  • Since they are not key-value pairs, in order to update the time, we have to SREM the old key and SADD new one afterwards.

I read up on HASH, and it seems pretty decent (overkill maybe?) for this use case. And probably a refactor is neede to make it clean.

The key can be just <user_id>, and contains field such as <user_key> (since we cache the keys to minimize DB calls), <time> etc.

Pros

  • Treat it as an object, and we do not need any extra keys anymore

Cons

  • When we need to find user that has time (not everyone will have this field), we have to probably check one by one.

So..

I went with option 2, and implement a set. Only thing that I don't really like is the update part. Need to remove first -> then re-write the updated time.

//Remove the old key and update with the new time in Set
//As long as users do not give us the key, they will always be in the pool
//We continuously update the time after each cold message to avoid annoyance
if err := processors.RedisClient.SRem(common.POTENTIAL_USER_SET, pair).Err(); err != nil {
log.Error(ctx, "SendPotentialUsers | Error while writing to redis: %v", err.Error())
} else {
log.Info(ctx, "SendPotentialUsers | Successful | Removed %v from potential_user set", pair)
}

toWrite := fmt.Sprint(userID, ":", time.Now().Unix())
if err := processors.RedisClient.SAdd(common.POTENTIAL_USER_SET, toWrite).Err(); err != nil {
log.Error(ctx, "SendPotentialUsers | Error while writing to redis: %v", err.Error())
} else {
log.Info(ctx, "SendPotentialUsers | Successful | Written %v to potential_user set", toWrite)
}

I guess I will leave it as it is for now, until I can think of a better solution.