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

E2E MULTI/EXEC/WATCH Support #619

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

Conversation

sjpotter
Copy link
Collaborator

@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 tezc and fadidahanna 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
Collaborator

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
Collaborator 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
Collaborator

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
@@ -412,6 +428,17 @@ RedisModuleCallReply *RaftExecuteCommandArray(RedisRaftCtx *rr,
* (although no harm is done).
*/
if (i == 0 && cmdlen == 5 && !strncasecmp(cmd, "MULTI", 5)) {
if (client_session) {
uint64_t flags = RedisModule_GetClientFlags(client_session->client);
if (flags) {
Copy link
Collaborator

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
Collaborator 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
Collaborator 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
Collaborator 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
Collaborator 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
Collaborator 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
Collaborator

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
Collaborator 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
Collaborator

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
Collaborator 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
Collaborator

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
Collaborator 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.

@@ -376,6 +392,7 @@ RedisModuleCallReply *RaftExecuteCommandArray(RedisRaftCtx *rr,
RedisModuleCallReply *reply = NULL;
RedisModuleUser *user = NULL;
RedisModuleCtx *ctx = req ? req->ctx : rr->ctx;
bool is_multi_session = false;
Copy link
Collaborator

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
Collaborator 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.

@@ -574,6 +574,7 @@ static void clientSessionRDBLoad(RedisModuleIO *rdb)
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
Collaborator

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
Collaborator 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
Collaborator

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
Collaborator

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.

@@ -359,6 +376,16 @@ void handleUnblock(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *priva
freeBlockedCommand(bc);
}

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

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
@@ -707,6 +709,11 @@ static void clientSessionRDBSave(RedisModuleIO *rdb)
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
Collaborator

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
Collaborator 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
@@ -434,17 +471,27 @@ RedisModuleCallReply *RaftExecuteCommandArray(RedisRaftCtx *rr,
* When we have an ACL, we will have a user set on the context, so need "C"
*/
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
Collaborator 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.

None yet

4 participants