Revision history for Async-Redis
0.002000 2026-04-26
- Architectural: unified single-reader socket ownership model
- One reader loop dispatches frames to inflight queue or active
subscription by frame type and connection mode
- Pipelines, auto-pipeline, and pubsub all participate in the
same FIFO via inflight entries; the _reading_responses flag
is removed
- Every submission goes through a write gate so inflight
registration order matches wire-write order
- Callback-driven subscriptions consume from an internal message
queue rather than reading the socket directly; subscribe /
psubscribe / ssubscribe route through _execute_command and
the unified reader
- Architectural: structured-concurrency contract via Future::Selector
- New Future::Selector field (_tasks) owns every fire-and-forget
background task on the client (reader, reconnect, autopipeline
submits, subscription callback driver). Replaces the previous
slot-per-task / ->retain pattern.
- Reader failure now propagates to any caller awaiting inside
the scope. Previously, an unhandled exception in the reader
left awaiting callers hanging while the client reported
connected=1; awaits now fail with the typed error.
- User-initiated disconnect takes a distinct path from
_reader_fatal: drains write-gate waiters with a typed
Disconnected error and cancels any in-flight reconnect task,
so commands suspended on the write gate unwind instead of
staying parked.
- 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
0.001006 2026-03-16
- Bug Fix: Pool forwards all connection parameters
- Pool now passes prefix, client_name, username, request_timeout,
path, and all other Async::Redis options through to connections
- Previously these were silently dropped when Pool created connections
- Bug Fix: Typed error objects for Redis command errors
- _decode_response now dies with Async::Redis::Error::Redis objects
instead of plain "Redis error: ..." strings
- AutoPipeline uses blessed() check instead of string matching,
fixing false positives for values starting with "Redis error:"
- Pipeline results contain error objects at failed slot positions
- Bug Fix: Pool max connection race condition
- Concurrent acquire() calls could exceed max connections
- Added _creating counter to track in-flight connection creations
- Dependency Change: Future::IO bumped to 0.23 (was 0.19)
- Future::IO 0.22+ broke load_impl('IOAsync') by adding a poll
method check that IO::Async's impl doesn't satisfy
- Removed IO::Async as a test dependency entirely
- Tests now use Future::IO's built-in poll-based default impl
- Test suite is fully event-loop agnostic
- Examples: pagi-chat stats timer uses Future::IO instead of IO::Async
0.001005 2026-03-15
- Feature: Unix domain socket connections
- Connect via path parameter or redis+unix:// URI scheme
- Constructor stores path and skips host/port for unix sockets
- Added docker-compose redis-unix service for testing
- Feature: PubSub auto-resubscribe on reconnect
- Subscriptions automatically re-established after connection drop
- on_reconnect callback on Subscription object for notification
- _read_pubsub_frame checks connected state to fail fast
- _reset_connection now clears in_pubsub flag
- Bug Fix: _reset_connection left in_pubsub=1 after disconnect
- Prevented reconnection because AUTH/SELECT blocked by pubsub guard
0.001004 2026-02-02
- Bug Fix: Socket close ordering
- Fixed Future::IO corruption when disconnect() called with active watchers
- Cancel _current_read_future before socket close ensures Future::IO
unregisters watchers while fileno is still valid
- Let Perl's DESTROY handle close() after futures are cancelled
- Breaking Change: Future::IO configuration
- Removed Future::IO->load_best_impl from module
- Libraries should not configure Future::IO - only application entry
points should (following LeoNerd's guidance)
- Added comprehensive EVENT LOOP CONFIGURATION documentation section
- Updated SYNOPSIS with proper configuration guidance
- Improvements:
- Apply connect_timeout to Redis handshake (AUTH, SELECT, CLIENT SETNAME)
- Removed unused read_timeout and write_timeout settings
- Examples:
- Updated pagi-chat example for PAGI 0.001016 auto-configured Future::IO
- Testing:
- New t/93-socket-cleanup/close-with-watchers.t for disconnect scenarios
- Enhanced t/10-connection/socket-cleanup.t
0.001003 2026-01-18
- New Feature: Enhanced Lua Script Helper API
- define_command() for registering named scripts with metadata
- run_script() for executing registered scripts by name
- get_script(), list_scripts() for registry access
- preload_scripts() for pipeline optimization
- Automatic EVALSHA with EVAL fallback (NOSCRIPT handling)
- Pipeline integration via $pipe->run_script()
- Optional method installation (install => 1)
- Support for fixed or dynamic key counts
- Script.pm Enhancements:
- New run() method with explicit keys/args arrays
- New run_on() method for explicit connection targeting
- Metadata fields: name, num_keys, description
- Documentation:
- New LUA SCRIPTING section in Async::Redis POD
- Comprehensive examples for all script methods
- Pipeline integration examples
- Testing:
- t/60-scripting/define-command.t (18 tests)
- t/60-scripting/script-registry.t (17 tests)
- t/60-scripting/pipeline-scripts.t (16 tests)
- t/60-scripting/pagi-channels.t (16 tests) - realistic channel layer scenarios
0.001002 2026-01-17
- Bug Fix: Concurrent command response matching
- Fixed race condition where multiple async commands on a single
connection could receive mismatched responses
- Implemented Response Queue pattern with FIFO ordering
- Commands now register in inflight queue before sending
- Single reader coroutine processes responses in order
- New Features:
- Added inflight_count() method to check pending commands
- Added _wait_for_inflight_drain() for pipeline/PubSub synchronization
- Documentation:
- Added CONCURRENT COMMANDS section to POD
- Documented Response Queue pattern and best practices
- Testing:
- Added t/92-concurrency/response-ordering.t test suite
- Tests for concurrent SET, GET, mixed command types
- Stress test with 100 concurrent commands
- Inflight tracking verification
0.001001 2026-01-03
- Initial release
- Core Features:
- Full async/await support via Future::IO
- Event loop agnostic (IO::Async, AnyEvent, UV, etc.)
- RESP2 protocol support via Protocol::Redis
- All Redis commands via auto-generated methods
- Connection Features:
- TCP and TLS/SSL connections
- URI connection strings
- Automatic reconnection with exponential backoff
- Connection pooling with health checks
- Fork-safe for pre-fork servers
- Command Features:
- Pipelining for improved throughput
- Transactions (MULTI/EXEC/WATCH)
- Lua scripting with EVALSHA optimization
- SCAN iterators (SCAN, HSCAN, SSCAN, ZSCAN)
- Key prefixing
- PubSub:
- Channel and pattern subscriptions
- Sharded subscriptions (Redis 7+)
- Observability:
- OpenTelemetry tracing and metrics
- Debug logging
- Credential redaction
- Testing:
- Comprehensive test suite
- Integration tests
- Performance benchmarks