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

Add finalizer support to async Redis client #926

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

Conversation

tudor
Copy link

@tudor tudor commented Feb 19, 2021

Also add libevent example that tests it and also demonstrates pubsub.

Fixes #925

@michael-grunder michael-grunder self-assigned this Feb 20, 2021
@tudor
Copy link
Author

tudor commented Mar 2, 2021

@michael-grunder have you had a chance to take a look at this? Thank you!

@michael-grunder
Copy link
Collaborator

Hi @tudor, sorry for the delay.

The logic looks good at first glance. We have to be pretty careful about backward compatibility now that we've released v1.0.0, but I will test the logic in detail over the next couple of days.

@tudor
Copy link
Author

tudor commented Mar 19, 2021

@michael-grunder I don't think there are any backwards compatibility issues. The only thing that changes in a potentially backwards incompatible way is struct redisCallback (its size and the offsets of the pendingSubs and privdata members), but that's not allocated explicitly by users, nor is it used in any inline functions (in fact, it probably shouldn't be in a public header file at all).

@tudor
Copy link
Author

tudor commented Mar 19, 2021

I guess code that explicitly accesses redisAsyncContext::replies would break, but I don't imagine that was ever intended for public consumption.

@kristjanvalur
Copy link
Contributor

I was just about to open an issue asking for this yesterday. I presume this is to support knowing when hiredis forgets the callback.
A couple of questions:

  1. The finalizer seems necessary only for subsriptions, because regular callbacks are called once, and only once, right?
  2. There is code in the subscription logic that replaces subscription callbacks in the dicts, if a previous subscription exists. Are the finalizers called in that case? Should they? Only if the privdata member is different?

@tudor
Copy link
Author

tudor commented Mar 31, 2021

@kristjanvalur

  1. I chose to run the finalizer for regular callbacks as well, because that allows me to generically free state in the C++ code that sits on top of hiredis without having to know whether the callback is a subscription callback or not. If you don't want a finalizer, pass NULL :)
  2. Similarly, I chose to always run the finalizer in the replacement case. Your old callback will never be called again (finalize it), and the new one will.

async.c Outdated
@@ -782,6 +809,7 @@ static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void
if (de != NULL) {
existcb = dictGetEntryVal(de);
cb.pending_subs = existcb->pending_subs + 1;
__redisRunFinalizer(ac,existcb);
Copy link
Contributor

@kristjanvalur kristjanvalur Mar 31, 2021

Choose a reason for hiding this comment

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

Wouldn't it be prudent to check here if the contents of extcb is different from cb? In particular the privdata member, but probably also the callback and finalizer, in case the caller uses dynamically created stubs and NULL for privdata.
A caller might re-register to the same calback, particularly in light of the rather vague docuementation of hiredis

Copy link
Author

@tudor tudor Mar 31, 2021

Choose a reason for hiding this comment

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

Dynamically creating functions is hard, so if you want dynamically-allocated state, you'll have trampoline functions and keep all the state in privdata. So a check for privdata would probably suffice.

But I stand by the belief that, as far as hiredis is concerned, we should keep the interface simple: if you replace a callback, the old one is gone (and finalized), and the new one takes its place.

(Ideally, hiredis wouldn't replace the callback if you use multiple callbacks to subscribe to the same channel-- it would just call all of them! but I understand why they made the decision that they did, and having multiple callbacks called with the same redisReply* would mean that you can't mutate the redisReply any more and you have to treat it as const.)

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree in principle, but async programming is hard, and as a precautionary principle, I would make sure that I didn't finalize the callback that I am installing. Checking that the finalize calllback and the privdata are different would make sense.
Alternatively, we could improve upon the barely existing documentation by stating that calling redisAsyncCall*() to subscribe, more than once, to the same pattern/channel was undefined.

Ideally, there should be separate api functions to subscribe/unsubscribe from channels/patterns because the calling semantics are completely different than for regular calls.

async.c Outdated
@@ -782,6 +809,7 @@ static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void
if (de != NULL) {
existcb = dictGetEntryVal(de);
cb.pending_subs = existcb->pending_subs + 1;
__redisRunFinalizer(ac,existcb);
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree in principle, but async programming is hard, and as a precautionary principle, I would make sure that I didn't finalize the callback that I am installing. Checking that the finalize calllback and the privdata are different would make sense.
Alternatively, we could improve upon the barely existing documentation by stating that calling redisAsyncCall*() to subscribe, more than once, to the same pattern/channel was undefined.

Ideally, there should be separate api functions to subscribe/unsubscribe from channels/patterns because the calling semantics are completely different than for regular calls.

async.c Outdated
@@ -782,6 +809,7 @@ static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void
if (de != NULL) {
existcb = dictGetEntryVal(de);
cb.pending_subs = existcb->pending_subs + 1;
__redisRunFinalizer(ac,existcb);
Copy link
Contributor

Choose a reason for hiding this comment

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

if (existcb->privdata != cb.privdata)
  __redisRunFinalizer(ac, existcb);

@tudor
Copy link
Author

tudor commented Aug 2, 2021

@kristjanvalur sorry for the long delay, I made the change you requested. PTAL?

@kristjanvalur
Copy link
Contributor

LGTM, although honestly, not much is happening in this repo. I've got a bunch of PRs that have been stuck for months, including a bunch of defects/fixes.

@tudor
Copy link
Author

tudor commented Aug 3, 2021

@michael-grunder PTAL? C++ programmers worldwide would thank you :)

Copy link
Collaborator

@michael-grunder michael-grunder left a comment

Choose a reason for hiding this comment

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

If you don't mind making those small changes I will get this merged tomorrow morning (West coast, PDT).

Comment on lines +49 to 51
redisFinalizerCallback *finalizer;
int pending_subs;
void *privdata;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we have to put the new member at the end of the structure, as the current changes breaks ABI compatibility by inserting a member in the middle.

Suggested change
redisFinalizerCallback *finalizer;
int pending_subs;
void *privdata;
int pending_subs;
void *privdata;
redisFinalizerCallback *finalizer;

Screen Shot 2021-08-08 at 7 09 28 PM

@@ -502,11 +521,12 @@ static int redisIsSubscribeReply(redisReply *reply) {

void redisProcessCallbacks(redisAsyncContext *ac) {
redisContext *c = &(ac->c);
redisCallback cb = {NULL, NULL, 0, NULL};
redisCallback cb = {NULL, NULL, NULL, 0, NULL};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we're placing the new callback at the end.

Suggested change
redisCallback cb = {NULL, NULL, NULL, 0, NULL};
redisCallback cb = {NULL, NULL, 0, NULL, NULL};

Copy link
Collaborator

Choose a reason for hiding this comment

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

redisCallback cb = {0}; would be fine too.

TARGET_LINK_LIBRARIES(example-libevent hiredis event)
ADD_EXECUTABLE(example-libevent-pubsub example-libevent-pubsub.c)
TARGET_LINK_LIBRARIES(example-libevent-pubsub hiredis event)
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 also update the Makefile with this new example, although I'm happy to do that after we merge the PR.

}

void getFinalizer(redisAsyncContext *c, void *privdata) {
printf("Get finalizer called\n");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to avoid the warning

Suggested change
printf("Get finalizer called\n");
(void)privdata;
printf("Get finalizer called\n");

}

void subscribeFinalizer(redisAsyncContext *c, void *privdata) {
printf("Subscribe finalizer called\n");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
printf("Subscribe finalizer called\n");
(void)privdata;
printf("Subscribe finalizer called\n");

@zuiderkwast
Copy link
Contributor

@tudor are you still interested in finishing this?

@tudor
Copy link
Author

tudor commented Mar 9, 2023

@zuiderkwast No, I'm not planning to work on this any more. I ended up having to parse the request and response (to know whether it's pub/sub or regular command) anyway for a different reason, in which case I can tell when the callback may be called multiple times.

@kristjanvalur
Copy link
Contributor

That's a shame, it looked like a worthwhile addition. If there is interest, I can pick this up.

@zuiderkwast
Copy link
Contributor

@kristjanvalur I was going to pick it up myself, but you can do it if you want.

We want it for hiredis-cluster, so we can implement pubsub without actually looking at the commands sent by the user. We haven't fully decided if this is enough though.

@kristjanvalur
Copy link
Contributor

@kristjanvalur I was going to pick it up myself, but you can do it if you want.

No, in that case it is best you do it, I haven't touched hiredis for more than two years and am quite rusty :)

@zuiderkwast
Copy link
Contributor

Hehe, I'm not sure I'm less rusty with hiredis internals. :-) I was just going to rebase and apply Michael's comments. I'll invite you for review.

@zuiderkwast
Copy link
Contributor

This PR doesn't correctly handle SUBSCRIBE with multiple channels with finalizer.

I've implemented this now in #1173. It's ready for review. Please have a look. (I'll try to get time to implement the RESET command too, and to allow calling commands while in monitor mode.)

I had to rewrite a lot, because a callback can be used for multiple channels, which can be usubscribed independenly. To keep track of the last reference to a callback (for calling the finalizer at the right time) I've added a reference counter in the callback struct.

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.

async: callback lifecycle management
4 participants