From c5a5395f1d2037996468bee8333428bec6c04934 Mon Sep 17 00:00:00 2001 From: Attila Kovacs Date: Sat, 14 Dec 2024 11:00:37 +0100 Subject: [PATCH] Improved socket-level configuration and site update --- README.md | 88 +++++++++++++++++++++++++++---------------- include/redisx-priv.h | 2 + include/redisx.h | 16 +++++++- src/redisx-client.c | 2 +- src/redisx-net.c | 29 ++++++++++---- src/redisx.c | 31 ++++++++++++++- 6 files changed, 123 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index f4879eb..0c840b7 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Before then the API may undergo slight changes and tweaks. Use the repository as | connect over TCP | __yes__ | | | connect over UDP | no | (why would you, really?) | | connect / disconnect hooks | __yes__ | | - | socket level configuration | __yes__ | User adjustable timeout and buffer size | + | socket level configuration | __yes__ | user-defined timeout and buffer size + callback | | socket error handling | __yes__ | user-defined callback | | RESP to JSON | __yes__ | via `xchange` library | | RESP to structured data | __yes__ | via `xchange` library | @@ -261,26 +261,6 @@ The first step is to create a `Redis` object, with the server name or IP address redisxSetPort(redis, 7089); ``` -Alternatively, you may initialize the client for a high-availability configuration using with a set of -[Redis Sentinel](https://redis.io/docs/latest/develop/reference/sentinel-clients/) servers, using -`redisxInitSentinel()`, e.g.: - -```c - // An array defining N sentinel servers and ports to use. - // A port number 0 or negative will use the default Redis port of 6379. - RedisServer sentinels[N] = { { "server1", 0 }, { "server2", 7024 } ... }; - - // Configure a Redis client instance for the Sentinel servers and "my-service" service name - Redis *redis = redisxInitSentinel(sentinels, N, "my-service"); - if (redis == NULL) { - // Abort: something did not got to plan... - return; - } - - // (optional) set a sentinel discovery timeout in ms... - redisxSetSentinelTimeout(redis, 30); -``` - Before connecting to the Redis server, you may configure the database authentication (if any): ```c @@ -305,30 +285,72 @@ will not be used, and RESP2 will be assumed -- which is best for older servers. actual protocol used after connecting, using `redisxGetProtocol()`). Note, that after connecting, you may retrieve the set of server properties sent in response to `HELLO` using `redisxGetHelloData()`. +Optionally, you can select the database index to use now (or later, after connecting), if not the default (index +0): + +```c + // (optional) Select the database index 2 + redisxSelectDB(redis, 2); +``` + +Note, that you can switch the database index any time, with the caveat that it's not possible to change it for the +subscription client when there are active subscriptions. + +#### Sentinel + +Alternatively, instead of `redisxInit()` above you may initialize the client for a high-availability configuration +using with a set of [Redis Sentinel](https://redis.io/docs/latest/develop/reference/sentinel-clients/) servers, using +`redisxInitSentinel()`, e.g.: + +```c + // An array defining N sentinel servers and ports to use. + // A port number 0 or negative will use the default Redis port of 6379. + RedisServer sentinels[N] = { { "server1", 0 }, { "server2", 7024 } ... }; + + // Configure a Redis client instance for the Sentinel servers and "my-service" service name + Redis *redis = redisxInitSentinel(sentinels, N, "my-service"); + if (redis == NULL) { + // Abort: something did not got to plan... + return; + } + + // (optional) set a sentinel discovery timeout in ms... + redisxSetSentinelTimeout(redis, 30); +``` + +After successful initialization, you may proceed with the configuration the same way as for the regular standalone +server connection above. + + +#### Socket-level configuration + You might also tweak the socket options used for clients, if you find the socket defaults sub-optimal for your -application (note, that this setting is common to all `Redis` instances managed by the library): +application: ```c // (optional) Set 1000 ms socket read/write timeout for future connections. redisxSetSocketTimeout(redis, 1000); // (optional) Set the TCP send/rcv buffer sizes to use if not default values. - // This setting applies to all new connections after... - redisxSetTcpBuf(65536); + redisxSetTcpBuf(redis, 65536); ``` -Optionally, you can select the database index to use now (or later, after connecting), if not the default (index -0): +If you want, you can perform further customization of the client sockets via a user-defined callback function, e.g.: ```c - Redis *redis = ... - - // (optional) Select the database index 2 - redisxSelectDB(redis, 2); + int my_socket_config(int sock, enum redisx_channel channel) { + // Set up the socket any way you like... + ... + + return X_SUCCESS; + } ``` -Note, that you can switch the database index any time, with the caveat that it's not possible to change it for the -subscription client when there are active subscriptions. +which you can then apply to your Redis instance as: + +```c + redisxSetSocketConfigurator(my_socket_config); +``` @@ -1259,7 +1281,7 @@ Then activate it as: ```c Redis *redis = ... - redisxSetTransmitErrorHandler(redis, my_error_handler); + redisxSetSocketErrorHandler(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 diff --git a/include/redisx-priv.h b/include/redisx-priv.h index e2bc4f3..5b62c74 100644 --- a/include/redisx-priv.h +++ b/include/redisx-priv.h @@ -70,9 +70,11 @@ typedef struct { char *password; ///< Redis password (if any) int timeoutMillis; ///< [ms] Socket read/write timeout + int tcpBufSize; ///< [bytes] TCP read/write buffer sizes to use int protocol; ///< RESP version to use boolean hello; ///< whether to use HELLO (introduced in Redis 6.0.0 only) RESP *helloData; ///< RESP data received from server during last connection. + RedisSocketConfigurator socketConf; ///< Additional user configuration of client sockets RedisClient *clients; diff --git a/include/redisx.h b/include/redisx.h index 50462dd..2af4a47 100644 --- a/include/redisx.h +++ b/include/redisx.h @@ -343,14 +343,26 @@ typedef void (*RedisPushProcessor)(RedisClient *cl, RESP *message, void *ptr); +/** + * User callback function allowing additional customization of the client socket before connection. + * + * @param socket The socket descriptor + * @param channel REDISX_INTERACTIVE_CHANNEL, REDISX_PIPELINE_CHANNEL, REDISX_SUBSCRIPTION_CHANNEL + * @return X_SUCCESS (0) if the socket may be used as is after the return. Any other value + * will indicate that the socket should not be used and that the caller itself should + * fail with an error. + */ +typedef int (*RedisSocketConfigurator)(int socket, enum redisx_channel channel); + void redisxSetVerbose(boolean value); boolean redisxIsVerbose(); void redisxDebugTraffic(boolean value); -void redisxSetTcpBuf(int size); int redisxSetSocketTimeout(Redis *redis, int millis); +int redisxSetTcpBuf(Redis *redis, int size); int redisxSetSentinelTimeout(Redis *redis, int millis); -int redisxSetTransmitErrorHandler(Redis *redis, RedisErrorHandler f); +int redisxSetSocketConfigurator(Redis *redis, RedisSocketConfigurator func); +int redisxSetSocketErrorHandler(Redis *redis, RedisErrorHandler f); int redisxSetPort(Redis *redis, int port); int redisxSetUser(Redis *redis, const char *username); diff --git a/src/redisx-client.c b/src/redisx-client.c index b9b9bc2..c337f21 100644 --- a/src/redisx-client.c +++ b/src/redisx-client.c @@ -69,7 +69,7 @@ int rCheckClient(const RedisClient *cl) { * @param op The operation that failed, e.g. 'send' or 'read'. * @return X_NO_SERVICE * - * @sa redisxSetTransmitErrorHandler() + * @sa redisxSetSocketErrorHandler() */ static int rTransmitError(ClientPrivate *cp, const char *op) { if(cp->isEnabled) { diff --git a/src/redisx-net.c b/src/redisx-net.c index 43f41ae..7b0be78 100644 --- a/src/redisx-net.c +++ b/src/redisx-net.c @@ -49,7 +49,6 @@ typedef struct ServerLink { static ServerLink *serverList; static pthread_mutex_t serverLock = PTHREAD_MUTEX_INITIALIZER; -static int tcpBufSize = REDISX_TCP_BUF_SIZE; /** @@ -113,11 +112,12 @@ static int rSetServerAsync(Redis *redis, const char *desc, const char *hostname, * Configure the Redis client sockets for optimal performance... * * \param socket The socket file descriptor. - * \param timeoutMillis [ms] Socket read/write timeout, or <0 to no set. + * \param timeoutMillis [ms] Socket read/write timeout, or <=0 to no set. + * \param tcpBufSize [bytes] Socket read / write buffer sizes, or <=0 to not set; * \param lowLatency TRUE (non-zero) if socket is to be configured for low latency, or else FALSE (0). * */ -static void rConfigSocket(int socket, int timeoutMillis, boolean lowLatency) { +static void rConfigSocket(int socket, int timeoutMillis, int tcpBufSize, boolean lowLatency) { const boolean enable = TRUE; if(timeoutMillis > 0) { @@ -716,7 +716,11 @@ int rConnectClient(Redis *redis, enum redisx_channel channel) { if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) return x_error(X_NO_SERVICE, errno, fn, "client %d socket creation failed", channel); - rConfigSocket(sock, p->timeoutMillis, rIsLowLatency(cp)); + rConfigSocket(sock, p->timeoutMillis, p->tcpBufSize, rIsLowLatency(cp)); + + if(p->socketConf) { + prop_error(fn, p->socketConf(sock, channel)); + } while(connect(sock, (struct sockaddr *) &serverAddress, sizeof(serverAddress)) != 0) { close(sock); @@ -827,6 +831,7 @@ Redis *redisxInit(const char *server) { p->protocol = REDISX_RESP2; // Default p->timeoutMillis = REDISX_DEFAULT_TIMEOUT_MILLIS; + p->tcpBufSize = REDISX_TCP_BUF_SIZE; // Create clients... p->clients = (RedisClient *) calloc(3, sizeof(RedisClient)); @@ -1010,11 +1015,21 @@ void redisxDestroy(Redis *redis) { /** * Set the size of the TCP/IP buffers (send and receive) for future client connections. * + * @param redis Pointer to a Redis instance. * @param size (bytes) requested buffer size, or <= 0 to use default value + * @return X_SUCCESS (0) if successful, or else X_NULL if the redis instance is NULL, + * or X_NO_INIT if the redis instance is not initialized, or X_FAILURE + * if Redis was initialized in Sentinel configuration. */ -void redisxSetTcpBuf(int size) { - xvprintf("Redis-X> Setting TCP buffer to %d\n.", size); - tcpBufSize = size; +int redisxSetTcpBuf(Redis *redis, int size) { + RedisPrivate *p; + + prop_error("redisxSetTcpBuf", rConfigLock(redis)); + p = (RedisPrivate *) redis->priv; + p->tcpBufSize = size; + rConfigUnlock(redis); + + return X_SUCCESS; } /** diff --git a/src/redisx.c b/src/redisx.c index da99e6f..5e6a287 100644 --- a/src/redisx.c +++ b/src/redisx.c @@ -230,6 +230,31 @@ enum redisx_protocol redisxGetProtocol(Redis *redis) { return protocol; } +/** + * Sets a user-defined callback for additioan custom configuring of client sockets + * + * + * @param redis The Redis server instance + * @param func The user-defined callback function, which performs the additional socket configuration + * @return X_SUCCESS (0) if successful, or or X_NULL if the redis argument in NULL, X_NO_INIT + * if the redis instance was not initialized. + * + * @sa redisxSetSocketErrorHandler() + */ +int redisxSetSocketConfigurator(Redis *redis, RedisSocketConfigurator func) { + static const char *fn = "redisxSetSocketConfigurator"; + + RedisPrivate *p; + + prop_error(fn, rConfigLock(redis)); + p = (RedisPrivate *) redis->priv; + p->hello = TRUE; + p->socketConf = func; + rConfigUnlock(redis); + + return X_SUCCESS; +} + /** * Sets the user-specific error handler to call if a socket level trasmit error occurs. * It replaces any prior handlers set earlier. @@ -247,9 +272,11 @@ enum redisx_protocol redisxGetProtocol(Redis *redis) { * * \return X_SUCCESS if the handler was successfully configured, or X_NULL if the * Redis instance is NULL. + * + * @sa redisxSetSocketConfigurator() */ -int redisxSetTransmitErrorHandler(Redis *redis, RedisErrorHandler f) { - static const char *fn = "redisxSetTransmitErrorHandler"; +int redisxSetSocketErrorHandler(Redis *redis, RedisErrorHandler f) { + static const char *fn = "redisxSetSocketErrorHandler"; RedisPrivate *p;