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.