2010-10-31 20:20:47 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2009-2010, Salvatore Sanfilippo <antirez at gmail dot com>
|
2010-12-16 21:08:39 +00:00
|
|
|
* Copyright (c) 2010, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
|
|
|
*
|
2010-10-31 20:20:47 +00:00
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* Redistribution and use in source and binary forms, with or without
|
|
|
|
* modification, are permitted provided that the following conditions are met:
|
|
|
|
*
|
|
|
|
* * Redistributions of source code must retain the above copyright notice,
|
|
|
|
* this list of conditions and the following disclaimer.
|
|
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
|
|
* documentation and/or other materials provided with the distribution.
|
|
|
|
* * Neither the name of Redis nor the names of its contributors may be used
|
|
|
|
* to endorse or promote products derived from this software without
|
|
|
|
* specific prior written permission.
|
|
|
|
*
|
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <string.h>
|
|
|
|
#include <assert.h>
|
|
|
|
#include "async.h"
|
|
|
|
#include "sds.h"
|
|
|
|
#include "util.h"
|
|
|
|
|
2010-11-01 13:16:01 +00:00
|
|
|
/* Forward declaration of function in hiredis.c */
|
|
|
|
void __redisAppendCommand(redisContext *c, char *cmd, size_t len);
|
|
|
|
|
2010-10-31 20:20:47 +00:00
|
|
|
static redisAsyncContext *redisAsyncInitialize(redisContext *c) {
|
|
|
|
redisAsyncContext *ac = realloc(c,sizeof(redisAsyncContext));
|
2010-12-07 09:22:30 +00:00
|
|
|
c = &(ac->c);
|
|
|
|
|
|
|
|
/* The regular connect functions will always set the flag REDIS_CONNECTED.
|
|
|
|
* For the async API, we want to wait until the first write event is
|
|
|
|
* received up before setting this flag, so reset it here. */
|
|
|
|
c->flags &= ~REDIS_CONNECTED;
|
|
|
|
|
2010-11-15 20:53:22 +00:00
|
|
|
ac->err = 0;
|
|
|
|
ac->errstr = NULL;
|
|
|
|
ac->data = NULL;
|
2010-12-07 09:22:30 +00:00
|
|
|
|
2010-12-29 14:41:03 +00:00
|
|
|
ac->ev.data = NULL;
|
|
|
|
ac->ev.addRead = NULL;
|
|
|
|
ac->ev.delRead = NULL;
|
|
|
|
ac->ev.addWrite = NULL;
|
|
|
|
ac->ev.delWrite = NULL;
|
|
|
|
ac->ev.cleanup = NULL;
|
2010-12-07 09:22:30 +00:00
|
|
|
|
|
|
|
ac->onConnect = NULL;
|
2010-11-15 20:53:22 +00:00
|
|
|
ac->onDisconnect = NULL;
|
2010-12-07 09:22:30 +00:00
|
|
|
|
2010-11-15 20:53:22 +00:00
|
|
|
ac->replies.head = NULL;
|
|
|
|
ac->replies.tail = NULL;
|
2010-10-31 20:20:47 +00:00
|
|
|
return ac;
|
|
|
|
}
|
|
|
|
|
2010-11-01 08:52:17 +00:00
|
|
|
/* We want the error field to be accessible directly instead of requiring
|
|
|
|
* an indirection to the redisContext struct. */
|
|
|
|
static void __redisAsyncCopyError(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
2010-11-02 15:36:38 +00:00
|
|
|
ac->err = c->err;
|
|
|
|
ac->errstr = c->errstr;
|
2010-11-01 08:52:17 +00:00
|
|
|
}
|
|
|
|
|
2010-10-31 20:20:47 +00:00
|
|
|
redisAsyncContext *redisAsyncConnect(const char *ip, int port) {
|
2010-11-01 08:27:43 +00:00
|
|
|
redisContext *c = redisConnectNonBlock(ip,port);
|
|
|
|
redisAsyncContext *ac = redisAsyncInitialize(c);
|
2010-11-01 08:52:17 +00:00
|
|
|
__redisAsyncCopyError(ac);
|
2010-11-01 08:27:43 +00:00
|
|
|
return ac;
|
2010-10-31 20:20:47 +00:00
|
|
|
}
|
|
|
|
|
2010-11-03 10:32:35 +00:00
|
|
|
redisAsyncContext *redisAsyncConnectUnix(const char *path) {
|
|
|
|
redisContext *c = redisConnectUnixNonBlock(path);
|
|
|
|
redisAsyncContext *ac = redisAsyncInitialize(c);
|
|
|
|
__redisAsyncCopyError(ac);
|
|
|
|
return ac;
|
|
|
|
}
|
|
|
|
|
2010-10-31 20:20:47 +00:00
|
|
|
int redisAsyncSetReplyObjectFunctions(redisAsyncContext *ac, redisReplyObjectFunctions *fn) {
|
|
|
|
redisContext *c = &(ac->c);
|
|
|
|
return redisSetReplyObjectFunctions(c,fn);
|
|
|
|
}
|
|
|
|
|
2010-12-07 09:22:30 +00:00
|
|
|
int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) {
|
|
|
|
if (ac->onConnect == NULL) {
|
|
|
|
ac->onConnect = fn;
|
2010-12-28 16:59:26 +00:00
|
|
|
|
|
|
|
/* The common way to detect an established connection is to wait for
|
|
|
|
* the first write event to be fired. This assumes the related event
|
|
|
|
* library functions are already set. */
|
2010-12-29 14:41:03 +00:00
|
|
|
if (ac->ev.addWrite) ac->ev.addWrite(ac->ev.data);
|
2010-12-07 09:22:30 +00:00
|
|
|
return REDIS_OK;
|
|
|
|
}
|
|
|
|
return REDIS_ERR;
|
|
|
|
}
|
|
|
|
|
2010-11-01 08:27:43 +00:00
|
|
|
int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn) {
|
|
|
|
if (ac->onDisconnect == NULL) {
|
|
|
|
ac->onDisconnect = fn;
|
|
|
|
return REDIS_OK;
|
|
|
|
}
|
|
|
|
return REDIS_ERR;
|
|
|
|
}
|
|
|
|
|
2010-11-01 09:01:34 +00:00
|
|
|
/* Helper functions to push/shift callbacks */
|
2010-11-01 09:10:03 +00:00
|
|
|
static int __redisPushCallback(redisCallbackList *list, redisCallback *source) {
|
|
|
|
redisCallback *cb;
|
|
|
|
|
|
|
|
/* Copy callback from stack to heap */
|
2010-12-31 10:22:13 +00:00
|
|
|
cb = malloc(sizeof(*cb));
|
2010-11-01 09:10:03 +00:00
|
|
|
if (!cb) redisOOM();
|
2010-11-01 09:13:05 +00:00
|
|
|
if (source != NULL) {
|
2010-12-31 10:22:13 +00:00
|
|
|
memcpy(cb,source,sizeof(*cb));
|
|
|
|
cb->next = NULL;
|
2010-11-01 09:13:05 +00:00
|
|
|
}
|
2010-11-01 09:10:03 +00:00
|
|
|
|
|
|
|
/* Store callback in list */
|
2010-11-01 09:01:34 +00:00
|
|
|
if (list->head == NULL)
|
|
|
|
list->head = cb;
|
|
|
|
if (list->tail != NULL)
|
|
|
|
list->tail->next = cb;
|
|
|
|
list->tail = cb;
|
2010-11-01 09:10:03 +00:00
|
|
|
return REDIS_OK;
|
2010-11-01 09:01:34 +00:00
|
|
|
}
|
|
|
|
|
2010-11-01 09:10:03 +00:00
|
|
|
static int __redisShiftCallback(redisCallbackList *list, redisCallback *target) {
|
2010-11-01 09:01:34 +00:00
|
|
|
redisCallback *cb = list->head;
|
|
|
|
if (cb != NULL) {
|
|
|
|
list->head = cb->next;
|
|
|
|
if (cb == list->tail)
|
|
|
|
list->tail = NULL;
|
2010-11-01 09:10:03 +00:00
|
|
|
|
|
|
|
/* Copy callback from heap to stack */
|
|
|
|
if (target != NULL)
|
|
|
|
memcpy(target,cb,sizeof(*cb));
|
|
|
|
free(cb);
|
|
|
|
return REDIS_OK;
|
2010-11-01 09:01:34 +00:00
|
|
|
}
|
2010-11-01 09:10:03 +00:00
|
|
|
return REDIS_ERR;
|
2010-11-01 09:01:34 +00:00
|
|
|
}
|
|
|
|
|
2010-12-28 18:19:25 +00:00
|
|
|
/* Helper function to free the context. */
|
|
|
|
static void __redisAsyncFree(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
2010-12-28 19:49:18 +00:00
|
|
|
redisCallback cb;
|
2010-12-28 18:19:25 +00:00
|
|
|
|
2010-12-28 19:49:18 +00:00
|
|
|
/* Execute pending callbacks with NULL reply. */
|
|
|
|
while (__redisShiftCallback(&ac->replies,&cb) == REDIS_OK) {
|
|
|
|
if (cb.fn != NULL) {
|
|
|
|
c->flags |= REDIS_IN_CALLBACK;
|
|
|
|
cb.fn(ac,NULL,cb.privdata);
|
|
|
|
c->flags &= ~REDIS_IN_CALLBACK;
|
|
|
|
}
|
|
|
|
}
|
2010-12-28 18:19:25 +00:00
|
|
|
|
|
|
|
/* Signal event lib to clean up */
|
2010-12-29 14:41:03 +00:00
|
|
|
if (ac->ev.cleanup) ac->ev.cleanup(ac->ev.data);
|
2010-12-28 18:19:25 +00:00
|
|
|
|
2010-12-28 19:49:18 +00:00
|
|
|
/* Execute disconnect callback. When redisAsyncFree() initiated destroying
|
|
|
|
* this context, the status will always be REDIS_OK. */
|
|
|
|
if (ac->onDisconnect && (c->flags & REDIS_CONNECTED)) {
|
|
|
|
if (c->flags & REDIS_FREEING) {
|
|
|
|
ac->onDisconnect(ac,REDIS_OK);
|
|
|
|
} else {
|
|
|
|
ac->onDisconnect(ac,(ac->err == 0) ? REDIS_OK : REDIS_ERR);
|
|
|
|
}
|
|
|
|
}
|
2010-12-28 18:19:25 +00:00
|
|
|
|
|
|
|
/* Cleanup self */
|
|
|
|
redisFree(c);
|
|
|
|
}
|
|
|
|
|
2010-12-28 19:29:26 +00:00
|
|
|
/* Free the async context. When this function is called from a callback,
|
|
|
|
* control needs to be returned to redisProcessCallbacks() before actual
|
|
|
|
* free'ing. To do so, a flag is set on the context which is picked up by
|
|
|
|
* redisProcessCallbacks(). Otherwise, the context is immediately free'd. */
|
2010-12-28 18:19:25 +00:00
|
|
|
void redisAsyncFree(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
2010-12-28 19:49:18 +00:00
|
|
|
c->flags |= REDIS_FREEING;
|
|
|
|
if (!(c->flags & REDIS_IN_CALLBACK))
|
2010-12-28 18:19:25 +00:00
|
|
|
__redisAsyncFree(ac);
|
|
|
|
}
|
|
|
|
|
2010-11-01 08:27:43 +00:00
|
|
|
/* Helper function to make the disconnect happen and clean up. */
|
|
|
|
static void __redisAsyncDisconnect(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
|
|
|
|
2010-11-01 09:01:34 +00:00
|
|
|
/* Make sure error is accessible if there is any */
|
|
|
|
__redisAsyncCopyError(ac);
|
|
|
|
|
2010-12-28 18:19:25 +00:00
|
|
|
if (ac->err == 0) {
|
2010-12-28 19:49:18 +00:00
|
|
|
/* For clean disconnects, there should be no pending callbacks. */
|
2010-11-01 09:10:03 +00:00
|
|
|
assert(__redisShiftCallback(&ac->replies,NULL) == REDIS_ERR);
|
2010-11-01 09:01:34 +00:00
|
|
|
} else {
|
2010-12-28 19:49:18 +00:00
|
|
|
/* Disconnection is caused by an error, make sure that pending
|
|
|
|
* callbacks cannot call new commands. */
|
2010-11-01 09:01:34 +00:00
|
|
|
c->flags |= REDIS_DISCONNECTING;
|
|
|
|
}
|
|
|
|
|
2010-12-28 19:49:18 +00:00
|
|
|
/* For non-clean disconnects, __redisAsyncFree() will execute pending
|
|
|
|
* callbacks with a NULL-reply. */
|
2010-12-28 18:19:25 +00:00
|
|
|
__redisAsyncFree(ac);
|
2010-11-01 08:27:43 +00:00
|
|
|
}
|
|
|
|
|
2010-12-28 19:29:26 +00:00
|
|
|
/* Tries to do a clean disconnect from Redis, meaning it stops new commands
|
|
|
|
* from being issued, but tries to flush the output buffer and execute
|
|
|
|
* callbacks for all remaining replies. When this function is called from a
|
|
|
|
* callback, there might be more replies and we can safely defer disconnecting
|
|
|
|
* to redisProcessCallbacks(). Otherwise, we can only disconnect immediately
|
|
|
|
* when there are no pending callbacks. */
|
|
|
|
void redisAsyncDisconnect(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
2010-12-28 19:49:18 +00:00
|
|
|
c->flags |= REDIS_DISCONNECTING;
|
|
|
|
if (!(c->flags & REDIS_IN_CALLBACK) && ac->replies.head == NULL)
|
2010-12-28 19:29:26 +00:00
|
|
|
__redisAsyncDisconnect(ac);
|
|
|
|
}
|
|
|
|
|
2010-11-01 08:27:43 +00:00
|
|
|
void redisProcessCallbacks(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
2010-11-01 09:10:03 +00:00
|
|
|
redisCallback cb;
|
2010-11-01 08:27:43 +00:00
|
|
|
void *reply = NULL;
|
|
|
|
int status;
|
|
|
|
|
|
|
|
while((status = redisGetReply(c,&reply)) == REDIS_OK) {
|
|
|
|
if (reply == NULL) {
|
|
|
|
/* When the connection is being disconnected and there are
|
|
|
|
* no more replies, this is the cue to really disconnect. */
|
|
|
|
if (c->flags & REDIS_DISCONNECTING && sdslen(c->obuf) == 0) {
|
|
|
|
__redisAsyncDisconnect(ac);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* When the connection is not being disconnected, simply stop
|
|
|
|
* trying to get replies and wait for the next loop tick. */
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2010-12-31 10:27:32 +00:00
|
|
|
/* Even if the context is subscribed, pending regular callbacks will
|
|
|
|
* get a reply before pub/sub messages arrive. */
|
|
|
|
if (__redisShiftCallback(&ac->replies,&cb) != REDIS_OK) {
|
|
|
|
/* No more regular callbacks, the context *must* be subscribed. */
|
|
|
|
assert(c->flags & REDIS_SUBSCRIBED);
|
|
|
|
|
|
|
|
/* TODO: find the right callback for pub/sub message. */
|
|
|
|
}
|
|
|
|
|
2010-11-01 09:10:03 +00:00
|
|
|
if (cb.fn != NULL) {
|
2010-12-28 19:29:26 +00:00
|
|
|
c->flags |= REDIS_IN_CALLBACK;
|
2010-11-01 09:10:03 +00:00
|
|
|
cb.fn(ac,reply,cb.privdata);
|
2010-12-28 19:29:26 +00:00
|
|
|
c->flags &= ~REDIS_IN_CALLBACK;
|
2010-12-31 10:27:32 +00:00
|
|
|
c->fn->freeObject(reply);
|
2010-12-28 18:35:26 +00:00
|
|
|
|
|
|
|
/* Proceed with free'ing when redisAsyncFree() was called. */
|
|
|
|
if (c->flags & REDIS_FREEING) {
|
|
|
|
__redisAsyncFree(ac);
|
|
|
|
return;
|
|
|
|
}
|
2010-11-01 08:27:43 +00:00
|
|
|
} else {
|
2010-12-31 10:27:32 +00:00
|
|
|
/* No callback for this reply. This can either be a NULL callback,
|
|
|
|
* or there were no callbacks to begin with. Either way, don't
|
|
|
|
* abort with an error, but simply ignore it because the client
|
|
|
|
* doesn't know what the server will spit out over the wire. */
|
2010-11-01 08:27:43 +00:00
|
|
|
c->fn->freeObject(reply);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Disconnect when there was an error reading the reply */
|
|
|
|
if (status != REDIS_OK)
|
|
|
|
__redisAsyncDisconnect(ac);
|
|
|
|
}
|
|
|
|
|
2010-10-31 20:20:47 +00:00
|
|
|
/* This function should be called when the socket is readable.
|
|
|
|
* It processes all replies that can be read and executes their callbacks.
|
|
|
|
*/
|
|
|
|
void redisAsyncHandleRead(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
|
|
|
|
|
|
|
if (redisBufferRead(c) == REDIS_ERR) {
|
2010-11-01 08:27:43 +00:00
|
|
|
__redisAsyncDisconnect(ac);
|
2010-10-31 20:20:47 +00:00
|
|
|
} else {
|
|
|
|
/* Always re-schedule reads */
|
2010-12-29 14:41:03 +00:00
|
|
|
if (ac->ev.addRead) ac->ev.addRead(ac->ev.data);
|
2010-11-01 08:53:34 +00:00
|
|
|
redisProcessCallbacks(ac);
|
2010-10-31 20:20:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void redisAsyncHandleWrite(redisAsyncContext *ac) {
|
|
|
|
redisContext *c = &(ac->c);
|
|
|
|
int done = 0;
|
|
|
|
|
|
|
|
if (redisBufferWrite(c,&done) == REDIS_ERR) {
|
2010-11-01 08:27:43 +00:00
|
|
|
__redisAsyncDisconnect(ac);
|
2010-10-31 20:20:47 +00:00
|
|
|
} else {
|
|
|
|
/* Continue writing when not done, stop writing otherwise */
|
|
|
|
if (!done) {
|
2010-12-29 14:41:03 +00:00
|
|
|
if (ac->ev.addWrite) ac->ev.addWrite(ac->ev.data);
|
2010-10-31 20:20:47 +00:00
|
|
|
} else {
|
2010-12-29 14:41:03 +00:00
|
|
|
if (ac->ev.delWrite) ac->ev.delWrite(ac->ev.data);
|
2010-10-31 20:20:47 +00:00
|
|
|
}
|
|
|
|
|
2010-12-07 09:22:30 +00:00
|
|
|
/* Always schedule reads after writes */
|
2010-12-29 14:41:03 +00:00
|
|
|
if (ac->ev.addRead) ac->ev.addRead(ac->ev.data);
|
2010-12-07 09:22:30 +00:00
|
|
|
|
|
|
|
/* Fire onConnect when this is the first write event. */
|
|
|
|
if (!(c->flags & REDIS_CONNECTED)) {
|
|
|
|
c->flags |= REDIS_CONNECTED;
|
|
|
|
if (ac->onConnect) ac->onConnect(ac);
|
|
|
|
}
|
2010-10-31 20:20:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Helper function for the redisAsyncCommand* family of functions.
|
|
|
|
*
|
|
|
|
* Write a formatted command to the output buffer and register the provided
|
|
|
|
* callback function with the context.
|
|
|
|
*/
|
|
|
|
static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, char *cmd, size_t len) {
|
|
|
|
redisContext *c = &(ac->c);
|
2010-11-01 09:10:03 +00:00
|
|
|
redisCallback cb;
|
2010-11-01 08:27:43 +00:00
|
|
|
|
2010-12-28 18:19:25 +00:00
|
|
|
/* Don't accept new commands when the connection is about to be closed. */
|
|
|
|
if (c->flags & (REDIS_DISCONNECTING | REDIS_FREEING)) return REDIS_ERR;
|
2010-11-01 13:16:01 +00:00
|
|
|
__redisAppendCommand(c,cmd,len);
|
2010-10-31 20:20:47 +00:00
|
|
|
|
|
|
|
/* Store callback */
|
2010-11-01 09:10:03 +00:00
|
|
|
cb.fn = fn;
|
|
|
|
cb.privdata = privdata;
|
|
|
|
__redisPushCallback(&ac->replies,&cb);
|
2010-10-31 20:20:47 +00:00
|
|
|
|
|
|
|
/* Always schedule a write when the write buffer is non-empty */
|
2010-12-29 14:41:03 +00:00
|
|
|
if (ac->ev.addWrite) ac->ev.addWrite(ac->ev.data);
|
2010-10-31 20:20:47 +00:00
|
|
|
|
|
|
|
return REDIS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap) {
|
|
|
|
char *cmd;
|
|
|
|
int len;
|
|
|
|
int status;
|
|
|
|
len = redisvFormatCommand(&cmd,format,ap);
|
|
|
|
status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
|
|
|
|
free(cmd);
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
|
|
|
int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...) {
|
|
|
|
va_list ap;
|
|
|
|
int status;
|
|
|
|
va_start(ap,format);
|
|
|
|
status = redisvAsyncCommand(ac,fn,privdata,format,ap);
|
|
|
|
va_end(ap);
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
|
|
|
int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen) {
|
|
|
|
char *cmd;
|
|
|
|
int len;
|
|
|
|
int status;
|
|
|
|
len = redisFormatCommandArgv(&cmd,argc,argv,argvlen);
|
|
|
|
status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
|
|
|
|
free(cmd);
|
|
|
|
return status;
|
|
|
|
}
|