The problem/use-case that the feature addresses
Currently, the way modules interact with Redis is via RM_Call(). RM_Call, today, explicitly does not provide persistence between calls, either reusing a preallocated client whose state is reset in between calls or instantiating an ephemeral client that is removed after each call.
This obviously doesn't work with the watch/multi/exec model that needs clients to be persistent between calls, as watches are associated with clients and when the keys being watched are modified, causes the client to be dirtied (thereby failing any future exec on said client)
Description of the feature
The ability for redis modules to provision/deprovision "persistent contexts/clients" on demand.
If a persistent context is passed to an RM_Call() it will note that its a persistent context, and instead of using the preallocated client or creating an ephemeral client, will use the persistent client associated with the context.
Example API
RedisModuleContext * RM_GetPersistentContext()
void RM_FreePersistentContext()
Alternatives you've considered
Intercepting all commands and implementing watch / dirty checking ourselves. This doesn't work for us in the context of a log based system where commands are executed after the fact as a) not so easy to determine which key is always being mutated b) if a key is actually mutated. (ex: while pop is a mutating command, if a key doesn't exist or is empty, its not mutating). In the end, even if possible, this would involve a module reimplementing the wheel to the current state of redis and miss out on future functionality / commands that might introduce new ways to dirty keys.
Comment From: guybe7
@sjpotter can you please give a concrete example?
thoughts: 1. once you're in module context (e.g. a module command, module timer, etc.) the keyspace does not change by other clients, so why do you need WATCH? 2. for MULTI/EXEC: once in module context everything is atomic, so why do need MULTI/EXEC
I think I don't understand the actual problem you're experiencing, and example would help
Comment From: zuiderkwast
It sounds like an interesting feature, but I too would like an example of your use case.
If you want to allow a client to call a module-implemented command inside a transaction, i.e. a sequence of commands like MULTI ; SET x y ; my-module-command ; EXEC, then the module would need to use the client calling the module and pass it on to RM_Call, if we want the RM_Calls to be done inside the transaction started by the client outside the module... (but I guess this is not at all what you need, is it?)
Comment From: sjpotter
The motivation is redis raft, where the module command isn't inside the multi/exec, but the watches/unwatches and multi/exec is a module command themselves.
or to rephrase, anything that would want to implement its own form of replication across redis instances via the module interface (at least where it intercepts/filters all commands and does the replication itself) would probably want a way to associate past watches with future multi/execs across all the redis instances one is replicating to, and the current RM_Call() interface doesn't allow that.
One could totally reimplement the wheel and do all the "state" tracking that redis does, but this seems like a waste when redis already does it and we would just have to manage the lifetimes of the clients/contexts we pass into an RM_Call instead. It would also be more "future proof" as any future redis capaiblities that invalidate watches would happen for free.
is this a bit clearer? tried ot make it clear with my initial title. "Enable Redis Modules to use Redis Transactions" not to be used within a redis transaction.
It could also be used anywhere that a redis module wants to implement a set of command that uses redis's form of opportunistic locks (and have redis enforce it) to implement its own forms of transactions on top of redis (while having redis enforce it via the watch/multi/exec model).
A simplistic idea might be where a single "MYWATCH X" statements gets translated into a number of other WATCH statements behind the scenes and MYTRANSACTION "set of my module commands" get translated into a multi/exec.
Comment From: zuiderkwast
Much clearer, thanks.
Does this mean that you'll need to create one persistent context per incoming client and store a mapping between the incoming client and its persistent context?
In this way, I can see how you can link a WATCH called from within a module command with a MULTI/EXEC called from within another module command, by the same context for the same incoming client.
Such a persistent context which stores a client state internally can enable a bunch of other things which affect the client state, such as SELECT and AUTH, that are currently impossible or meaningless in RM_Call.
Do I understand you correctly this time?
I think it's a very nice approach. One drawback is that we'll have to keep backward compatibility, so there will have to be a difference between a persistent and a non-persistent context (as you noted), since for the non-persistent contexts, the client state is reset after each call.
Comment From: sjpotter
yes. pretty much. what you described. map incoming client id to a persistent context and module would be smart enough to manage lifecycle of that context (much like it has to for existing ones)