]> git.saurik.com Git - redis.git/blame - deps/hiredis/async.c
Sentinel: SENTINEL FAILOVER command implemented.
[redis.git] / deps / hiredis / async.c
CommitLineData
24f753a8 1/*
b66e5add 2 * Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
3 * Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis at gmail dot com>
a1e97d69 4 *
24f753a8
PN
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * * Redistributions of source code must retain the above copyright notice,
11 * this list of conditions and the following disclaimer.
12 * * Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 * * Neither the name of Redis nor the names of its contributors may be used
16 * to endorse or promote products derived from this software without
17 * specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 * POSSIBILITY OF SUCH DAMAGE.
30 */
31
b66e5add 32#include "fmacros.h"
33#include <stdlib.h>
24f753a8 34#include <string.h>
9703b1b3 35#include <strings.h>
24f753a8 36#include <assert.h>
9703b1b3 37#include <ctype.h>
b66e5add 38#include <errno.h>
24f753a8 39#include "async.h"
b66e5add 40#include "net.h"
9703b1b3 41#include "dict.c"
24f753a8 42#include "sds.h"
b66e5add 43
44#define _EL_ADD_READ(ctx) do { \
45 if ((ctx)->ev.addRead) (ctx)->ev.addRead((ctx)->ev.data); \
46 } while(0)
47#define _EL_DEL_READ(ctx) do { \
48 if ((ctx)->ev.delRead) (ctx)->ev.delRead((ctx)->ev.data); \
49 } while(0)
50#define _EL_ADD_WRITE(ctx) do { \
51 if ((ctx)->ev.addWrite) (ctx)->ev.addWrite((ctx)->ev.data); \
52 } while(0)
53#define _EL_DEL_WRITE(ctx) do { \
54 if ((ctx)->ev.delWrite) (ctx)->ev.delWrite((ctx)->ev.data); \
55 } while(0)
56#define _EL_CLEANUP(ctx) do { \
57 if ((ctx)->ev.cleanup) (ctx)->ev.cleanup((ctx)->ev.data); \
58 } while(0);
24f753a8
PN
59
60/* Forward declaration of function in hiredis.c */
61void __redisAppendCommand(redisContext *c, char *cmd, size_t len);
62
9703b1b3
PN
63/* Functions managing dictionary of callbacks for pub/sub. */
64static unsigned int callbackHash(const void *key) {
65 return dictGenHashFunction((unsigned char*)key,sdslen((char*)key));
66}
67
68static void *callbackValDup(void *privdata, const void *src) {
69 ((void) privdata);
70 redisCallback *dup = malloc(sizeof(*dup));
71 memcpy(dup,src,sizeof(*dup));
72 return dup;
73}
74
75static int callbackKeyCompare(void *privdata, const void *key1, const void *key2) {
76 int l1, l2;
77 ((void) privdata);
78
79 l1 = sdslen((sds)key1);
80 l2 = sdslen((sds)key2);
81 if (l1 != l2) return 0;
82 return memcmp(key1,key2,l1) == 0;
83}
84
85static void callbackKeyDestructor(void *privdata, void *key) {
86 ((void) privdata);
87 sdsfree((sds)key);
88}
89
90static void callbackValDestructor(void *privdata, void *val) {
91 ((void) privdata);
92 free(val);
93}
94
95static dictType callbackDict = {
96 callbackHash,
97 NULL,
98 callbackValDup,
99 callbackKeyCompare,
100 callbackKeyDestructor,
101 callbackValDestructor
102};
103
24f753a8
PN
104static redisAsyncContext *redisAsyncInitialize(redisContext *c) {
105 redisAsyncContext *ac = realloc(c,sizeof(redisAsyncContext));
a1e97d69
PN
106 c = &(ac->c);
107
108 /* The regular connect functions will always set the flag REDIS_CONNECTED.
109 * For the async API, we want to wait until the first write event is
110 * received up before setting this flag, so reset it here. */
111 c->flags &= ~REDIS_CONNECTED;
112
113 ac->err = 0;
114 ac->errstr = NULL;
115 ac->data = NULL;
a1e97d69 116
9703b1b3
PN
117 ac->ev.data = NULL;
118 ac->ev.addRead = NULL;
119 ac->ev.delRead = NULL;
120 ac->ev.addWrite = NULL;
121 ac->ev.delWrite = NULL;
122 ac->ev.cleanup = NULL;
a1e97d69
PN
123
124 ac->onConnect = NULL;
125 ac->onDisconnect = NULL;
126
127 ac->replies.head = NULL;
128 ac->replies.tail = NULL;
9703b1b3
PN
129 ac->sub.invalid.head = NULL;
130 ac->sub.invalid.tail = NULL;
131 ac->sub.channels = dictCreate(&callbackDict,NULL);
132 ac->sub.patterns = dictCreate(&callbackDict,NULL);
24f753a8
PN
133 return ac;
134}
135
136/* We want the error field to be accessible directly instead of requiring
137 * an indirection to the redisContext struct. */
138static void __redisAsyncCopyError(redisAsyncContext *ac) {
139 redisContext *c = &(ac->c);
140 ac->err = c->err;
141 ac->errstr = c->errstr;
142}
143
144redisAsyncContext *redisAsyncConnect(const char *ip, int port) {
145 redisContext *c = redisConnectNonBlock(ip,port);
146 redisAsyncContext *ac = redisAsyncInitialize(c);
147 __redisAsyncCopyError(ac);
148 return ac;
149}
150
151redisAsyncContext *redisAsyncConnectUnix(const char *path) {
152 redisContext *c = redisConnectUnixNonBlock(path);
153 redisAsyncContext *ac = redisAsyncInitialize(c);
154 __redisAsyncCopyError(ac);
155 return ac;
156}
157
a1e97d69
PN
158int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) {
159 if (ac->onConnect == NULL) {
160 ac->onConnect = fn;
9703b1b3
PN
161
162 /* The common way to detect an established connection is to wait for
163 * the first write event to be fired. This assumes the related event
164 * library functions are already set. */
b66e5add 165 _EL_ADD_WRITE(ac);
a1e97d69
PN
166 return REDIS_OK;
167 }
168 return REDIS_ERR;
169}
170
24f753a8
PN
171int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn) {
172 if (ac->onDisconnect == NULL) {
173 ac->onDisconnect = fn;
174 return REDIS_OK;
175 }
176 return REDIS_ERR;
177}
178
179/* Helper functions to push/shift callbacks */
180static int __redisPushCallback(redisCallbackList *list, redisCallback *source) {
181 redisCallback *cb;
182
183 /* Copy callback from stack to heap */
9703b1b3 184 cb = malloc(sizeof(*cb));
24f753a8 185 if (source != NULL) {
9703b1b3
PN
186 memcpy(cb,source,sizeof(*cb));
187 cb->next = NULL;
24f753a8
PN
188 }
189
190 /* Store callback in list */
191 if (list->head == NULL)
192 list->head = cb;
193 if (list->tail != NULL)
194 list->tail->next = cb;
195 list->tail = cb;
196 return REDIS_OK;
197}
198
199static int __redisShiftCallback(redisCallbackList *list, redisCallback *target) {
200 redisCallback *cb = list->head;
201 if (cb != NULL) {
202 list->head = cb->next;
203 if (cb == list->tail)
204 list->tail = NULL;
205
206 /* Copy callback from heap to stack */
207 if (target != NULL)
208 memcpy(target,cb,sizeof(*cb));
209 free(cb);
210 return REDIS_OK;
211 }
212 return REDIS_ERR;
213}
214
9703b1b3 215static void __redisRunCallback(redisAsyncContext *ac, redisCallback *cb, redisReply *reply) {
24f753a8 216 redisContext *c = &(ac->c);
9703b1b3
PN
217 if (cb->fn != NULL) {
218 c->flags |= REDIS_IN_CALLBACK;
219 cb->fn(ac,reply,cb->privdata);
220 c->flags &= ~REDIS_IN_CALLBACK;
221 }
222}
223
224/* Helper function to free the context. */
225static void __redisAsyncFree(redisAsyncContext *ac) {
226 redisContext *c = &(ac->c);
227 redisCallback cb;
228 dictIterator *it;
229 dictEntry *de;
230
231 /* Execute pending callbacks with NULL reply. */
232 while (__redisShiftCallback(&ac->replies,&cb) == REDIS_OK)
233 __redisRunCallback(ac,&cb,NULL);
234
235 /* Execute callbacks for invalid commands */
236 while (__redisShiftCallback(&ac->sub.invalid,&cb) == REDIS_OK)
237 __redisRunCallback(ac,&cb,NULL);
238
239 /* Run subscription callbacks callbacks with NULL reply */
240 it = dictGetIterator(ac->sub.channels);
241 while ((de = dictNext(it)) != NULL)
242 __redisRunCallback(ac,dictGetEntryVal(de),NULL);
243 dictReleaseIterator(it);
244 dictRelease(ac->sub.channels);
245
246 it = dictGetIterator(ac->sub.patterns);
247 while ((de = dictNext(it)) != NULL)
248 __redisRunCallback(ac,dictGetEntryVal(de),NULL);
249 dictReleaseIterator(it);
250 dictRelease(ac->sub.patterns);
251
252 /* Signal event lib to clean up */
b66e5add 253 _EL_CLEANUP(ac);
9703b1b3
PN
254
255 /* Execute disconnect callback. When redisAsyncFree() initiated destroying
256 * this context, the status will always be REDIS_OK. */
257 if (ac->onDisconnect && (c->flags & REDIS_CONNECTED)) {
258 if (c->flags & REDIS_FREEING) {
259 ac->onDisconnect(ac,REDIS_OK);
260 } else {
261 ac->onDisconnect(ac,(ac->err == 0) ? REDIS_OK : REDIS_ERR);
262 }
263 }
264
265 /* Cleanup self */
266 redisFree(c);
267}
268
269/* Free the async context. When this function is called from a callback,
270 * control needs to be returned to redisProcessCallbacks() before actual
271 * free'ing. To do so, a flag is set on the context which is picked up by
272 * redisProcessCallbacks(). Otherwise, the context is immediately free'd. */
273void redisAsyncFree(redisAsyncContext *ac) {
274 redisContext *c = &(ac->c);
275 c->flags |= REDIS_FREEING;
276 if (!(c->flags & REDIS_IN_CALLBACK))
277 __redisAsyncFree(ac);
24f753a8
PN
278}
279
280/* Helper function to make the disconnect happen and clean up. */
281static void __redisAsyncDisconnect(redisAsyncContext *ac) {
282 redisContext *c = &(ac->c);
24f753a8
PN
283
284 /* Make sure error is accessible if there is any */
285 __redisAsyncCopyError(ac);
24f753a8 286
9703b1b3
PN
287 if (ac->err == 0) {
288 /* For clean disconnects, there should be no pending callbacks. */
24f753a8
PN
289 assert(__redisShiftCallback(&ac->replies,NULL) == REDIS_ERR);
290 } else {
9703b1b3
PN
291 /* Disconnection is caused by an error, make sure that pending
292 * callbacks cannot call new commands. */
24f753a8 293 c->flags |= REDIS_DISCONNECTING;
24f753a8
PN
294 }
295
9703b1b3
PN
296 /* For non-clean disconnects, __redisAsyncFree() will execute pending
297 * callbacks with a NULL-reply. */
298 __redisAsyncFree(ac);
299}
24f753a8 300
9703b1b3
PN
301/* Tries to do a clean disconnect from Redis, meaning it stops new commands
302 * from being issued, but tries to flush the output buffer and execute
303 * callbacks for all remaining replies. When this function is called from a
304 * callback, there might be more replies and we can safely defer disconnecting
305 * to redisProcessCallbacks(). Otherwise, we can only disconnect immediately
306 * when there are no pending callbacks. */
307void redisAsyncDisconnect(redisAsyncContext *ac) {
308 redisContext *c = &(ac->c);
309 c->flags |= REDIS_DISCONNECTING;
310 if (!(c->flags & REDIS_IN_CALLBACK) && ac->replies.head == NULL)
311 __redisAsyncDisconnect(ac);
312}
24f753a8 313
9703b1b3
PN
314static int __redisGetSubscribeCallback(redisAsyncContext *ac, redisReply *reply, redisCallback *dstcb) {
315 redisContext *c = &(ac->c);
316 dict *callbacks;
317 dictEntry *de;
318 int pvariant;
319 char *stype;
320 sds sname;
321
322 /* Custom reply functions are not supported for pub/sub. This will fail
323 * very hard when they are used... */
324 if (reply->type == REDIS_REPLY_ARRAY) {
325 assert(reply->elements >= 2);
326 assert(reply->element[0]->type == REDIS_REPLY_STRING);
327 stype = reply->element[0]->str;
328 pvariant = (tolower(stype[0]) == 'p') ? 1 : 0;
329
330 if (pvariant)
331 callbacks = ac->sub.patterns;
332 else
333 callbacks = ac->sub.channels;
334
335 /* Locate the right callback */
336 assert(reply->element[1]->type == REDIS_REPLY_STRING);
337 sname = sdsnewlen(reply->element[1]->str,reply->element[1]->len);
338 de = dictFind(callbacks,sname);
339 if (de != NULL) {
340 memcpy(dstcb,dictGetEntryVal(de),sizeof(*dstcb));
341
342 /* If this is an unsubscribe message, remove it. */
343 if (strcasecmp(stype+pvariant,"unsubscribe") == 0) {
344 dictDelete(callbacks,sname);
345
346 /* If this was the last unsubscribe message, revert to
347 * non-subscribe mode. */
348 assert(reply->element[2]->type == REDIS_REPLY_INTEGER);
349 if (reply->element[2]->integer == 0)
350 c->flags &= ~REDIS_SUBSCRIBED;
351 }
352 }
353 sdsfree(sname);
354 } else {
355 /* Shift callback for invalid commands. */
356 __redisShiftCallback(&ac->sub.invalid,dstcb);
357 }
358 return REDIS_OK;
24f753a8
PN
359}
360
361void redisProcessCallbacks(redisAsyncContext *ac) {
362 redisContext *c = &(ac->c);
363 redisCallback cb;
364 void *reply = NULL;
365 int status;
366
367 while((status = redisGetReply(c,&reply)) == REDIS_OK) {
368 if (reply == NULL) {
369 /* When the connection is being disconnected and there are
370 * no more replies, this is the cue to really disconnect. */
371 if (c->flags & REDIS_DISCONNECTING && sdslen(c->obuf) == 0) {
372 __redisAsyncDisconnect(ac);
373 return;
374 }
375
376 /* When the connection is not being disconnected, simply stop
377 * trying to get replies and wait for the next loop tick. */
378 break;
379 }
380
9703b1b3
PN
381 /* Even if the context is subscribed, pending regular callbacks will
382 * get a reply before pub/sub messages arrive. */
383 if (__redisShiftCallback(&ac->replies,&cb) != REDIS_OK) {
b66e5add 384 /* A spontaneous reply in a not-subscribed context can only be the
385 * error reply that is sent when a new connection exceeds the
386 * maximum number of allowed connections on the server side. This
387 * is seen as an error instead of a regular reply because the
388 * server closes the connection after sending it. To prevent the
389 * error from being overwritten by an EOF error the connection is
390 * closed here. See issue #43. */
391 if ( !(c->flags & REDIS_SUBSCRIBED) && ((redisReply*)reply)->type == REDIS_REPLY_ERROR ) {
392 c->err = REDIS_ERR_OTHER;
393 snprintf(c->errstr,sizeof(c->errstr),"%s",((redisReply*)reply)->str);
394 __redisAsyncDisconnect(ac);
395 return;
396 }
397 /* No more regular callbacks and no errors, the context *must* be subscribed. */
9703b1b3
PN
398 assert(c->flags & REDIS_SUBSCRIBED);
399 __redisGetSubscribeCallback(ac,reply,&cb);
400 }
401
24f753a8 402 if (cb.fn != NULL) {
9703b1b3 403 __redisRunCallback(ac,&cb,reply);
b66e5add 404 c->reader->fn->freeObject(reply);
9703b1b3
PN
405
406 /* Proceed with free'ing when redisAsyncFree() was called. */
407 if (c->flags & REDIS_FREEING) {
408 __redisAsyncFree(ac);
409 return;
410 }
24f753a8 411 } else {
9703b1b3
PN
412 /* No callback for this reply. This can either be a NULL callback,
413 * or there were no callbacks to begin with. Either way, don't
414 * abort with an error, but simply ignore it because the client
415 * doesn't know what the server will spit out over the wire. */
b66e5add 416 c->reader->fn->freeObject(reply);
24f753a8
PN
417 }
418 }
419
420 /* Disconnect when there was an error reading the reply */
421 if (status != REDIS_OK)
422 __redisAsyncDisconnect(ac);
423}
424
b66e5add 425/* Internal helper function to detect socket status the first time a read or
426 * write event fires. When connecting was not succesful, the connect callback
427 * is called with a REDIS_ERR status and the context is free'd. */
428static int __redisAsyncHandleConnect(redisAsyncContext *ac) {
429 redisContext *c = &(ac->c);
430
431 if (redisCheckSocketError(c,c->fd) == REDIS_ERR) {
432 /* Try again later when connect(2) is still in progress. */
433 if (errno == EINPROGRESS)
434 return REDIS_OK;
435
436 if (ac->onConnect) ac->onConnect(ac,REDIS_ERR);
437 __redisAsyncDisconnect(ac);
438 return REDIS_ERR;
439 }
440
441 /* Mark context as connected. */
442 c->flags |= REDIS_CONNECTED;
443 if (ac->onConnect) ac->onConnect(ac,REDIS_OK);
444 return REDIS_OK;
445}
446
24f753a8
PN
447/* This function should be called when the socket is readable.
448 * It processes all replies that can be read and executes their callbacks.
449 */
450void redisAsyncHandleRead(redisAsyncContext *ac) {
451 redisContext *c = &(ac->c);
452
b66e5add 453 if (!(c->flags & REDIS_CONNECTED)) {
454 /* Abort connect was not successful. */
455 if (__redisAsyncHandleConnect(ac) != REDIS_OK)
456 return;
457 /* Try again later when the context is still not connected. */
458 if (!(c->flags & REDIS_CONNECTED))
459 return;
460 }
461
24f753a8
PN
462 if (redisBufferRead(c) == REDIS_ERR) {
463 __redisAsyncDisconnect(ac);
464 } else {
465 /* Always re-schedule reads */
b66e5add 466 _EL_ADD_READ(ac);
24f753a8
PN
467 redisProcessCallbacks(ac);
468 }
469}
470
471void redisAsyncHandleWrite(redisAsyncContext *ac) {
472 redisContext *c = &(ac->c);
473 int done = 0;
474
b66e5add 475 if (!(c->flags & REDIS_CONNECTED)) {
476 /* Abort connect was not successful. */
477 if (__redisAsyncHandleConnect(ac) != REDIS_OK)
478 return;
479 /* Try again later when the context is still not connected. */
480 if (!(c->flags & REDIS_CONNECTED))
481 return;
482 }
483
24f753a8
PN
484 if (redisBufferWrite(c,&done) == REDIS_ERR) {
485 __redisAsyncDisconnect(ac);
486 } else {
487 /* Continue writing when not done, stop writing otherwise */
b66e5add 488 if (!done)
489 _EL_ADD_WRITE(ac);
490 else
491 _EL_DEL_WRITE(ac);
24f753a8 492
a1e97d69 493 /* Always schedule reads after writes */
b66e5add 494 _EL_ADD_READ(ac);
24f753a8
PN
495 }
496}
497
9703b1b3
PN
498/* Sets a pointer to the first argument and its length starting at p. Returns
499 * the number of bytes to skip to get to the following argument. */
500static char *nextArgument(char *start, char **str, size_t *len) {
501 char *p = start;
502 if (p[0] != '$') {
503 p = strchr(p,'$');
504 if (p == NULL) return NULL;
505 }
506
507 *len = (int)strtol(p+1,NULL,10);
508 p = strchr(p,'\r');
509 assert(p);
510 *str = p+2;
511 return p+2+(*len)+2;
512}
513
514/* Helper function for the redisAsyncCommand* family of functions. Writes a
515 * formatted command to the output buffer and registers the provided callback
516 * function with the context. */
24f753a8
PN
517static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, char *cmd, size_t len) {
518 redisContext *c = &(ac->c);
519 redisCallback cb;
9703b1b3
PN
520 int pvariant, hasnext;
521 char *cstr, *astr;
522 size_t clen, alen;
523 char *p;
524 sds sname;
24f753a8 525
9703b1b3
PN
526 /* Don't accept new commands when the connection is about to be closed. */
527 if (c->flags & (REDIS_DISCONNECTING | REDIS_FREEING)) return REDIS_ERR;
24f753a8 528
9703b1b3 529 /* Setup callback */
24f753a8
PN
530 cb.fn = fn;
531 cb.privdata = privdata;
9703b1b3
PN
532
533 /* Find out which command will be appended. */
534 p = nextArgument(cmd,&cstr,&clen);
535 assert(p != NULL);
536 hasnext = (p[0] == '$');
537 pvariant = (tolower(cstr[0]) == 'p') ? 1 : 0;
538 cstr += pvariant;
539 clen -= pvariant;
540
541 if (hasnext && strncasecmp(cstr,"subscribe\r\n",11) == 0) {
542 c->flags |= REDIS_SUBSCRIBED;
543
544 /* Add every channel/pattern to the list of subscription callbacks. */
545 while ((p = nextArgument(p,&astr,&alen)) != NULL) {
546 sname = sdsnewlen(astr,alen);
547 if (pvariant)
548 dictReplace(ac->sub.patterns,sname,&cb);
549 else
550 dictReplace(ac->sub.channels,sname,&cb);
551 }
552 } else if (strncasecmp(cstr,"unsubscribe\r\n",13) == 0) {
553 /* It is only useful to call (P)UNSUBSCRIBE when the context is
554 * subscribed to one or more channels or patterns. */
555 if (!(c->flags & REDIS_SUBSCRIBED)) return REDIS_ERR;
556
557 /* (P)UNSUBSCRIBE does not have its own response: every channel or
558 * pattern that is unsubscribed will receive a message. This means we
559 * should not append a callback function for this command. */
560 } else {
561 if (c->flags & REDIS_SUBSCRIBED)
562 /* This will likely result in an error reply, but it needs to be
563 * received and passed to the callback. */
564 __redisPushCallback(&ac->sub.invalid,&cb);
565 else
566 __redisPushCallback(&ac->replies,&cb);
567 }
568
569 __redisAppendCommand(c,cmd,len);
24f753a8
PN
570
571 /* Always schedule a write when the write buffer is non-empty */
b66e5add 572 _EL_ADD_WRITE(ac);
24f753a8
PN
573
574 return REDIS_OK;
575}
576
577int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap) {
578 char *cmd;
579 int len;
580 int status;
581 len = redisvFormatCommand(&cmd,format,ap);
582 status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
583 free(cmd);
584 return status;
585}
586
587int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...) {
588 va_list ap;
589 int status;
590 va_start(ap,format);
591 status = redisvAsyncCommand(ac,fn,privdata,format,ap);
592 va_end(ap);
593 return status;
594}
595
596int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen) {
597 char *cmd;
598 int len;
599 int status;
600 len = redisFormatCommandArgv(&cmd,argc,argv,argvlen);
601 status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
602 free(cmd);
603 return status;
604}