Skip to content

Commit 2a9b54f

Browse files
committed
updated documentation and added blocking doc
1 parent e7495e0 commit 2a9b54f

File tree

3 files changed

+372
-88
lines changed

3 files changed

+372
-88
lines changed

BLOCK.md

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
Blocking commands in Redis modules
2+
===
3+
4+
Redis has a few blocking commands among the built-in set of commands.
5+
One of the most used is `BLPOP` (or the symmetric `BRPOP`) which blocks
6+
waiting for elements arriving in a list.
7+
8+
The interesting fact about blocking commands is that they do not block
9+
the whole server, but just the client calling them. Usually the reason to
10+
block is that we expect some external event to happen: this can be
11+
some change in the Redis data structures like in the `BLPOP` case, a
12+
long computation happening in a thread, to receive some data from the
13+
network, and so forth.
14+
15+
Redis modules have the ability to implement blocking commands as well,
16+
this documentation shows how the API works and describes a few patterns
17+
that can be used in order to model blocking commands.
18+
19+
How blocking and resuming works.
20+
---
21+
22+
_Note: You may want to check the `helloblock.c` example in the Redis source tree
23+
inside the `src/modules` directory, for a simple to understand example
24+
on how the blocking API is applied._
25+
26+
In Redis modules, commands are implemented by callback functions that
27+
are invoked by the Redis core when the specific command is called
28+
by the user. Normally the callback terminates its execution sending
29+
some reply to the client. Using the following function instead, the
30+
function implementing the module command may request that the client
31+
is put into the blocked state:
32+
33+
RedisModuleBlockedClient *RedisModule_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(void*), long long timeout_ms);
34+
35+
The function returns a `RedisModuleBlockedClient` object, which is later
36+
used in order to unblock the client. The arguments have the following
37+
meaning:
38+
39+
* `ctx` is the command execution context as usually in the rest of the API.
40+
* `reply_callback` is the callback, having the same prototype of a normal command function, that is called when the client is unblocked in order to return a reply to the client.
41+
* `timeout_callback` is the callback, having the same prototype of a normal command function that is called when the client reached the `ms` timeout.
42+
* `free_privdata` is the callback that is called in order to free the private data. Private data is a pointer to some data that is passed between the API used to unblock the client, to the callback that will send the reply to the client. We'll see how this mechanism works later in this document.
43+
* `ms` is the timeout in milliseconds. When the timeout is reached, the timeout callback is called and the client is automatically aborted.
44+
45+
Once a client is blocked, it can be unblocked with the following API:
46+
47+
int RedisModule_UnblockClient(RedisModuleBlockedClient *bc, void *privdata);
48+
49+
The function takes as argument the blocked client object returned by
50+
the previous call to `RedisModule_BlockClient()`, and unblock the client.
51+
Immediately before the client gets unblocked, the `reply_callback` function
52+
specified when the client was blocked is called: this function will
53+
have access to the `privdata` pointer used here.
54+
55+
IMPORTANT: The above function is thread safe, and can be called from within
56+
a thread doing some work in order to implement the command that blocked
57+
the client.
58+
59+
The `privdata` data will be freed automatically using the `free_privdata`
60+
callback when the client is unblocked. This is useful **since the reply
61+
callback may never be called** in case the client timeouts or disconnects
62+
from the server, so it's important that it's up to an external function
63+
to have the responsibility to free the data passed if needed.
64+
65+
To better understand how the API works, we can imagine writing a command
66+
that blocks a client for one second, and then send as reply "Hello!".
67+
68+
Note: arity checks and other non important things are not implemented
69+
int his command, in order to take the example simple.
70+
71+
int Example_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
72+
int argc)
73+
{
74+
RedisModuleBlockedClient *bc =
75+
RedisModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);
76+
77+
pthread_t tid;
78+
pthread_create(&tid,NULL,threadmain,bc);
79+
80+
return REDISMODULE_OK;
81+
}
82+
83+
void *threadmain(void *arg) {
84+
RedisModuleBlockedClient *bc = arg;
85+
86+
sleep(1); /* Wait one second and unblock. */
87+
RedisModule_UnblockClient(bc,NULL);
88+
}
89+
90+
The above command blocks the client ASAP, spawining a thread that will
91+
wait a second and will unblock the client. Let's check the reply and
92+
timeout callbacks, which are in our case very similar, since they
93+
just reply the client with a different reply type.
94+
95+
int reply_func(RedisModuleCtx *ctx, RedisModuleString **argv,
96+
int argc)
97+
{
98+
return RedisModule_ReplyWithSimpleString(ctx,"Hello!");
99+
}
100+
101+
int timeout_func(RedisModuleCtx *ctx, RedisModuleString **argv,
102+
int argc)
103+
{
104+
return RedisModule_ReplyWithNull(ctx);
105+
}
106+
107+
The reply callback just sends the "Hello!" string to the client.
108+
The important bit here is that the reply callback is called when the
109+
client is unblocked from the thread.
110+
111+
The timeout command returns `NULL`, as it often happens with actual
112+
Redis blocking commands timing out.
113+
114+
Passing reply data when unblocking
115+
---
116+
117+
The above example is simple to understand but lacks an important
118+
real world aspect of an actual blocking command implementation: often
119+
the reply function will need to know what to reply to the client,
120+
and this information is often provided as the client is unblocked.
121+
122+
We could modify the above example so that the thread generates a
123+
random number after waiting one second. You can think at it as an
124+
actually expansive operation of some kind. Then this random number
125+
can be passed to the reply function so that we return it to the command
126+
caller. In order to make this working, we modify the functions as follow:
127+
128+
void *threadmain(void *arg) {
129+
RedisModuleBlockedClient *bc = arg;
130+
131+
sleep(1); /* Wait one second and unblock. */
132+
133+
long *mynumber = RedisModule_Alloc(sizeof(long));
134+
*mynumber = rand();
135+
RedisModule_UnblockClient(bc,mynumber);
136+
}
137+
138+
As you can see, now the unblocking call is passing some private data,
139+
that is the `mynumber` pointer, to the reply callback. In order to
140+
obtain this private data, the reply callback will use the following
141+
fnuction:
142+
143+
void *RedisModule_GetBlockedClientPrivateData(RedisModuleCtx *ctx);
144+
145+
So our reply callback is modified like that:
146+
147+
int reply_func(RedisModuleCtx *ctx, RedisModuleString **argv,
148+
int argc)
149+
{
150+
long *mynumber = RedisModule_GetBlockedClientPrivateData(ctx);
151+
/* IMPORTANT: don't free mynumber here, but in the
152+
* free privdata callback. */
153+
return RedisModule_ReplyWithLongLong(ctx,mynumber);
154+
}
155+
156+
Note that we also need to pass a `free_privdata` function when blocking
157+
the client with `RedisModule_BlockClient()`, since the allocated
158+
long value must be freed. Our callback will look like the following:
159+
160+
void free_privdata(void *privdata) {
161+
RedisModule_Free(privdata);
162+
}
163+
164+
NOTE: It is important to stress that the private data is best freed in the
165+
`free_privdata` callback becaues the reply function may not be called
166+
if the client disconnects or timeout.
167+
168+
Also note that the private data is also accessible from the timeout
169+
callback, always using the `GetBlockedClientPrivateData()` API.
170+
171+
Aborting the blocking of a client
172+
---
173+
174+
One problem that sometimes arises is that we need to allocate resources
175+
in order to implement the non blocking command. So we block the client,
176+
then, for example, try to create a thread, but the thread creation function
177+
returns an error. What to do in such a condition in order to recover? We
178+
don't want to take the client blocked, nor we want to call `UnblockClient()`
179+
because this will trigger the reply callback to be called.
180+
181+
In this case the best thing to do is to use the following function:
182+
183+
int RedisModule_AbortBlock(RedisModuleBlockedClient *bc);
184+
185+
Practically this is how to use it:
186+
187+
int Example_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
188+
int argc)
189+
{
190+
RedisModuleBlockedClient *bc =
191+
RedisModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);
192+
193+
pthread_t tid;
194+
if (pthread_create(&tid,NULL,threadmain,bc) != 0) {
195+
RedisModule_AbortBlock(bc);
196+
RedisModule_ReplyWithError(ctx,"Sorry can't create a thread");
197+
}
198+
199+
return REDISMODULE_OK;
200+
}
201+
202+
The client will be unblocked but the reply callback will not be called.
203+
204+
Implementing the command, reply and timeout callback using a single function
205+
---
206+
207+
The following functions can be used in order to implement the reply and
208+
callback with the same function that implements the primary command
209+
function:
210+
211+
int RedisModule_IsBlockedReplyRequest(RedisModuleCtx *ctx);
212+
int RedisModule_IsBlockedTimeoutRequest(RedisModuleCtx *ctx);
213+
214+
So I could rewrite the example command without using a separated
215+
reply and timeout callback:
216+
217+
int Example_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
218+
int argc)
219+
{
220+
if (RedisModule_IsBlockedReplyRequest(ctx)) {
221+
long *mynumber = RedisModule_GetBlockedClientPrivateData(ctx);
222+
return RedisModule_ReplyWithLongLong(ctx,mynumber);
223+
} else if (RedisModule_IsBlockedTimeoutRequest) {
224+
return RedisModule_ReplyWithNull(ctx);
225+
}
226+
227+
RedisModuleBlockedClient *bc =
228+
RedisModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);
229+
230+
pthread_t tid;
231+
if (pthread_create(&tid,NULL,threadmain,bc) != 0) {
232+
RedisModule_AbortBlock(bc);
233+
RedisModule_ReplyWithError(ctx,"Sorry can't create a thread");
234+
}
235+
236+
return REDISMODULE_OK;
237+
}
238+
239+
Functionally is the same but there are people that will prefer the less
240+
verbose implementation that concentrates most of the command logic in a
241+
single function.
242+
243+
Working on copies of data inside a thread
244+
---
245+
246+
An interesting pattern in order to work with threads implementing the
247+
slow part of a command, is to work with a copy of the data, so that
248+
while some operation is performed in a key, the user continues to see
249+
the old version. However when the thread terminated its work, the
250+
representations are swapped and the new, processed version, is used.
251+
252+
An example of this approach is the
253+
[Neural Redis module](https://github.com/antirez/neural-redis)
254+
where neural networks are trained in different threads while the
255+
user can still execute and inspect their older versions.
256+
257+
Future work
258+
---
259+
260+
An API is work in progress right now in order to allow Redis modules APIs
261+
to be called in a safe way from threads, so that the threaded command
262+
can access the data space and do incremental operations.
263+
264+
There is no ETA for this feature but it may appear in the course of the
265+
Redis 4.0 release at some point.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Read this before starting, as it's more than an API reference.
4646
3. [TYPES.md](TYPES.md) - Describes the API for creating new data structures inside Redis modules,
4747
copied from the Redis repo.
4848

49+
4. [BLOCK.md](BLOCK.md) - Describes the API for blocking a client while performing asynchronous tasks on a separate thread.
50+
4951

5052
# Quick Start Guide
5153

0 commit comments

Comments
 (0)