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
