Async-Redis
view release on metacpan or search on metacpan
- Adds Future::Selector 0.05 as a runtime dependency.
- Security: TLS identity enforced by default
- SSL_hostname and SSL_verifycn_name set when connecting by
hostname. IP literals verify against IP SAN by default (fails
if the cert has no IP SAN for the connected address). New
verify_hostname option for opt-out when connecting by IP to a
hostname-only cert.
- Security: failed handshake no longer leaves object connected
- connect() rolls back (reset + typed error) on AUTH/SELECT
failure
- Password "0" is now sent correctly (was silently skipped by
truthy guard)
- Correctness: blocking command deadlines
- BLMPOP/BZMPOP read timeout from position 0 (was last)
- Server timeout 0 means indefinite (no client-side deadline)
- WAIT, WAITAOF, XREAD BLOCK, XREADGROUP BLOCK all covered
- Correctness: subscription lifecycle
- _close (intentional) distinct from _fail_fatal (unrecoverable);
iterator next() returns undef vs typed error accordingly
- Reconnect uses _pause_for_reconnect / _resume_after_reconnect
verbs that preserve replay state; _resume_after_reconnect
sets in_pubsub=1 before replay to mirror initial-subscribe
timing
- Identity-guarded parent-slot clearing prevents stale _close
from wiping a newer subscription
- Correctness: pool release and shutdown
- Double-release is a loud no-op (was silent double-push into
idle)
- Shutdown flag rejects further acquires; active releases are
destroyed
- refaddr-based identity (was stringification)
- release(undef) is a silent no-op
- Correctness: timeout and reset invariant
- _await_with_deadline non-throwing helper replaces the
wait_any + throwing await pattern that silently skipped
timeout cleanup
- _reader_fatal detaches inflight before closing the socket so
the typed error is preserved; idempotent via
_fatal_in_progress guard under eval+finally
- Correctness: transaction state cleanup
- watch() and multi_start() now set their state flags only
after the underlying command succeeds (previously set the
flag, then awaited; a failed command left the client claiming
to be in MULTI/WATCH state)
- watch_multi() unwinds WATCH on a callback die (previously a
callback exception left the connection holding watches; the
next command on the client would hit a poisoned state)
- DISCARD now correctly clears the watching flag (Redis DISCARD
drops watches; the client previously claimed to still be
watching)
- Breaking: removed `install` option from define_command
- The option used to install the script as a method on the
Async::Redis class. Pass `install => 1` now dies with a clear
message pointing callers at run_script(); use that or hold
the returned Async::Redis::Script directly.
- Privacy: OTel command arguments no longer in spans by default
- otel_include_args now defaults to 0; pass 1 to re-enable
- Added: message_queue_depth constructor option (default 1)
- Added: key prefixing for PFADD, PFCOUNT, PFMERGE, GETBIT, SETBIT,
BITCOUNT, BITPOS, HSTRLEN, ZMSCORE
- Added: Async::Redis::Cookbook (POD) â runnable, tested recipes
for connection management, pipelines, transactions, pubsub,
pool, scripts, and observability.
- Added: examples/ â async job queue, bulk insert, and a stress
harness (examples/stress/) that drives all major features under
load with chaos injection (CLIENT KILL) and integrity verification.
- Added: GitHub Actions CI workflow (thanks @GaNardelli, PR #4)
runs the full test suite with RELEASE_TESTING=1 against a
Dockerized Redis on every push and pull request.
- Documentation: TASK LIFECYCLE POD section explaining the
Future::Selector contract; user-disconnect vs reader-fatal path
distinction; broad POD review and corrections across the public
surface.
0.001008 2026-04-22
- Bug Fix: _reconnect no longer spins forever on permanent failure
- New reconnect_max_attempts constructor option (default 10;
0 = unlimited). Once exceeded, _reconnect dies with an
Async::Redis::Error::Disconnected; the failure propagates
through _reconnect_pubsub to the Subscription's read loop,
where it routes to on_fail / on_error per existing contract.
- Previously, an unreachable Redis would cause _reconnect to
loop with exponential backoff (capped at reconnect_delay_max
= 60s) indefinitely, with no way for a consumer to tell
"reconnecting" from "broken".
- Bug Fix: _reconnect_attempt resets to 0 on successful reconnect
- Previously the counter only incremented, so cumulative
reconnects across a long-running process caused subsequent
backoff delays to start from an ever-larger base
(0.1 * 2^N where N was total historical reconnects). Now
each successful reconnect resets the counter to 0.
0.001007 2026-04-21
- Feature: Callback-driven Subscription delivery
- New on_message($cb) and on_error($cb) setters on
Async::Redis::Subscription, alongside the existing
on_reconnect($cb)
- Callback signature is ($sub, $msg) / ($sub, $err), consistent
with on_reconnect
- Message hashref shape matches next(): type, channel, pattern
(always present, undef for non-pmessage), data
- Synchronous by default; return a Future from the callback for
opt-in backpressure (driver awaits it before reading the next
frame; failed Futures route to on_error)
- Once on_message is set, next() croaks â sticky callback mode
for the lifetime of the subscription
- Default on_error behavior is to die loudly to prevent silent
zombie subscriptions; register an explicit no-op to swallow
- Designed for fire-and-forget listeners (channel-layer
middleware, websocket gateways, background dispatchers) that
trigger Future::AsyncAwait "lost its returning future"
warnings with the iterator pattern
- Behavior change: Async::Redis::disconnect now calls _close on any
active Subscription before closing the socket, so the
subscription's driver doesn't trip over an EOF on its pending
read. Additive â existing iterator-mode callers are unaffected
- Internal: factored _dispatch_frame and _read_frame_with_reconnect
out of Subscription::next; shared between the iterator and
callback paths. Reconnect semantics are identical across both
- Pattern subscription responses now include pattern => undef on
non-pmessage frames (previously omitted); no exists() check needed
( run in 0.839 second using v1.01-cache-2.11-cpan-6aa56a78535 )