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