view release on metacpan or search on metacpan
0.09 2026-04-08
- Add benchmarks section to POD
- Add 19 practical examples in eg/
- Hide bundled hiredis symbols (--exclude-libs)
- Add Alpine Linux and Valkey to CI
0.08 2026-04-06
- Fork from EV::Hiredis, rename to EV::Redis
- Switch to ExtUtils::MakeMaker, update bundled hiredis to v1.3.0+
- Add reconnection, flow control, TLS/SSL, RESP3, fire-and-forget
- Add connection options (timeouts, keepalive, priority, cloexec, etc.)
- Fix multiple UAF/memory safety issues (ASAN-verified)
0.07 2023-05-03
- fix mem leak on "arrayref-" commands like mget (#17)
0.06 2023-04-25
- support of hiredis connect/command timeouts (#13)
- drop support of Perl 5.10 (#14)
0.05 2022-09-11
Maximum number of reconnection attempts. 0 means unlimited. Default is 0.
Negative values are treated as 0 (unlimited).
- priority => $num
Priority for the underlying libev IO watchers. Higher priority watchers are
invoked before lower priority ones. Valid range is -2 (lowest) to +2 (highest),
with 0 being the default. See [EV](https://metacpan.org/pod/EV) documentation for details on priorities.
- keepalive => $seconds
Enable TCP keepalive with the specified interval in seconds. When enabled,
the OS will periodically send probes on idle connections to detect dead peers.
0 means disabled (default). Recommended for long-lived connections behind
NAT gateways or firewalls.
- prefer\_ipv4 => $bool
Prefer IPv4 addresses when resolving hostnames. Mutually exclusive with
`prefer_ipv6`.
- prefer\_ipv6 => $bool
watchers are invoked before lower priority ones when multiple watchers are
pending. Valid range is -2 (lowest) to +2 (highest), with 0 being the default.
Values outside this range are clamped automatically.
Can be changed at any time, including while connected.
$redis->priority(1); # higher priority
$redis->priority(-1); # lower priority
$redis->priority(99); # clamped to 2
my $prio = $redis->priority; # get current priority
## keepalive($seconds)
Get or set the TCP keepalive interval in seconds. When set, the OS sends
periodic probes on idle connections to detect dead peers. 0 means disabled
(default). When set to a positive value while connected, takes effect
immediately. Setting to 0 while connected records the preference for future
connections but does not disable keepalives on the current socket.
## prefer\_ipv4($bool)
Get or set IPv4 preference for DNS resolution. Mutually exclusive with
`prefer_ipv6` (setting one clears the other). Takes effect on the next
connection.
## prefer\_ipv6($bool)
Get or set IPv6 preference for DNS resolution. Mutually exclusive with
deps/hiredis/README.md view on Meta::CPAN
These functions return `REDIS_OK` on success.
On failure, `REDIS_ERR` is returned and the underlying connection is closed.
To configure these for an asynchronous context (see *Asynchronous API* below), use `ac->c` to get the redisContext out of an asyncRedisContext.
```C
int redisEnableKeepAlive(redisContext *c);
int redisEnableKeepAliveWithInterval(redisContext *c, int interval);
```
Enables TCP keepalive by setting the following socket options (with some variations depending on OS):
* `SO_KEEPALIVE`;
* `TCP_KEEPALIVE` or `TCP_KEEPIDLE`, value configurable using the `interval` parameter, default 15 seconds;
* `TCP_KEEPINTVL` set to 1/3 of `interval`;
* `TCP_KEEPCNT` set to 3.
```C
int redisSetTcpUserTimeout(redisContext *c, unsigned int timeout);
```
deps/hiredis/sockcompat.c view on Meta::CPAN
return ret != SOCKET_ERROR ? ret : -1;
}
int win32_poll(struct pollfd *fds, nfds_t nfds, int timeout) {
int ret = WSAPoll(fds, nfds, timeout);
_updateErrno(ret != SOCKET_ERROR);
return ret != SOCKET_ERROR ? ret : -1;
}
int win32_redisKeepAlive(SOCKET sockfd, int interval_ms) {
struct tcp_keepalive cfg;
DWORD bytes_in;
int res;
cfg.onoff = 1;
cfg.keepaliveinterval = interval_ms;
cfg.keepalivetime = interval_ms;
res = WSAIoctl(sockfd, SIO_KEEPALIVE_VALS, &cfg,
sizeof(struct tcp_keepalive), NULL, 0,
&bytes_in, NULL, NULL);
return res == 0 ? 0 : _wsaErrorToErrno(res);
}
#endif /* _WIN32 */
lib/EV/Redis.pm view on Meta::CPAN
$self->on_error($args{on_error} // sub { die @_ });
$self->on_connect($args{on_connect}) if exists $args{on_connect};
$self->on_disconnect($args{on_disconnect}) if exists $args{on_disconnect};
$self->on_push($args{on_push}) if exists $args{on_push};
$self->connect_timeout($args{connect_timeout}) if defined $args{connect_timeout};
$self->command_timeout($args{command_timeout}) if defined $args{command_timeout};
$self->max_pending($args{max_pending}) if defined $args{max_pending};
$self->waiting_timeout($args{waiting_timeout}) if defined $args{waiting_timeout};
$self->resume_waiting_on_reconnect($args{resume_waiting_on_reconnect}) if defined $args{resume_waiting_on_reconnect};
$self->priority($args{priority}) if defined $args{priority};
$self->keepalive($args{keepalive}) if defined $args{keepalive};
$self->prefer_ipv4($args{prefer_ipv4}) if exists $args{prefer_ipv4};
$self->prefer_ipv6($args{prefer_ipv6}) if exists $args{prefer_ipv6};
$self->source_addr($args{source_addr}) if defined $args{source_addr};
$self->tcp_user_timeout($args{tcp_user_timeout}) if defined $args{tcp_user_timeout};
$self->cloexec($args{cloexec}) if exists $args{cloexec};
$self->reuseaddr($args{reuseaddr}) if exists $args{reuseaddr};
# Configure reconnect if specified
if ($args{reconnect}) {
$self->reconnect(
lib/EV/Redis.pm view on Meta::CPAN
Maximum number of reconnection attempts. 0 means unlimited. Default is 0.
Negative values are treated as 0 (unlimited).
=item * priority => $num
Priority for the underlying libev IO watchers. Higher priority watchers are
invoked before lower priority ones. Valid range is -2 (lowest) to +2 (highest),
with 0 being the default. See L<EV> documentation for details on priorities.
=item * keepalive => $seconds
Enable TCP keepalive with the specified interval in seconds. When enabled,
the OS will periodically send probes on idle connections to detect dead peers.
0 means disabled (default). Recommended for long-lived connections behind
NAT gateways or firewalls.
=item * prefer_ipv4 => $bool
Prefer IPv4 addresses when resolving hostnames. Mutually exclusive with
C<prefer_ipv6>.
=item * prefer_ipv6 => $bool
lib/EV/Redis.pm view on Meta::CPAN
watchers are invoked before lower priority ones when multiple watchers are
pending. Valid range is -2 (lowest) to +2 (highest), with 0 being the default.
Values outside this range are clamped automatically.
Can be changed at any time, including while connected.
$redis->priority(1); # higher priority
$redis->priority(-1); # lower priority
$redis->priority(99); # clamped to 2
my $prio = $redis->priority; # get current priority
=head2 keepalive($seconds)
Get or set the TCP keepalive interval in seconds. When set, the OS sends
periodic probes on idle connections to detect dead peers. 0 means disabled
(default). When set to a positive value while connected, takes effect
immediately. Setting to 0 while connected records the preference for future
connections but does not disable keepalives on the current socket.
=head2 prefer_ipv4($bool)
Get or set IPv4 preference for DNS resolution. Mutually exclusive with
C<prefer_ipv6> (setting one clears the other). Takes effect on the next
connection.
=head2 prefer_ipv6($bool)
Get or set IPv6 preference for DNS resolution. Mutually exclusive with
src/EV__Redis.xs view on Meta::CPAN
typedef ev_redis_t* EV__Redis;
typedef struct ev_loop* EV__Loop;
#define EV_REDIS_MAGIC 0xDEADBEEF
#define EV_REDIS_FREED 0xFEEDFACE
#define CLEAR_HANDLER(field) \
do { if (NULL != (field)) { SvREFCNT_dec(field); (field) = NULL; } } while(0)
struct ev_redis_s {
unsigned int magic; /* Set to EV_REDIS_MAGIC when alive */
struct ev_loop* loop;
redisAsyncContext* ac;
SV* error_handler;
SV* connect_handler;
SV* disconnect_handler;
SV* push_handler;
struct timeval* connect_timeout;
struct timeval* command_timeout;
ngx_queue_t cb_queue;
ngx_queue_t wait_queue;
src/EV__Redis.xs view on Meta::CPAN
int reconnect_delay_ms; /* delay between reconnect attempts */
int max_reconnect_attempts; /* 0 = unlimited */
int reconnect_attempts; /* current attempt count */
ev_timer reconnect_timer;
int reconnect_timer_active;
int intentional_disconnect; /* set before explicit disconnect() */
int priority; /* libev watcher priority, default 0 */
int in_cb_cleanup; /* prevent re-entrant cb_queue modification */
int in_wait_cleanup; /* prevent re-entrant wait_queue modification */
int callback_depth; /* nesting depth of C-level callbacks invoking Perl code */
int keepalive; /* TCP keepalive interval in seconds, 0 = disabled */
int prefer_ipv4; /* prefer IPv4 DNS resolution */
int prefer_ipv6; /* prefer IPv6 DNS resolution */
char* source_addr; /* local address to bind to */
unsigned int tcp_user_timeout; /* TCP_USER_TIMEOUT in ms, 0 = OS default */
int cloexec; /* set SOCK_CLOEXEC on socket */
int reuseaddr; /* set SO_REUSEADDR on socket */
redisAsyncContext* ac_saved; /* saved ac pointer for deferred disconnect cleanup */
#ifdef EV_REDIS_SSL
redisSSLContext* ssl_ctx;
#endif
src/EV__Redis.xs view on Meta::CPAN
opts->options |= REDIS_OPT_SET_SOCK_CLOEXEC;
}
if (self->reuseaddr) {
opts->options |= REDIS_OPT_REUSEADDR;
}
if (NULL != self->source_addr && NULL == self->path) {
opts->endpoint.tcp.source_addr = self->source_addr;
}
}
/* Set up a newly allocated redisAsyncContext: SSL, keepalive, libev, callbacks.
* On failure: frees ac, nulls self->ac, emits error with err_prefix. */
static int post_connect_setup(EV__Redis self, const char* err_prefix) {
self->ac_saved = NULL;
self->ac->data = (void*)self;
#ifdef EV_REDIS_SSL
if (NULL != self->ssl_ctx) {
if (REDIS_OK != redisInitiateSSLWithContext(&self->ac->c, self->ssl_ctx)) {
SV* err = sv_2mortal(newSVpvf("%s: SSL initiation failed: %s",
err_prefix, self->ac->errstr[0] ? self->ac->errstr : "unknown error"));
redisAsyncFree(self->ac);
self->ac = NULL;
emit_error(self, err);
return REDIS_ERR;
}
}
#endif
if (self->keepalive > 0) {
redisEnableKeepAliveWithInterval(&self->ac->c, self->keepalive);
}
if (self->tcp_user_timeout > 0) {
redisSetTcpUserTimeout(&self->ac->c, self->tcp_user_timeout);
}
if (REDIS_OK != redisLibevAttach(self->loop, self->ac)) {
SV* err = sv_2mortal(newSVpvf("%s: cannot attach libev", err_prefix));
redisAsyncFree(self->ac);
self->ac = NULL;
emit_error(self, err);
src/EV__Redis.xs view on Meta::CPAN
if (cbt->sub_count <= 0) {
Safefree(cbt);
}
}
return;
}
/* self is NULL when DESTROY nulled ac->data (deferred free inside
* REDIS_IN_CALLBACK) or during PL_dirty. Still invoke the callback
* with a disconnect error so users can clean up resources. The hiredis
* context (c) is still alive here â safe to read c->errstr.
* cb may be NULL during PL_dirty where we pre-null it.
* For persistent commands (multi-channel subscribe), hiredis fires
* reply_cb once per channel with the same cbt. Invoke the callback
* only once (null cb after), use sub_count to track when to free. */
if (self == NULL) {
if (NULL != cbt->cb) {
invoke_callback_error(cbt->cb,
sv_2mortal(newSVpv(c->errstr[0] ? c->errstr : "disconnected", 0)));
SvREFCNT_dec(cbt->cb);
cbt->cb = NULL;
src/EV__Redis.xs view on Meta::CPAN
}
self->callback_depth--;
self->current_cb = NULL;
/* If DESTROY was called during our callback (e.g., user undef'd $redis),
* self->magic is EV_REDIS_FREED but self is still valid (DESTROY defers
* Safefree when callback_depth > 0). Complete cleanup here.
* For persistent commands (multi-channel subscribe), hiredis will fire
* reply_cb again for remaining channels via __redisAsyncFree. Null the
* callback to prevent double invocation, but leave cbt alive so those
* later calls see it and can track sub_count for proper cleanup. */
if (self->magic == EV_REDIS_FREED) {
if (NULL != cbt->cb) {
SvREFCNT_dec(cbt->cb);
cbt->cb = NULL;
}
if (!cbt->persist) {
Safefree(cbt);
}
check_destroyed(self);
src/EV__Redis.xs view on Meta::CPAN
disconnect(EV::Redis self);
CODE:
{
/* Stop any pending reconnect timer on explicit disconnect */
self->intentional_disconnect = 1;
stop_reconnect_timer(self);
self->reconnect_attempts = 0;
if (NULL == self->ac) {
/* Already disconnected â still stop waiting timer and clear
* wait queue (e.g., resume_waiting_on_reconnect kept them alive
* after a connection drop, but user now explicitly disconnects). */
stop_waiting_timer(self);
if (!ngx_queue_empty(&self->wait_queue)) {
self->callback_depth++;
clear_wait_queue_sv(self, err_disconnected);
self->callback_depth--;
check_destroyed(self);
}
return;
}
src/EV__Redis.xs view on Meta::CPAN
if (NULL != self->ac) {
redisLibevSetPriority(self->ac, prio);
}
}
RETVAL = self->priority;
}
OUTPUT:
RETVAL
int
keepalive(EV::Redis self, SV* value = NULL);
CODE:
{
if (NULL != value && SvOK(value)) {
int interval = SvIV(value);
if (interval < 0) croak("keepalive interval must be non-negative");
if (interval > MAX_TIMEOUT_MS / 1000) croak("keepalive interval too large");
self->keepalive = interval;
if (NULL != self->ac && interval > 0) {
redisEnableKeepAliveWithInterval(&self->ac->c, interval);
}
}
RETVAL = self->keepalive;
}
OUTPUT:
RETVAL
int
prefer_ipv4(EV::Redis self, SV* value = NULL);
CODE:
{
if (NULL != value && SvOK(value)) {
self->prefer_ipv4 = SvTRUE(value) ? 1 : 0;
t/options.t view on Meta::CPAN
use lib 't/lib';
use RedisTestHelper qw(get_redis_version);
my $redis_server;
eval {
$redis_server = Test::RedisServer->new;
} or plan skip_all => 'redis-server is required to this test';
my %connect_info = $redis_server->connect_info;
# --- keepalive ---
{
my $r = EV::Redis->new;
is $r->keepalive, 0, 'keepalive default is 0';
$r->keepalive(15);
is $r->keepalive, 15, 'keepalive setter/getter roundtrip';
$r->keepalive(0);
is $r->keepalive, 0, 'keepalive can be disabled';
}
{
my $r = EV::Redis->new(keepalive => 30);
is $r->keepalive, 30, 'keepalive via constructor';
}
{
eval { EV::Redis->new->keepalive(-1) };
like $@, qr/non-negative/, 'keepalive rejects negative';
eval { EV::Redis->new->keepalive(2_000_001) };
like $@, qr/too large/, 'keepalive rejects too large';
}
# keepalive set while connected
{
my $r = EV::Redis->new(
path => $connect_info{sock},
on_error => sub { },
);
my $t; $t = EV::timer 0.1, 0, sub {
undef $t;
$r->keepalive(10);
is $r->keepalive, 10, 'keepalive set while connected';
$r->disconnect;
};
EV::run;
}
# --- prefer_ipv4 / prefer_ipv6 ---
{
my $r = EV::Redis->new;
is $r->prefer_ipv4, 0, 'prefer_ipv4 default is 0';
);
die "exec redis-server failed: $!";
}
};
if ($@ || !$redis_pid) {
diag "Failed to start TLS Redis: $@";
done_testing;
exit;
}
# Wait for Redis to be ready (check if process is alive and port is listening)
my $ready = 0;
for (1..50) {
# Check if the child process died (TLS not compiled, bad args, etc.)
my $kid = waitpid($redis_pid, POSIX::WNOHANG());
if ($kid > 0) {
$redis_pid = undef;
last;
}
# Try connecting with redis-cli over TLS
if (system("redis-cli -h 127.0.0.1 -p $tls_port --tls --insecure PING >/dev/null 2>&1") == 0) {
xt/pod_spell.t view on Meta::CPAN
str
utf-8
backend
reconnection
libev
TLS
tls
SSL
ssl
SNI
keepalive
keepalives
IPv4
IPv6
NAT
capath
OpenSSL
TCP
RESP3
CLOEXEC
cloexec
reuseaddr