Async-Redis

 view release on metacpan or  search on metacpan

META.json  view on Meta::CPAN

            "ExtUtils::MakeMaker" : "0"
         }
      },
      "develop" : {
         "requires" : {
            "Dist::Zilla" : "0"
         }
      },
      "runtime" : {
         "recommends" : {
            "IO::Socket::SSL" : "0",
            "Protocol::Redis::XS" : "0"
         },
         "requires" : {
            "Digest::SHA" : "0",
            "Future" : "0.49",
            "Future::AsyncAwait" : "0.66",
            "Future::IO" : "0.23",
            "Future::Selector" : "0.05",
            "IO::Socket::INET" : "0",
            "Protocol::Redis" : "0",

META.yml  view on Meta::CPAN

generated_by: 'Dist::Zilla version 6.032, CPAN::Meta::Converter version 2.150010'
license: artistic_2
meta-spec:
  url: http://module-build.sourceforge.net/META-spec-v1.4.html
  version: '1.4'
name: Async-Redis
no_index:
  directory:
    - examples
recommends:
  IO::Socket::SSL: '0'
  Protocol::Redis::XS: '0'
requires:
  Digest::SHA: '0'
  Future: '0.49'
  Future::AsyncAwait: '0.66'
  Future::IO: '0.23'
  Future::Selector: '0.05'
  IO::Socket::INET: '0'
  Protocol::Redis: '0'
  Socket: '0'

README.md  view on Meta::CPAN

**Required:**
- Future::IO (0.19+)
- Future::AsyncAwait
- Protocol::Redis

**Recommended:**
- Protocol::Redis::XS (faster parsing)
- IO::Async (or your preferred event loop)

**Optional:**
- IO::Socket::SSL (for TLS)
- OpenTelemetry::SDK (for observability)

## See Also

- [Future::IO](https://metacpan.org/pod/Future::IO) - The underlying async I/O abstraction
- [Future::AsyncAwait](https://metacpan.org/pod/Future::AsyncAwait) - Async/await syntax
- [Redis](https://metacpan.org/pod/Redis) - Synchronous Redis client
- [Net::Async::Redis](https://metacpan.org/pod/Net::Async::Redis) - Another async Redis client

## Author

cpanfile  view on Meta::CPAN

requires 'Future::IO', '0.23';
requires 'Future::Selector', '0.05';
requires 'Protocol::Redis';
requires 'IO::Socket::INET';
requires 'Socket';
requires 'Time::HiRes';
requires 'Digest::SHA';

# Optional dependencies (for performance/features)
recommends 'Protocol::Redis::XS';  # Faster RESP parsing
recommends 'IO::Socket::SSL';      # TLS support
suggests 'OpenTelemetry::SDK';     # Observability integration

# Test dependencies
on 'test' => sub {
    requires 'Test2::V0';
    requires 'Test::Lib';
};

# Development dependencies
on 'develop' => sub {

lib/Async/Redis.pm  view on Meta::CPAN

        ? $raw / 1000
        : $raw + 0;

    # Zero means block indefinitely — no client-side deadline
    return undef if $seconds == 0;

    return Time::HiRes::time() + $seconds + $self->{blocking_timeout_buffer};
}

sub _ssl_verify_peer {
    require IO::Socket::SSL;
    return IO::Socket::SSL::SSL_VERIFY_PEER();
}

sub _ssl_verify_none {
    require IO::Socket::SSL;
    return IO::Socket::SSL::SSL_VERIFY_NONE();
}

# Build the IO::Socket::SSL option hash for the current connection.
# Handles chain verification, SNI, hostname identity checking, and
# client cert/key/CA forwarding. Called by _tls_upgrade and directly
# by unit tests.
sub _build_tls_options {
    my ($self) = @_;
    my %ssl_opts = (SSL_startHandshake => 0);

    my $tls      = $self->{tls};
    my $tls_hash = ref $tls eq 'HASH' ? $tls : {};

lib/Async/Redis.pm  view on Meta::CPAN

        $ssl_opts{SSL_verify_mode} = $self->_ssl_verify_none;
    }

    return %ssl_opts;
}

# Non-blocking TLS upgrade
async sub _tls_upgrade {
    my ($self, $socket) = @_;

    require IO::Socket::SSL;

    my %ssl_opts = $self->_build_tls_options;

    # Start SSL (does not block because SSL_startHandshake => 0)
    IO::Socket::SSL->start_SSL($socket, %ssl_opts)
        or die Async::Redis::Error::Connection->new(
            message => "SSL setup failed: " . IO::Socket::SSL::errstr(),
            host    => $self->{host},
            port    => $self->{port},
        );

    # Drive handshake with non-blocking loop
    my $deadline = Time::HiRes::time() + $self->{connect_timeout};

    while (1) {
        # Check timeout
        if (Time::HiRes::time() >= $deadline) {

lib/Async/Redis.pm  view on Meta::CPAN


        if ($rv) {
            # Handshake complete!
            return $socket;
        }

        # Check what the handshake needs
        my $remaining = $deadline - Time::HiRes::time();
        $remaining = 0.1 if $remaining <= 0;

        if ($IO::Socket::SSL::SSL_ERROR == IO::Socket::SSL::SSL_ERROR_WANT_READ()) {
            # Wait for socket to become readable with timeout
            my $read_f = Future::IO->waitfor_readable($socket);
            my $timeout_f = Future::IO->sleep($remaining)->then(sub {
                return Future->fail('tls_timeout');
            });

            my $wait_f = Future->wait_any($read_f, $timeout_f);
            await $wait_f;

            if ($wait_f->is_failed) {
                die Async::Redis::Error::Timeout->new(
                    message => "TLS handshake timed out",
                    timeout => $self->{connect_timeout},
                );
            }
        }
        elsif ($IO::Socket::SSL::SSL_ERROR == IO::Socket::SSL::SSL_ERROR_WANT_WRITE()) {
            # Wait for socket to become writable with timeout
            my $write_f = Future::IO->waitfor_writable($socket);
            my $timeout_f = Future::IO->sleep($remaining)->then(sub {
                return Future->fail('tls_timeout');
            });

            my $wait_f = Future->wait_any($write_f, $timeout_f);
            await $wait_f;

            if ($wait_f->is_failed) {
                die Async::Redis::Error::Timeout->new(
                    message => "TLS handshake timed out",
                    timeout => $self->{connect_timeout},
                );
            }
        }
        else {
            # Actual error
            die Async::Redis::Error::Connection->new(
                message => "TLS handshake failed: " . IO::Socket::SSL::errstr(),
                host    => $self->{host},
                port    => $self->{port},
            );
        }
    }
}

# Reconnect with exponential backoff
async sub _reconnect {
    my ($self) = @_;

t/10-connection/tls.t  view on Meta::CPAN

use warnings;
use Test::Lib;
use Test::Async::Redis ':redis';
use Test2::V0;
use Async::Redis;
use Time::HiRes qw(time);

# Helper: await a Future and return its result (throws on failure)

subtest 'TLS module availability' => sub {
    my $has_ssl = eval { require IO::Socket::SSL; 1 };
    ok(1, "IO::Socket::SSL " . ($has_ssl ? "available" : "not available"));
};

subtest 'constructor accepts TLS parameters' => sub {
    my $redis = Async::Redis->new(
        host => 'localhost',
        tls  => 1,
    );

    is($redis->{tls}, 1, 'tls enabled');
};

t/10-connection/tls.t  view on Meta::CPAN


subtest 'rediss URI enables TLS' => sub {
    my $redis = Async::Redis->new(
        uri => 'rediss://localhost:6380',
    );

    ok($redis->{tls}, 'TLS enabled from rediss://');
};

SKIP: {
    my $has_ssl = eval { require IO::Socket::SSL; 1 };
    skip "IO::Socket::SSL not available", 1 unless $has_ssl;

    subtest 'TLS without server fails gracefully' => sub {
        my $redis = Async::Redis->new(
            host            => 'localhost',
            port            => 16380,  # unlikely to have TLS Redis here
            tls             => 1,
            connect_timeout => 1,
        );

        my $error;

t/10-connection/tls.t  view on Meta::CPAN


        ok($error, 'connection failed (expected - no TLS server)');
    };
}

# TLS tests with actual TLS Redis require specific setup
SKIP: {
    skip "Set TLS_REDIS_HOST and TLS_REDIS_PORT to test TLS", 3
        unless $ENV{TLS_REDIS_HOST} && $ENV{TLS_REDIS_PORT};

    my $has_ssl = eval { require IO::Socket::SSL; 1 };
    skip "IO::Socket::SSL not available", 3 unless $has_ssl;

    subtest 'TLS connection works' => sub {
        my $redis = Async::Redis->new(
            host => $ENV{TLS_REDIS_HOST},
            port => $ENV{TLS_REDIS_PORT},
            tls  => {
                verify => 0,  # skip verification for testing
            },
        );



( run in 2.056 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )