diff --git a/README.md b/README.md index 776ad92..f53d16f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ A simple, light-weight C/C++ Redis client. - [Managing Redis server connections](#managing-redis-server-connections) - [Simple Redis queries](#simple-redis-queries) - [Publish/subscribe (PUB/SUB) support](#publish-subscribe-support) + - [Advanced queries](#advanced-queries) - [Atomic execution blocks and LUA scripts](#atomic-transaction-blocks-and-lua-scripts) - [Error handling](#error-handling) - [Debug support](#debug-support) @@ -637,22 +638,104 @@ appropriate (provided no other subscription needs it) via `redisxRemoveSubscribe ----------------------------------------------------------------------------- + +## Advanced queries + +Sometimes you might want to micro manage how requests are sent and responses to them are receieved. __RedisX__ +provides a set of asynchronous client functions that do that. These functions should be called with the specific +client's mutex locked, to ensure that other threads do not interfere with your sequence of requests and responses. +E.g.: + +```c + RedisClient *cl = redisxGetClient(...); + + // Obtain an exclusive lock on the client + int status = redisxLockEnabled(cl); + if (status != X_SUCCESS) { + // Abort: the client is probably not connected + return; + } + + // Now send commands, and receive responses as you like using the redisx...Async() calls + ... + + // When done, release the lock + redisxUnlockClient(cl); +``` + +While you have the exclusive lock you may send any number of requests, e.g. via `redisxSendRequestAsync()` and/or +`redixSendArrayRequestAsync()`. Then collect replies either with `redisxReadReplyAsync()` or else +`redisxIgnoreReplyAsync()`. For example, the basic anatomy of sending a single request and then receiving a response, +while we have exclusive access to the client, might look something like this: + +```c + ... + // Send a command to Redis + int status = redisxSendRequestAsync(cl, ...); + + if(status == X_SUCCESS) { + // Read the response + RESP *reply = redisxReadReplyAsync(cl); + + // check and process the response + if(redisxCheckRESP(reply, ...) != X_SUCCESS) { + // Ooops, not the reply what we expected... + ... + } + else { + // Process the response + ... + } + + // Destroy the reply + redisxDestroyRESP(reply); + } + ... +``` + +In some cases you may be OK with just firing off Redis commands, without necessarily caring about responses. Rather +than ignoring the replies with `redisxIgnoreReplyAsync()` you might call `redisxSkiReplyAsync()` instead __before__ +`redisxSendRequestAsync()` to instruct Redis to not even bother about sending a response to your request (it saves +time and network bandwidth!): + +```c + // We don't want to receive a response to our next command... + int status = redisxSkipReplyAsync(cl); + + if (status == X_SUCCESS) { + // Now send the request... + status = redisxSendRequest(cl, ...); + } + + if (status != X_SUCCESS) { + // Ooops, the request did not go through... + ... + } +``` + +Of course you can build up arbitrarily complex set of queries and deal with a set of responses in different ways. Do +what works best for your application. + +----------------------------------------------------------------------------- + ## Atomic execution blocks and LUA scripts - [Execution blocks](#execution-blocks) - [LUA script loading and execution](#lua-script-loading-and-execution) + - [Custom functions](#custom-functions) -Sometimes you want to ececute a series of Redis command atomically, such that nothing else may alter the database +Sometimes you may want to execute a series of Redis command atomically, such that nothing else may alter the database while the set of commands execute, so they may return a coherent state. For example, you want to set or query a -collection of related variables so they change together and are reported together. You have two choices. (1) you -can execute the Redis commands in an execution block, or else (2) load a LUA script onto the Redis server and call -it with some parameters (possibly many times over). +collection of related variables so they change together and are reported together. You have two choices. (1) you can +execute the Redis commands in an execution block, or else (2) load a LUA script onto the Redis server and call it with +some parameters (possibly many times over). ### Execution blocks -Execution blocks offer a fairly simple way of bunching +Execution blocks offer a fairly simple way of bunching together a set of Redis commands that need to be executed +atomically. Such an execution block in RedisX may look something like: ```c Redis *redis = ...; @@ -688,7 +771,9 @@ Execution blocks offer a fairly simple way of bunching ``` If at any point things don't go according to plan in the middle of the block, you can call `redisAbortBlockAsync()` to -abort and discard all prior commands submitted in the execution block already. +abort and discard all prior commands submitted in the execution block already. It is important to remembet that every +time you call `redisxStartBlockAsync()`, you must call either `redisxExecBlockAsync()` to execute it or else +`redisxAbortBlockAsync() to discard it. Failure to do so, will effectively end you up with a hung Redis client. ### LUA script loading and execution @@ -722,7 +807,7 @@ its SHA1 sum, a set of redis keys the script may use, and a set of other paramet int status; // Execute the script, with one redis key argument (and no parameters)... - RESP *r = redisxRequest("EVALSHA", SHA1, "1", "my-redis-key-argument", &status); + RESP *r = redisxRequest("EVALSHA", scriptSHA1, "1", "my-redis-key-argument", &status); // Check status and inspect RESP ... @@ -735,14 +820,24 @@ One thing to keep in mind about LUA scripts is that they are not persistent. The server is restarted. + +### Custom functions + +Functions, introduced in Redis 7, offer another evolutionary step over the LUA scripting described above. Unlike +scripts, functions are persistent and they can be called by name rather than a cryptic SHA1 sum. Otherwise, they offer +more or less the same functionality as scripts. __RedisX__ does not currently have a built-in high-level support +for managing and calling user-defined functions, but it is a feature that may be added in the not-too-distant future. +Stay tuned. + + ----------------------------------------------------------------------------- ## Error handling -Error handling of RedisX is an extension of that of __xchange__, with further error codes defined in `redisx.h`. -The RedisX functions that return an error status (either directly, or into the integer designated by a pointer -argument), can be inspected by `redisxErrorDescription()`, e.g.: +The principal error handling of RedisX is an extension of that of __xchange__, with further error codes defined in +`redisx.h`. The RedisX functions that return an error status (either directly, or into the integer designated by a +pointer argument), can be inspected by `redisxErrorDescription()`, e.g.: ```c Redis *redis ... @@ -754,6 +849,25 @@ argument), can be inspected by `redisxErrorDescription()`, e.g.: } ``` +In addition you can define your own handler function to deal with transmission (send/receive) errors, by defining +your own `RedisErrorHandler` function, such as: + +```c + void my_error_handler(Redis *redis, enum redisx_channel channel, const char *op) { + fprintf(stderr, "ERROR! %s: Redis at %s, channel %d\n", op, redis->id, channel); + } +``` + +Then activate it as: + +```c + Redis *redis = ... + + redisSetTransmitErrorHandler(redis, my_error_handler); +``` + +After that, every time there is an error with sending or receiving packets over the network to any of the Redis +clients used, your handler will report it the way you want it. ----------------------------------------------------------------------------- @@ -780,7 +894,8 @@ Some obvious ways the library could evolve and grow in the not too distant futur - Support for the [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) standard and Redis `HELLO`. - Support for Redis sentinel, high-availability server configurations. - TLS support (perhaps...) - - Add functions for `CLIENT TRACKING` / `CLIENT CACHING` support. + - Ass high-level support for managing and calling custom Redis functions. + - Add support for `CLIENT TRACKING` / `CLIENT CACHING`. - Add more high-level redis commands, e.g. for lists, streams, etc. - Improved debug capabilities (e.g. with built-in error traces) - Improved error handling (e.g. by consistently setting `errno` beyond just the __RedisX__ error status). diff --git a/src/redisx-client.c b/src/redisx-client.c index 2acb2f2..93e50ba 100644 --- a/src/redisx-client.c +++ b/src/redisx-client.c @@ -617,107 +617,6 @@ int redisxSendArrayRequestAsync(RedisClient *cl, char *args[], int lengths[], in return X_SUCCESS; } -/** - * Returns the result of the most generic type of Redis request with any number of arguments. This is not the - * highest throughput mode (that would be sending asynchronous pipeline request, and then asynchronously collecting - * the results such as with redisxSendArrayRequestAsync() / redisxReadReplyAsync(), because it requires separate network - * roundtrips for each and every request. But, it is simple and perfectly good method when one needs to retrieve - * only a few (<1000) variables per second... - * - * \param redis Pointer to a Redis instance. - * \param args An array of strings to send to Redis, corresponding to a single query. - * \param lengths Array indicating the number of bytes to send from each string argument. Zero - * values can be used to determine the string length automatically using strlen(), - * and the length argument itself may be NULL to determine the lengths of all - * string arguments automatically. - * \param n Number of string arguments. - * \param status Pointer to the return error status, which is either - * - * X_SUCCESS on success. - * X_NO_INIT if the Redis client librarywas not initialized via initRedis. - * X_NULL if the argument is NULL or n<1. - * X_NO_SERVICE if not connected to Redis. - * X_FAILURE If there was a socket level error. - * - * - * \return A freshly allocated RESP array containing the Redis response, or NULL if no valid - * response could be obtained. - * - * @sa redisxRequest() - * @sa redisxSendArrayRequestAsync() - * @sa redisxReadReplyAsync() - */ -RESP *redisxArrayRequest(Redis *redis, char *args[], int lengths[], int n, int *status) { - static const char *funcName = "redisxArrayRequest()"; - RESP *reply = NULL; - RedisClient *cl; - - if(redis == NULL || args == NULL || n < 1) *status = X_NULL; - else *status = X_SUCCESS; - - if(*status) { - redisxError(funcName, *status); - return NULL; - } - - xvprintf("Redis-X> request %s... [%d].\n", args[0], n); - - cl = redis->interactive; - *status = redisxLockEnabled(cl); - if(*status) { - redisxError(funcName, *status); - return NULL; - } - - *status = redisxSendArrayRequestAsync(cl, args, lengths, n); - if(!(*status)) reply = redisxReadReplyAsync(cl); - redisxUnlockClient(cl); - - if(*status) redisxError(funcName, *status); - - return reply; -} - -/** - * Returns the result of a Redis command with up to 3 regularly terminated string arguments. This is not the highest - * throughput mode (that would be sending asynchronous pipeline request, and then asynchronously collecting the results - * such as with redisxSendRequestAsync() / redisxReadReplyAsync(), because it requires separate network roundtrips for each - * and every request. But, it is simple and perfectly good method when one needs to retrieve only a few (<1000) - * variables per second... - * - * To make Redis calls with binary (non-string) data, you can use redisxArrayRequest() instead, where you can - * set the number of bytes for each argument explicitly. - * - * \param redis Pointer to a Redis instance. - * \param command Redis command, e.g. "HGET" - * \param arg1 First terminated string argument or NULL. - * \param arg2 Second terminated string argument or NULL. - * \param arg3 Third terminated string argument or NULL. - * \param status Pointer to the return error status, which is either X_SUCCESS on success or else - * the error code set by redisxArrayRequest(). - * - * \return A freshly allocated RESP array containing the Redis response, or NULL if no valid - * response could be obtained. - * - * @sa redisxArrayRequest() - * @sa redisxSendRequestAsync() - * @sa redisxReadReplyAsync() - */ -RESP *redisxRequest(Redis *redis, const char *command, const char *arg1, const char *arg2, const char *arg3, int *status) { - const char *args[] = { command, arg1, arg2, arg3 }; - int n; - - if(redis == NULL) return NULL; - - if(command == NULL) n = 0; - else if(arg1 == NULL) n = 1; - else if(arg2 == NULL) n = 2; - else if(arg3 == NULL) n = 3; - else n = 4; - - return redisxArrayRequest(redis, (char **) args, NULL, n, status); -} - /** * Silently consumes a reply from the specified Redis channel. * diff --git a/src/redisx.c b/src/redisx.c index f467061..15c847b 100644 --- a/src/redisx.c +++ b/src/redisx.c @@ -553,13 +553,107 @@ int redisxSetPipelineConsumer(Redis *redis, void (*f)(RESP *)) { } -/// \cond PRIVATE +/** + * Returns the result of a Redis command with up to 3 regularly terminated string arguments. This is not the highest + * throughput mode (that would be sending asynchronous pipeline request, and then asynchronously collecting the results + * such as with redisxSendRequestAsync() / redisxReadReplyAsync(), because it requires separate network roundtrips for each + * and every request. But, it is simple and perfectly good method when one needs to retrieve only a few (<1000) + * variables per second... + * + * To make Redis calls with binary (non-string) data, you can use redisxArrayRequest() instead, where you can + * set the number of bytes for each argument explicitly. + * + * \param redis Pointer to a Redis instance. + * \param command Redis command, e.g. "HGET" + * \param arg1 First terminated string argument or NULL. + * \param arg2 Second terminated string argument or NULL. + * \param arg3 Third terminated string argument or NULL. + * \param status Pointer to the return error status, which is either X_SUCCESS on success or else + * the error code set by redisxArrayRequest(). + * + * \return A freshly allocated RESP array containing the Redis response, or NULL if no valid + * response could be obtained. + * + * @sa redisxArrayRequest() + * @sa redisxSendRequestAsync() + * @sa redisxReadReplyAsync() + */ +RESP *redisxRequest(Redis *redis, const char *command, const char *arg1, const char *arg2, const char *arg3, int *status) { + const char *args[] = { command, arg1, arg2, arg3 }; + int n; + if(redis == NULL) return NULL; + if(command == NULL) n = 0; + else if(arg1 == NULL) n = 1; + else if(arg2 == NULL) n = 2; + else if(arg3 == NULL) n = 3; + else n = 4; + return redisxArrayRequest(redis, (char **) args, NULL, n, status); +} +/** + * Returns the result of the most generic type of Redis request with any number of arguments. This is not the + * highest throughput mode (that would be sending asynchronous pipeline request, and then asynchronously collecting + * the results such as with redisxSendArrayRequestAsync() / redisxReadReplyAsync(), because it requires separate network + * roundtrips for each and every request. But, it is simple and perfectly good method when one needs to retrieve + * only a few (<1000) variables per second... + * + * \param redis Pointer to a Redis instance. + * \param args An array of strings to send to Redis, corresponding to a single query. + * \param lengths Array indicating the number of bytes to send from each string argument. Zero + * values can be used to determine the string length automatically using strlen(), + * and the length argument itself may be NULL to determine the lengths of all + * string arguments automatically. + * \param n Number of string arguments. + * \param status Pointer to the return error status, which is either + * + * X_SUCCESS on success. + * X_NO_INIT if the Redis client librarywas not initialized via initRedis. + * X_NULL if the argument is NULL or n<1. + * X_NO_SERVICE if not connected to Redis. + * X_FAILURE If there was a socket level error. + * + * + * \return A freshly allocated RESP array containing the Redis response, or NULL if no valid + * response could be obtained. + * + * @sa redisxRequest() + * @sa redisxSendArrayRequestAsync() + * @sa redisxReadReplyAsync() + */ +RESP *redisxArrayRequest(Redis *redis, char *args[], int lengths[], int n, int *status) { + static const char *funcName = "redisxArrayRequest()"; + RESP *reply = NULL; + RedisClient *cl; + + if(redis == NULL || args == NULL || n < 1) *status = X_NULL; + else *status = X_SUCCESS; + + if(*status) { + redisxError(funcName, *status); + return NULL; + } + + xvprintf("Redis-X> request %s... [%d].\n", args[0], n); + + cl = redis->interactive; + *status = redisxLockEnabled(cl); + if(*status) { + redisxError(funcName, *status); + return NULL; + } + + *status = redisxSendArrayRequestAsync(cl, args, lengths, n); + if(!(*status)) reply = redisxReadReplyAsync(cl); + redisxUnlockClient(cl); + + if(*status) redisxError(funcName, *status); + + return reply; +} -/// \endcond /** * Returns a string description for one of the RM error codes.