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

valkey-cli doesn't auto exit from subscribed mode on reaching zero subscription #1432

Open
wants to merge 11 commits into
base: unstable
Choose a base branch
from

Conversation

Nikhil-Manglore
Copy link

@Nikhil-Manglore Nikhil-Manglore commented Dec 12, 2024

Resolves issue with valkey-cli not auto exiting from subscribed mode on reaching zero pub/sub subscription (previously filled on Redis) redis/redis#12592

Copy link

codecov bot commented Dec 12, 2024

Codecov Report

Attention: Patch coverage is 88.63636% with 5 lines in your changes missing coverage. Please review.

Project coverage is 70.71%. Comparing base (b3b4bdc) to head (6743126).

Files with missing lines Patch % Lines
src/valkey-cli.c 88.63% 5 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #1432      +/-   ##
============================================
- Coverage     70.83%   70.71%   -0.13%     
============================================
  Files           120      120              
  Lines         64911    64955      +44     
============================================
- Hits          45982    45932      -50     
- Misses        18929    19023      +94     
Files with missing lines Coverage Δ
src/valkey-cli.c 54.25% <88.63%> (-1.24%) ⬇️

... and 15 files with indirect coverage changes

Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

Hello! Welcome to Valkey!

I remember this issue. :)

src/valkey-cli.c Outdated Show resolved Hide resolved
src/valkey-cli.c Outdated Show resolved Hide resolved
Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

Logic looks correct, but I have two comments.

I think it would be good to add a test for valkey-cli that uses SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE in the same session, an exits pubsub mode when all three are unsubscribed.

src/valkey-cli.c Outdated
Comment on lines 2230 to 2250
/* Handle pubsub mode */
if (config.pubsub_mode) {
if (isPubsubPush(reply)) {
if (reply->elements >= 3) {
char *cmd = reply->element[0]->str;
int count = reply->element[2]->integer;

if (strcmp(cmd, "subscribe") == 0 || strcmp(cmd, "unsubscribe") == 0 ||
strcmp(cmd, "psubscribe") == 0 || strcmp(cmd, "punsubscribe") == 0) {
config.pubsub_unsharded_count = count;
} else if (strcmp(cmd, "ssubscribe") == 0 || strcmp(cmd, "sunsubscribe") == 0) {
config.pubsub_sharded_count = count;
}

if (config.pubsub_unsharded_count == 0 && config.pubsub_sharded_count == 0) {
config.pubsub_mode = 0;
cliRefreshPrompt();
}
}
}
}
Copy link
Contributor

@zuiderkwast zuiderkwast Dec 18, 2024

Choose a reason for hiding this comment

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

The logic looks right to me. I think we should move it though.

You have put this logic in cliReadReply, but I think we should put it where cliReadReply is called, or just a few lines below that. That's where most of the client state is handled per command and it's also where the logic is that sets config.pubsub_mode = 1;, so it seems more logical to put this in the same place. The reply can be accessed as config.last_reply.

Somewhere here:

        /* Read response, possibly skipping pubsub/push messages. */
        while (1) {
            if (cliReadReply(output_raw) != REDIS_OK) {
                zfree(argvlen);
                return REDIS_ERR;
            }
            fflush(stdout);
            if (config.pubsub_mode || num_expected_pubsub_push > 0) {
                if (isPubsubPush(config.last_reply)) {
                    if (num_expected_pubsub_push > 0 && !strcasecmp(config.last_reply->element[0]->str, command)) {
                        /* This pushed message confirms the
                         * [p|s][un]subscribe command. */
                        if (is_subscribe && !config.pubsub_mode) {
                            config.pubsub_mode = 1;
                            cliRefreshPrompt();
                        }

tests/unit/pubsub.tcl Outdated Show resolved Hide resolved
Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

Good, thanks. I have some new comments. :)

src/valkey-cli.c Outdated Show resolved Hide resolved
tests/integration/valkey-cli.tcl Outdated Show resolved Hide resolved
Comment on lines 646 to 647
# Verify that we've exited pubsub mode
assert_equal "PONG" [run_command $fd "PING"]
Copy link
Contributor

Choose a reason for hiding this comment

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

This only verifies that the server believes that the client is not in pubsub mode. It would work even without these cli fixes.

What we need to test is that the client know that we have exited pubsub mode. Maybe we can check that the prompt is back to normal 127.0.0.1:6379> instead of 127.0.0.1:6379(subscribed mode)>? I don't know if this test framework can do this. I hope so.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the suggestions, I pushed the updated code. Let me know if you want this check handled another way.

Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

It's getting better! I'm not sure if check for "(subscribed mode)" in the prompt works as intended though.

Maybe we need to test this in some other way. If you can't solve it, then I'll try to think of something.

src/valkey-cli.c Outdated
Comment on lines 2421 to 2426
if (config.pubsub_unsharded_count == 0 && config.pubsub_sharded_count == 0) {
config.pubsub_mode = 0;
cliRefreshPrompt();
} else {
config.pubsub_mode = 1;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

There is already a check that sets pubsub_mode = 1 below. It becomes redundant with this change. We should only do this in one place.

I think it's better to do it here, but we also need to call cliRefreshPrompt and we should do it only if we're not already in pubsub mode. I suggest like this:

Suggested change
if (config.pubsub_unsharded_count == 0 && config.pubsub_sharded_count == 0) {
config.pubsub_mode = 0;
cliRefreshPrompt();
} else {
config.pubsub_mode = 1;
}
if (config.pubsub_unsharded_count == 0 && config.pubsub_sharded_count == 0) {
if (config.pubsub_mode) {
config.pubsub_mode = 0;
cliRefreshPrompt();
}
} else if (!config.pubsub_mode) {
config.pubsub_mode = 1;
cliRefreshPrompt();
}

And this code below can be deleted:

                    if (is_subscribe && !config.pubsub_mode) {
                        config.pubsub_mode = 1;
                        cliRefreshPrompt();
                    }

Copy link
Author

Choose a reason for hiding this comment

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

I was manually testing the code and it seems that when we just use a unsubscribe command to unsubscribe to all channels the count isn't updated properly. So although we are able to unsubscribe to all channels we won't be able to automatically exit subscribe mode. I will continue to look into it.

Copy link
Contributor

Choose a reason for hiding this comment

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

When unsubscribing all channels, the server sends multiple unsubscribe messages and the last one should have count 0. So we should be able to detect this.

I think now that this code only detects the first unsubscribe message. The rest are received in cliWaitForMessagesOrStdin.

This code:

        } else if (FD_ISSET(context->fd, &readfds)) {
            /* Message from the server */
            if (cliReadReply(0) != REDIS_OK) {
                cliPrintContextError();
                exit(1);
            }
            fflush(stdout);

It also calls cliReadReply. Maybe your first solution where the code was inside cliReadReply could actually detect the unsubscribe all channels case. We can put it back there if you want. But can we also do the check to set subscribed node to 1 here? It's good to have the code for pubsub_mode = 1 and pubsub_mode = 0 in the same place.

Copy link
Author

@Nikhil-Manglore Nikhil-Manglore Dec 27, 2024

Choose a reason for hiding this comment

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

Sorry for the delay. I reverted to the previous commit where the code was in cliReadReply and I still seem to get the same issue even though I'm sure it was working last week when I was manually testing. I put a print statement to debug and here is the output

127.0.0.1:6379> subscribe ch1 ch2 ch3
1) "subscribe"
2) "ch1"
3) (integer) 1
1) "subscribe"
2) "ch2"
3) (integer) 2
1) "subscribe"
2) "ch3"
3) (integer) 3
127.0.0.1:6379(subscribed mode)> unsubscribe
1) "unsubscribe"
2) "ch3"
3) (integer) 2
Command: unsubscribe, Count: 2
1) "unsubscribe"
2) "ch2"
3) (integer) 1
1) "unsubscribe"
2) "ch1"
3) (integer) 0

So it seems to only get the reply of the first unsubscribe instead of all of them

I will continue to look into this issue but am currently unsure why this is occurring

Copy link
Collaborator

Choose a reason for hiding this comment

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

@Nikhil-Manglore How is the deep dive going here?

Copy link
Author

Choose a reason for hiding this comment

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

I just fixed the issue by adding code to check for multiple unsubscribe messages in the method cliWaitForMessagesOrStdin(). I will push that code now and begin fixing the test cases.

Comment on lines 666 to 671
# Verify that we've exited pubsub mode
set response [run_command $fd "PING"]

if {[string first "(subscribed mode)" $response] >= 0} {
return -code error "Client is still in pubsub mode"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting! Does this work though? Does the response really contain the prompt?

We should add the opposite check when we enter subscribed mode, to see that the prompt does include "(subscribed mode)" when we are in subscribed mode. In this way, we also know that this kind of check works.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've done some source code reading. The prompt is only printed when STDIN is a TTY. When we run the cli from the tests, the prompt is not printed.

(The code for this is in the command line editor linenoise (deps/linenoise/linenoise.c), function linenoise(), which calls linenoiseNoTTY which doesn't print the prompt.)

So, to test if valkey-cli thinks that it's in pubsub mode or not, we need to do something else. Either we can modify linenoise to add some way to make it behave like an interactive terminal even it it's not running in a terminal. That seems complicated though. I have another idea.

There is some prepared work in valkey-cli for "internal commands", starting with ":".

not connected> :banana
unknown valkey-cli internal command ':banana'

The only such commands right now are :set nohints and :set hints to disable or enable syntax hints.

We could add an internal command to be able to check if we're in pubsub mode. I think for example :get prompt (to print the config.prompt) would be simple enough and useful for testing. WDYT?

Copy link
Author

@Nikhil-Manglore Nikhil-Manglore Jan 3, 2025

Choose a reason for hiding this comment

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

I have pushed the updated code. I was able to implement all functionality while being able to refactor the code as we had initially discussed. I also was able to implement your suggestion into the test cases and they work.

Also I don't think we can delete this code because otherwise the client never enters subscribe mode

if (is_subscribe && !config.pubsub_mode) {
        config.pubsub_mode = 1;
        cliRefreshPrompt();
}
                    ```

Comment on lines +2453 to +2456
} else {
config.pubsub_mode = 1;
cliRefreshPrompt();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we are already in pubsub_mode, do we need to perform this ?

Comment on lines +2262 to +2266
if (strcmp(cmd, "unsubscribe") == 0) {
config.pubsub_unsharded_count = count;
} else if (strcmp(cmd, "sunsubscribe") == 0) {
config.pubsub_sharded_count = count;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

How are we dealing with punsubscribe here?

@@ -608,6 +608,67 @@ if {!$::tls} { ;# fake_redis_node doesn't support TLS
assert_equal "a\n1\nb\n2\nc\n3" [exec {*}$cmdline ZRANGE new_zset 0 -1 WITHSCORES]
}

test "valkey-cli pubsub mode with multiple subscription types" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we split them up into smaller test cases?

@@ -2246,7 +2249,32 @@ static void cliWaitForMessagesOrStdin(void) {
sds out = cliFormatReply(reply, config.output, 0);
fwrite(out, sdslen(out), 1, stdout);
fflush(stdout);

// Handle unsubscribe responses
if (isPubsubPush(reply) && reply->elements >= 3) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This code block seems like duplication of the code below. Could you check if we can place it in one place and handle all scenarios.

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.

3 participants