Skip to content

Conversation

@sjpotter
Copy link
Contributor

@sjpotter sjpotter commented May 11, 2023

Leverages redis/redis#12159 and redis/redis#12219 to enable End to End Multi/Exec/Watch support.

redis/redis#12159 - Enables RedisRaft to have fully implement session support where watches an be tracked and validated for dirtyness. On snapshot we save all current session dirtyness and on restore include that state in the dirtyness check at exec apply time.

redis/redis#12219 - Enables RedisRaft to have finer grain control over command interception. This enables us to handle "PING" and "SHUTDOWN" within a multi correctly. Normally, we don't want to intercept them, but if our client is in a multi state, we do (to prevent shutdown from working, as in normal redis, and to have PING's "PONG" response be part of the MULTI array response).

This PR enables most of the MULTI/EXEC/WATCH tests

@sjpotter sjpotter requested review from fadidahanna and tezc May 11, 2023 13:52
@codecov-commenter
Copy link

codecov-commenter commented May 11, 2023

Codecov Report

Merging #619 (86233a6) into master (f56edd1) will increase coverage by 0.26%.
The diff coverage is 89.74%.

❗ Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please install the Github App Integration for your organization. Read more.

@@            Coverage Diff             @@
##           master     #619      +/-   ##
==========================================
+ Coverage   59.65%   59.91%   +0.26%     
==========================================
  Files          44       44              
  Lines       15533    15584      +51     
  Branches     1830     1844      +14     
==========================================
+ Hits         9266     9337      +71     
+ Misses       6267     6247      -20     
Impacted Files Coverage Δ
src/commands.c 91.86% <ø> (ø)
src/redisraft.h 100.00% <ø> (ø)
src/multi.c 56.18% <58.82%> (+7.08%) ⬆️
src/snapshot.c 85.31% <83.33%> (-0.03%) ⬇️
deps/common/redismodule.h 100.00% <100.00%> (ø)
src/clientstate.c 100.00% <100.00%> (ø)
src/raft.c 89.43% <100.00%> (+0.27%) ⬆️
src/redisraft.c 73.90% <100.00%> (+0.11%) ⬆️

... and 3 files with indirect coverage changes

if (req) {
RedisModule_ReplyWithNull(req->ctx);
}
endClientSession(rr, client_session->client_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should unwatch all keys for this client.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see comment below, unsure its necessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

Did you take a look at Redis exec implementation? please make sure the we remove the client from the watched clients lists somewhere in our use case.

src/raft.c Outdated
if (i == 0 && cmdlen == 5 && !strncasecmp(cmd, "MULTI", 5)) {
if (client_session) {
uint64_t flags = RedisModule_GetClientFlags(client_session->client);
if (flags) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should check for a specific flag

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't remember if we discussed this but why don't we pass multi-exec to rm_call()? So, it can return proper reply depending on client's watched keys? Do we have to manually check dirty result?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Its possible, currently we don't add "exec" to the log entry, so that would be a change there, was trying to minimize changes needed for now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I was thinking that if we pass watch, multi and exec to the RM_Call(), then it will minimize the API and be quite similar to real clients. Then, we don't have to deal with checking the flag before RM_Call() and no need to generate reply manually.

Also, I assume RM_Call() will reset client after EXEC, so we don't have to destroy moduleclient. Currently, existence of moduleclient is tied to watch/multi/exec, I think it does not have to be like that. We may even maintain a moduleclient per client even they are not watching any keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I don't think we can rely on the client being reset after exec. client state persists across exec (just not necc for our session usage).

It might be reasonable to maintain a module client per connected client, but there might be side effects that we dont understand yet.

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 strongly don't want to make this change now, as I think its underestimating the effort and the logical change that we have to the code.

Currently, every time (minus blocking, but that returns a different CallReply type) we do an RM_Call() whether it be within our simulated multi or single commands, it returns a result that we care about.

If we use the persistent client to enqueue, we would get multiple QUEUED responses that we dont care about, and only get a proper response at the very end when we do EXEC().

While this might be an improvement (not convinced, but willing to accept as true), its a fundamental change to a crucial part of the code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, I don't think we can rely on the client being reset after exec. client state persists across exec (just not necc for our session usage).

Not sure I understand but I meant dirty flag will be cleaned up automatically, so moduleclient might be reusable. Right now, IIUC you have to free moduleclient once dirty flag is set.

Btw what does necc mean? :) is it shorthand for necessary?

Ok, indeed queued reply is a bit annoying. Overall, still I see moduleclient as a replacement for real client, so it can carry some state as if like a real connection. Maybe we consider this approach in future if we need something else other than watch. If I'm not mistaken, most changes will be in redisraft.

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, sorry, necc was short for necessary (which I see doesn't make sense as an extra C, should have used nec.

I dont disagree that its possible for the future (though in practice it will just be slower, but perhaps more accurate), as we aren't going to be the queued entries individually on the log, so we will still queue on leader.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

ClientSession *client_session = NULL;
RedisModule_DictDelC(rr->client_session_dict, &id, sizeof(id), &client_session);
if (client_session) {
freeClientSession(rr, client_session);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we should call unwatch all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It shouldn't do anything, it should be no different than calling watch in a normal rm_call(). we are releasing/resetting the client back to the temp pool.

src/raft.c Outdated
} else {
RedisModule_SetContextUser(ctx, NULL);
}
RedisModule_SetContextClient(ctx, NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need to do it twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what twice? in one case we set the ctx's user to null, in the other case we set the ctx's client to null.

Copy link
Contributor

Choose a reason for hiding this comment

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

in client_session == True case, we already set the ctx's client => if we set the ctx's client to null anyway, no need to set it first at client_session case

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, we could check it / wrap it in more if/else conditions, but setting it to null always afterwards, even if set up front, seems cleaner.

RedisModuleCallReply *reply = NULL;
RedisModuleUser *user = NULL;
RedisModuleCtx *ctx = req ? req->ctx : rr->ctx;
bool is_multi_session = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

The name is a little bit confusing - it's not a multi session, but a multi session with a valid persist client

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 guess in my head this is a multi using session support, not a multi without session support. i.e. is_multi would be if just a multi is multi_session is a multi when we are handling a session.

unsigned long long id = RedisModule_LoadUnsigned(rdb);
client_session->client_id = id;
client_session->local = false;
client_session->client = RedisModule_CreateModuleClient(rr->ctx);
Copy link
Contributor

Choose a reason for hiding this comment

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

What about rdbSave? don't we want to save the client state too? (dirty state for example)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done now

Copy link
Collaborator

Choose a reason for hiding this comment

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

If missing, maybe we can add a test for snapshot & snapshot delivery. Just to hit those lines where we save/restore sessions

src/raft.c Outdated
} else {
RedisModule_SetContextUser(ctx, NULL);
}
RedisModule_SetContextClient(ctx, NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

in client_session == True case, we already set the ctx's client => if we set the ctx's client to null anyway, no need to set it first at client_session case

if (req) {
RedisModule_ReplyWithNull(req->ctx);
}
endClientSession(rr, client_session->client_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you take a look at Redis exec implementation? please make sure the we remove the client from the watched clients lists somewhere in our use case.


static bool isClientSessionDirty(ClientSession *client_session)
{
uint64_t flags = RedisModule_GetClientFlags(client_session->client);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should check for a specific dirty flag

src/snapshot.c Outdated
ClientSession *client_session;
while (RedisModule_DictNextC(iter, NULL, (void **) &client_session) != NULL) {
RedisModule_SaveUnsigned(rdb, client_session->client_id);
if (RedisModule_GetClientFlags(client_session->client)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

see my comment for isClientSessionDirty()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@sjpotter sjpotter changed the title test watch support with redis PR E2E MULTI/EXEC/WATCH Support May 28, 2023
src/raft.c Outdated
*/
char *resp_call_fmt;
if (cmds->cmd_flags & CMD_SPEC_MULTI) {
if (cmds->cmd_flags & CMD_SPEC_MULTI || client_session) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't know what Redis currently does but can't we send WATCH and then BLPOP (without multi)?
Btw, a test case might be good for this depending on the answer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

needs to fix.

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