EV-Redis

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

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

README.md  view on Meta::CPAN


    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

README.md  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

## 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';

t/tls.t  view on Meta::CPAN

        );
        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



( run in 1.940 second using v1.01-cache-2.11-cpan-39bf76dae61 )