Skip to content

Harden Redis pooling, subscriber transport, and topology#453

Merged
binaryfire merged 16 commits into
0.4from
audit/redis-pooling-subscriber-lifecycles
Jul 26, 2026
Merged

Harden Redis pooling, subscriber transport, and topology#453
binaryfire merged 16 commits into
0.4from
audit/redis-pooling-subscriber-lifecycles

Conversation

@binaryfire

@binaryfire binaryfire commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

This completes the Redis lifecycle audit and fixes the parts of the package that were still built around unsafe retry, incomplete topology, or line-oriented subscriber assumptions.

The main change is ownership clarity. A Redis command is executed once. The checked-out connection is then either retained or discarded based on its actual protocol state; the framework no longer reconnects and replays a command whose server-side outcome may be unknown. Stateful operations continue to pin the exact wrapper to the current coroutine, and callback-form pipelines and transactions still return that connection as soon as the callback finishes.

Subscriber traffic now uses a dedicated coroutine-hooked stream with exact RESP2 decoding. It handles fragmented reads, complete writes, binary payloads, protocol errors, TLS, Unix sockets, IPv6, Sentinel master discovery, and Cluster master selection without CRLF packet framing or a timer coroutine per message.

The change also brings the supported phpredis surface up to current Laravel conventions where they fit Hypervel's pooled architecture, while preserving Hypervel's transforms, SafeScan helpers, raw held access, connection-local topology, and immediate callback release.

For more detailes, see: docs/plans/2026-07-25-1811-redis-pooling-subscriber-and-laravel-parity.md

Redis command and pool behavior

  • Remove framework-level command replay after RedisException.
  • Retain synchronized server-error connections and discard generations with unknown transport or protocol state.
  • Refresh Sentinel connections after READONLY and MASTERDOWN without reconnecting standalone endpoints that cannot change.
  • Normalize command routing case-insensitively while preserving exact macro and event names.
  • Keep MULTI, PIPELINE, WATCH, DISCARD, callback release, copied-context ownership, and terminal defer behavior exact.
  • Reject pooled RESET before native dispatch because it can terminate PHP in pipeline mode and otherwise clears authentication and database state owned by the pool.
  • Reject pooled sharded subscription instead of retaining a wrapper indefinitely without slot-aware routing.
  • Preserve callback failures when concurrency-lease release also fails.

Connection configuration and Laravel-facing APIs

  • Complete standalone and Cluster host, scheme, context, ACL, option, prefix, retry, backoff, TCP keepalive, client-name, and failover handling.
  • Preserve string zero credentials and connection-local option precedence.
  • Validate Sentinel nodes and Cluster seeds through the canonical Redis configuration parser.
  • Centralize Sentinel node fallback and current-master resolution.
  • Add Laravel-shaped Redis connection macros without requiring Redis availability during registration.
  • Add boot-only enableEvents() and disableEvents() controls that affect future pools without reading configuration on every command.
  • Remove the old named-proxy extend() API, which had Laravel's method name but incompatible semantics. Macros and held raw access remain the supported extension points.
  • Keep the split package manifest, facade annotations, and static test cleanup aligned with the production surface.

Subscriber transport and lifecycle

  • Replace Engine EOF packet framing with exact RESP2 parsing over one hooked PHP stream.
  • Decode simple strings, errors, integers, bulk strings, arrays, and nulls using declared lengths.
  • Treat malformed, overflowing, truncated, or unsupported frames as terminal protocol failures.
  • Retain the first receive-loop cause and propagate it to blocked or later foreground operations.
  • Bound command acknowledgements and pings while leaving idle subscription receive correctly unbounded.
  • Track channel and pattern subscriptions independently and wait for the exact acknowledgement count.
  • Support complete standalone, TLS, Unix, IPv4, IPv6, Sentinel, and Cluster subscriber endpoints and credentials.
  • Release Cluster discovery wrappers before dialing the dedicated subscriber.
  • Remove per-message timers and make close, timeout, and worker-shutdown interruption deterministic.

Linked packages

  • Reverb publishes through the independent pooled Redis connection instead of gating writes on subscriber state or retaining an unbounded outage queue.
  • Failed Reverb metrics requests remove their exact pending metric and listener without replacing the publish failure.
  • Horizon copies the complete named Hypervel Redis topology, hash-tags Cluster prefixes, and publishes the resolved prefix at both Redis configuration precedence levels.
  • Telescope formats Redis parameters without invoking user serialization or string conversion and ignores the actual pipeline and MULTI opener events.
  • Sentry records the normalized Redis database index on successful and failed commands.

Documentation and audit records

  • Update the Redis guide and package README for current options, topology, subscribers, macros, event controls, and intentional pooled differences.
  • Remove the automatic replay claim and document the supported alternatives to pooled RESET, sharded subscription, and connector-driver extension.
  • Close the Redis work unit in the audit ledger and route the remaining Cache limiter and named-Redis consumer checks to their owning audits.

Test reliability

The full merged gate exposed an existing Faker locale test that asserted one exact draw from process-global mt_rand(). Container resolution may yield in Hypervel's coroutine runtime, allowing another coroutine to advance that shared sequence. The test now verifies exact locale-provider ownership and localized output instead of depending on global RNG ordering. It does not depend on the host locale, country, or timezone.

Validation

  • composer fix
  • Focused Redis, subscriber, Reverb, Horizon, Telescope, Sentry, and Foundation regression suites
  • Live Redis and Valkey integration coverage
  • Redis split-package manifest validation
  • Package-checklist parity and stale-path scans
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Added Redis connection macros (macro/mixin/hasMacro/flushMacros) with boot-time command event enable/disable.
    • Expanded Redis subscriber support with RESP2 decoding and binary-safe Pub/Sub message fidelity.
    • Improved Sentinel master resolution and broader Horizon prefix hash-tag handling.
  • Bug Fixes

    • Prevented pooled command replay after failures; rejected unsupported pooled operations (e.g., reset, pooled ssubscribe).
    • Fixed exception precedence during concurrency-limiter lease release and tightened Redis subscriber/publish failure behavior.
  • Documentation

    • Updated Redis docs/config guidance, including TLS/context rules and pooled command constraints.
  • Tests

    • Added/updated extensive Redis, Horizon, Telescope, Sentry, and Reverb coverage for the new/changed behaviors.

Record the complete Redis pooling, subscriber transport, topology, and Laravel parity design before implementation.

Capture the verified phpredis and RESP boundaries, protected Hypervel capabilities, accepted intentional differences, implementation order, regression matrix, hot-path assessment, and anti-overengineering constraints that govern the work unit.
Stop replaying ambiguous Redis failures and classify whether each native generation can be safely retained or must be discarded.

Complete current supported phpredis configuration, ACL, context, option, prefix, Sentinel, and Cluster behavior while preserving exact pooled wrapper ownership and callback-immediate release.

Add Laravel-shaped macros and boot-only event controls, normalize command routing case-insensitively, reject pool-destructive RESET and sharded subscription operations, remove the incompatible named-proxy extension API, and keep facade and static test cleanup metadata aligned.
Exercise exact wrapper ownership, failure disposition without replay, mixed-case stateful routing, callback-immediate release, macro event granularity, event overrides, and facade metadata.

Cover standalone, Sentinel, and Cluster configuration; context and credential normalization; supported native options; prefix precedence; fresh topology resolution; terminal cleanup; and the separate-process RESET safety boundary against live Redis and Valkey behavior.
Replace CRLF-delimited Engine packet framing with one coroutine-hooked PHP stream and an exact RESP2 decoder.

Handle complete writes, partial reads, binary bulk payloads, nested arrays, nulls, server errors, malformed lengths, and terminal EOF without corrupting the protocol stream or spinning.

Support validated TCP, TLS, IPv4, IPv6, and Unix endpoints with real transport fixtures bound to ephemeral ports and exception-safe teardown.
Retain the first receive-loop failure, close every routing channel exactly once, bound foreground acknowledgements and pings, and prevent cleanup-induced errors from replacing the real cause.

Route RESP replies semantically, track channel and pattern subscriptions independently, remove timer-per-message delivery, preserve complete credentials and topology, and make close and worker-shutdown interruption deterministic.

Add unit and live-service coverage for binary Pub/Sub, acknowledgement accounting, receive failures, timeouts, channel races, credentials, and teardown.
Always release acquired concurrency leases while keeping the callback failure primary when release also fails.

Apply the same direct control flow to both public callback paths without adding a shared abstraction, and distinguish acquisition timeouts from user callbacks that happen to throw the same exception type.

Cover the complete callback and release success/failure matrix at both call sites.
Send scaling messages directly through the pooled Redis publisher instead of gating them on the unrelated subscriber connection and retaining an unbounded outage queue.

Propagate encoding and Redis failures truthfully, return the actual publish count, and remove the exact pending metric and listener when a metrics request cannot be published while preserving the original failure.

Cover disconnected subscribers, Redis outages, response races, publish counts, and cleanup failure precedence.
Copy the configured Hypervel Redis connection instead of reading Laravel's incompatible top-level cluster shape.

Hash-tag clustered Horizon prefixes and publish the resolved prefix at both configuration precedence levels so RedisConfig cannot restore a stale source prefix.

Verify standalone and Cluster topology, pool settings, credentials, prefix precedence, fallback behavior, and missing connections through the real normalized consumer path.
Format Redis parameters recursively using scalar values and debug types so observation cannot invoke JsonSerializable, string conversion, or other user code after a successful command.

Ignore the real pipeline and MULTI opener events strictly and case-insensitively instead of filtering a dead transaction name.

Cover nested parameters, objects, closures, resources, throwing serializers, mixed-case batch openers, and boot-time event disabling.
Read Redis span metadata from the normalized database key used by real Hypervel connections instead of the test-only db key.

Correct the masking fixture and verify that both successful and failed commands retain a non-zero configured database index.
Document the current phpredis options, connection-local standalone, Sentinel, and Cluster topology, exact subscriber transport, macros, and boot-only event controls.

Remove the unsafe automatic replay claim and explain the deliberate pooled RESET, sharded subscription, and connector-extension differences with the supported alternatives.

Keep the guide task-focused while preserving Hypervel-specific transforms, SafeScan, raw held access, and native retry and backoff behavior.
Mark Redis complete and record the verified command, subscriber, topology, consumer, metadata, performance, and retained-state findings with their final owning boundaries.

Close the carried Redis and pool revalidation items, route the remaining Cache limiter and named-Redis consumer checks to their active or later audits, and make Cache the next work unit.

Record the complete validation and review evidence without claiming that pending consumer audits are already complete.
Verify Faker locale selection through exact provider ownership and localized output rather than seeded process-global random draws.

Global Mt19937 state is shared across coroutines, so container resolution can yield while another task advances the sequence and makes an exact state-name assertion flaky.

Keep the public locale behavior covered without adding RNG isolation, scheduler controls, or any framework source change.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a835b7ce-0d3b-4696-84fd-3fdf2186f080

📥 Commits

Reviewing files that changed from the base of the PR and between ac665c5 and 3bdceb9.

📒 Files selected for processing (1)
  • tests/Redis/RedisPoolTest.php

📝 Walkthrough

Walkthrough

Redis pooling, Sentinel and cluster topology, RESP2 subscriber transport, macro/event handling, Reverb publishing, limiter failure precedence, observability, Horizon prefix handling, documentation, and integration tests were updated. Unsupported pooled commands and connector extension APIs were removed, while configuration validation and lifecycle cleanup were expanded.

Changes

Redis lifecycle and parity

Layer / File(s) Summary
Audit routing and implementation plan
docs/plans/*
Audit routing, Redis findings, implementation sequencing, validation requirements, and completion criteria were updated.
Configuration and topology
src/redis/src/RedisConfig.php, src/redis/src/PhpRedis*, src/redis/src/RedisSentinelFactory.php, src/redis/src/RedisServiceProvider.php
Host, scheme, TLS context, credentials, options, Sentinel resolution, topology validation, and dependency wiring were revised.
Pooled command and proxy behavior
src/redis/src/RedisConnection.php, src/redis/src/RedisManager.php, src/redis/src/RedisProxy.php, src/support/src/Facades/Redis.php
Command replay was removed, macros and boot-only event controls were added, command names are normalized, and pooled reset, subscription, and extension APIs are restricted or removed.
Subscriber transport and routing
src/redis/src/Subscriber/*
Native stream sockets, RESP2 decoding, subscription tracking, acknowledgement routing, timeout handling, credential forwarding, and terminal failure propagation were implemented.
Cross-package integrations
src/reverb/src/*, src/sentry/src/*, src/telescope/src/*, src/horizon/src/*
Reverb publishes directly, metric cleanup preserves primary failures, Redis event enablement uses the manager, observability metadata is corrected, and Horizon prefixes support cluster hash tags.
Documentation and validation
src/boost/docs/redis.md, src/redis/README.md, tests/Redis/*, tests/Integration/*, tests/Horizon/*
Redis behavior, configuration, unsupported operations, transport fixtures, topology cases, lifecycle cleanup, metadata, and integration coverage were updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: Redis pooling, subscriber transport, and topology hardening.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/redis-pooling-subscriber-lifecycles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

The PR hardens Redis connection ownership, topology handling, subscriber transport, and Laravel-facing compatibility.

  • Removes ambiguous command replay and tightens pooled connection retention, pinning, and release behavior.
  • Replaces line-oriented subscriber framing with binary-safe RESP2 stream decoding across standalone, Sentinel, and Cluster topologies.
  • Expands Redis configuration, macros, event controls, and integrations with Horizon, Reverb, Telescope, and Sentry.
  • Updates documentation, audit records, and focused regression and integration coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/redis/src/PhpRedisConnection.php Expands standalone and Sentinel connection configuration while preserving verified phpredis context-driven TLS behavior.
src/redis/src/PhpRedisClusterConnection.php Completes Cluster seed, context, option, prefix, and failover handling.
src/redis/src/RedisProxy.php Tightens pooled command ownership and constructs dedicated topology-aware subscribers.
src/redis/src/Subscriber/Connection.php Implements exact RESP2 stream transport with binary-safe reads, complete writes, endpoint normalization, and deterministic failure handling.
src/redis/src/Subscriber/Subscriber.php Coordinates subscription acknowledgements, receive-loop failures, timeouts, and channel-pattern lifecycle state.
src/redis/src/RedisConfig.php Centralizes canonical Redis configuration parsing and topology-specific option normalization.
src/redis/src/RedisManager.php Adds Laravel-shaped macros and boot-time event controls while retaining named pooled connection management.
src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php Decouples publishing from subscriber state and removes outage-queue retention.
src/horizon/src/Horizon.php Copies complete named Redis topology and normalizes Cluster-safe Horizon prefixes.
src/boost/docs/redis.md Documents the expanded Redis configuration, topology, subscriber, macro, event, and pooled-operation behavior.

Reviews (3): Last reviewed commit: "test(redis): cover pool event configurat..." | Re-trigger Greptile

Comment thread src/boost/docs/redis.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (7)
tests/Redis/Subscriber/CommandInvokerTest.php (1)

544-642: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Namespace the generic Connection test doubles.

BlockingConnection and ControlledConnection are collision-prone generic names declared in the shared Hypervel\Tests\Redis\Subscriber namespace. As per coding guidelines, "Use test-specific namespaces for collision-prone generic helper classes, with the test class name as the final namespace segment" — e.g. Hypervel\Tests\Redis\Subscriber\CommandInvokerTest\ControlledConnection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Redis/Subscriber/CommandInvokerTest.php` around lines 544 - 642, Move
the test doubles BlockingConnection and ControlledConnection into the
test-specific namespace Hypervel\Tests\Redis\Subscriber\CommandInvokerTest,
updating any references within CommandInvokerTest accordingly. Keep their
implementations unchanged and use the test class name as the final namespace
segment.

Source: Coding guidelines

tests/Redis/Subscriber/SubscriberTest.php (1)

337-357: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated readExact() test helper across two subscriber test suites. Both files carry a byte-identical private helper (same docblock, same RuntimeException message); the shared root cause is that no common home exists for RespServer-adjacent read helpers.

  • tests/Redis/Subscriber/SubscriberTest.php#L337-L357: remove the local helper and use the shared implementation.
  • tests/Redis/Subscriber/CommandInvokerTest.php#L505-L525: remove the local helper and use the shared implementation.

Suggested home: a readExact() method on the RespServer fixture in tests/Redis/Fixtures/RespServer.php, or a small trait beside it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Redis/Subscriber/SubscriberTest.php` around lines 337 - 357, The
readExact() test helper is duplicated across both subscriber test suites. Add
one shared readExact() implementation to the RespServer fixture or an adjacent
trait, update tests/Redis/Fixtures/RespServer.php (shared home) accordingly,
remove the local helper from tests/Redis/Subscriber/SubscriberTest.php lines
337-357, and remove the local helper from
tests/Redis/Subscriber/CommandInvokerTest.php lines 505-525; update both callers
to use the shared implementation.
src/redis/src/Subscriber/CommandInvoker.php (1)

319-343: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the 30-second push timeout into a constant.

30.0 and the "remained full for 30 seconds" text can drift apart independently.

♻️ Proposed refactor
+    private const float MESSAGE_PUSH_TIMEOUT = 30.0;
+
     private function pushMessage(Message $message): void
     {
-        if ($this->messageChannel->push($message, 30.0)) {
+        if ($this->messageChannel->push($message, self::MESSAGE_PUSH_TIMEOUT)) {
             return;
         }
 
         if (! $this->messageChannel->isTimeout()) {
             throw new SocketException('The Redis subscriber message channel was closed.');
         }
 
         $exception = new SocketException(sprintf(
-            "Redis subscriber message channel [{$message->channel}] remained full for 30 seconds."
+            'Redis subscriber message channel [%s] remained full for %s seconds.',
+            $message->channel,
+            self::MESSAGE_PUSH_TIMEOUT,
         ));

Note the proposed private const float typed constant requires PHP 8.3+.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/redis/src/Subscriber/CommandInvoker.php` around lines 319 - 343, Update
pushMessage to define and reuse a private timeout constant for the 30-second
channel push duration, replacing the literal 30.0 and deriving the “remained
full for 30 seconds” message from that same constant so the configured duration
and text cannot diverge.
src/redis/src/RedisConfig.php (1)

88-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that the override only affects pools created afterwards.

RedisPool snapshots connectionConfig() in its constructor (src/redis/src/Pool/RedisPool.php lines 30-34), so calling enableEvents()/disableEvents() after a pool exists leaves already-created pools/connections on the old event.enable value. The "boot-only" note captures the intent, but making the pool-snapshot consequence explicit in the docblock would prevent misuse. This is worker-lifetime singleton state under Swoole, so the flag persists for the life of the worker.

As per coding guidelines: "Review and report the Swoole persistence impact whenever changing static properties, caches, singleton-held state, manager registries, or other worker-lifetime state."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/redis/src/RedisConfig.php` around lines 88 - 113, Update the docblocks
for enableEvents() and disableEvents() to state that the override applies only
to pools created after the method is called; existing RedisPool instances retain
their snapshotted connection configuration for the worker lifetime.

Source: Coding guidelines

tests/Redis/PhpRedisClusterConnectionTest.php (1)

229-241: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Validate the endpoint before starting the server.

If the guard at Line 239 throws, $server->wait() in the finally at Line 259 is never reached and the started server is leaked. Parse and validate the endpoint before start().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Redis/PhpRedisClusterConnectionTest.php` around lines 229 - 241, Move
endpoint parsing and the host/port validation before the `$server->start()` call
in the test setup. Keep the existing `InvalidArgumentException` guard and
subsequent server startup behavior unchanged, ensuring validation failures occur
before the server can be started and leaked.
src/redis/src/Limiters/ConcurrencyLimiter.php (1)

105-111: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Suppressed cleanup/release exceptions aren't logged, unlike closeSubscriber()'s pattern.

All three sites deliberately swallow a secondary exception (lease release / listener cleanup) to preserve the primary exception's precedence — the precedence logic itself is correct and well-tested. However, none of them log the swallowed exception, whereas RedisPubSubProvider::closeSubscriber() (same PR) establishes the pattern of logging suppressed cleanup failures before letting the primary error propagate. Without a log line, an operator debugging a stuck concurrency slot or a leaked metrics listener has no trace that the release/cleanup itself also failed.

  • src/redis/src/Limiters/ConcurrencyLimiter.php#L105-L111: log the caught $exception (e.g. via Log::error) before discarding it when $callbackException !== null.
  • src/redis/src/Limiters/ConcurrencyLimiterBuilder.php#L125-L131: apply the same logging when the release exception is suppressed in favor of $callbackException.
  • src/reverb/src/Protocols/Pusher/MetricsHandler.php#L153-L161: log the stopListening() cleanup failure in the inner catch (Throwable) block instead of only a comment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/redis/src/Limiters/ConcurrencyLimiter.php` around lines 105 - 111, Log
suppressed cleanup exceptions before discarding them, while preserving primary
exception precedence: in src/redis/src/Limiters/ConcurrencyLimiter.php lines
105-111 and src/redis/src/Limiters/ConcurrencyLimiterBuilder.php lines 125-131,
log the caught release exception when $callbackException is present; in
src/reverb/src/Protocols/Pusher/MetricsHandler.php lines 153-161, log the inner
stopListening() failure instead of only commenting on it.
src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php (1)

42-42: 🩺 Stability & Availability | 🔵 Trivial

Removing queuedPublishes improves worker-lifetime memory safety.

publish() now always goes straight through the pooled RedisProxy, independent of subscriber connection state, so no buffering is needed. This also removes a singleton-held, unbounded-growth-prone buffer (queuedPublishes) that could accumulate messages for the lifetime of the worker during prolonged disconnects — a net improvement worth calling out given this touches worker-lifetime state.

As per coding guidelines: "Review and report the Swoole persistence impact whenever changing static properties, caches, singleton-held state, manager registries, or other worker-lifetime state."

Also applies to: 175-178, 208-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php` at line 42,
Remove the singleton-held queuedPublishes buffer and all related queueing/flush
logic from RedisPubSubProvider, including its declaration and references near
publish(). Ensure publish() always sends directly through the pooled RedisProxy
regardless of subscriber connection state, and document the Swoole
worker-lifetime memory-safety impact of removing this unbounded state.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/plans/2026-07-25-1811-redis-pooling-subscriber-and-laravel-parity.md`:
- Around line 1573-1584: Update the Redis dependency changes described under
“Split metadata” to also add hypervel/macroable to the root composer.json using
composer require, while retaining the direct dependency in
src/redis/composer.json. Verify both manifests remain aligned and pass metadata
validation.
- Line 1253: Update the Horizon prefix assignment using an explicit
null/empty-value check instead of the truthiness-based `?:` operator, so a
configured prefix of `"0"` is preserved while null or empty configuration still
follows the intended default policy.
- Around line 1161-1191: Clarify and test the worker-lifetime behavior of
RedisConfig::enableEvents() and disableEvents(): after either call, every
subsequently assembled connection config, including configs for existing and
later pools managed by RedisManager, must reflect the override through
connectionConfig(). Document this shared-container call-boundary behavior in the
method docblocks and add coverage for both enable and disable paths without
adding a reset API.
- Around line 513-522: Update Subscriber\Connection’s stream-context
normalization to preserve an existing $context['stream'] shape without wrapping
it again under 'ssl'. Branch between already-normalized stream contexts and raw
flat or $context['ssl'] options, adding the SSL wrapper only for the latter;
cover both forms at the subscriber stream boundary.

In `@src/redis/README.md`:
- Line 8: Update the “Differences From Laravel” heading in the Redis README to
use the configured setext heading style required by MD003, matching the
surrounding documentation conventions.

In `@src/redis/src/RedisProxy.php`:
- Around line 578-585: Update RedisProxy::subscriber() at the createSubscriber
call to pass the cluster’s configured scheme instead of null, using the scheme
from the cluster configuration (including tls:// seeds or scheme => 'tls') while
leaving the pooled connection scheme unchanged.

In `@src/testing/src/PHPUnit/AfterEachTestSubscriber.php`:
- Line 246: Update src/testing/composer.json to declare hypervel/redis as a
required dependency, matching the RedisConnection::flushMacros() reference in
AfterEachTestSubscriber. Use the project’s existing dependency version
conventions and ensure consumers installing hypervel/testing receive the Redis
package automatically.

In `@tests/Redis/RedisConnectionTest.php`:
- Around line 834-900: Ensure every macro registered by the RedisConnection
macro tests is removed after each test, rather than relying on test ordering.
Add teardown cleanup or register an AfterEachTestSubscriber hook to call
RedisConnection::flushMacros(), covering registrations from
testMixinRegistersConnectionMacros,
testMacroLookupPreservesExactNamesAndMayShadowNativeCommands, and
testRedisExceptionInsideMacroUsesConnectionDispositionRule.

In `@tests/Redis/RedisProxyTest.php`:
- Around line 65-86: Move macro cleanup out of
testMacroRegistrationMethodsDoNotCheckoutRedis and into tearDown or the test
suite’s centralized after-each cleanup so it runs even when assertions fail.
Ensure RedisProxy/RedisConnection static macros such as greeting and farewell
are flushed after every test, and remove the inline flushMacros call from the
test.

---

Nitpick comments:
In `@src/redis/src/Limiters/ConcurrencyLimiter.php`:
- Around line 105-111: Log suppressed cleanup exceptions before discarding them,
while preserving primary exception precedence: in
src/redis/src/Limiters/ConcurrencyLimiter.php lines 105-111 and
src/redis/src/Limiters/ConcurrencyLimiterBuilder.php lines 125-131, log the
caught release exception when $callbackException is present; in
src/reverb/src/Protocols/Pusher/MetricsHandler.php lines 153-161, log the inner
stopListening() failure instead of only commenting on it.

In `@src/redis/src/RedisConfig.php`:
- Around line 88-113: Update the docblocks for enableEvents() and
disableEvents() to state that the override applies only to pools created after
the method is called; existing RedisPool instances retain their snapshotted
connection configuration for the worker lifetime.

In `@src/redis/src/Subscriber/CommandInvoker.php`:
- Around line 319-343: Update pushMessage to define and reuse a private timeout
constant for the 30-second channel push duration, replacing the literal 30.0 and
deriving the “remained full for 30 seconds” message from that same constant so
the configured duration and text cannot diverge.

In `@src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php`:
- Line 42: Remove the singleton-held queuedPublishes buffer and all related
queueing/flush logic from RedisPubSubProvider, including its declaration and
references near publish(). Ensure publish() always sends directly through the
pooled RedisProxy regardless of subscriber connection state, and document the
Swoole worker-lifetime memory-safety impact of removing this unbounded state.

In `@tests/Redis/PhpRedisClusterConnectionTest.php`:
- Around line 229-241: Move endpoint parsing and the host/port validation before
the `$server->start()` call in the test setup. Keep the existing
`InvalidArgumentException` guard and subsequent server startup behavior
unchanged, ensuring validation failures occur before the server can be started
and leaked.

In `@tests/Redis/Subscriber/CommandInvokerTest.php`:
- Around line 544-642: Move the test doubles BlockingConnection and
ControlledConnection into the test-specific namespace
Hypervel\Tests\Redis\Subscriber\CommandInvokerTest, updating any references
within CommandInvokerTest accordingly. Keep their implementations unchanged and
use the test class name as the final namespace segment.

In `@tests/Redis/Subscriber/SubscriberTest.php`:
- Around line 337-357: The readExact() test helper is duplicated across both
subscriber test suites. Add one shared readExact() implementation to the
RespServer fixture or an adjacent trait, update
tests/Redis/Fixtures/RespServer.php (shared home) accordingly, remove the local
helper from tests/Redis/Subscriber/SubscriberTest.php lines 337-357, and remove
the local helper from tests/Redis/Subscriber/CommandInvokerTest.php lines
505-525; update both callers to use the shared implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3968944-5414-43e1-83ca-b30e9597c6bf

📥 Commits

Reviewing files that changed from the base of the PR and between 1e0ea64 and f428321.

📒 Files selected for processing (60)
  • docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
  • docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
  • docs/plans/2026-07-25-1811-redis-pooling-subscriber-and-laravel-parity.md
  • src/boost/docs/redis.md
  • src/horizon/src/Horizon.php
  • src/redis/README.md
  • src/redis/composer.json
  • src/redis/src/Limiters/ConcurrencyLimiter.php
  • src/redis/src/Limiters/ConcurrencyLimiterBuilder.php
  • src/redis/src/PhpRedisClusterConnection.php
  • src/redis/src/PhpRedisConnection.php
  • src/redis/src/RedisConfig.php
  • src/redis/src/RedisConnection.php
  • src/redis/src/RedisManager.php
  • src/redis/src/RedisProxy.php
  • src/redis/src/RedisSentinelFactory.php
  • src/redis/src/RedisServiceProvider.php
  • src/redis/src/Subscriber/CommandInvoker.php
  • src/redis/src/Subscriber/Connection.php
  • src/redis/src/Subscriber/Constants.php
  • src/redis/src/Subscriber/Exceptions/ServerException.php
  • src/redis/src/Subscriber/Subscriber.php
  • src/reverb/src/Protocols/Pusher/MetricsHandler.php
  • src/reverb/src/Servers/Hypervel/Scaling/RedisPubSubProvider.php
  • src/sentry/src/Features/RedisFeature.php
  • src/support/src/Facades/Redis.php
  • src/telescope/src/Watchers/RedisWatcher.php
  • src/testing/src/PHPUnit/AfterEachTestSubscriber.php
  • tests/Integration/Foundation/FoundationHelpersTest.php
  • tests/Integration/Horizon/Feature/RedisPrefixTest.php
  • tests/Integration/Redis/RedisConnectionIntegrationTest.php
  • tests/Integration/Redis/RedisConnectorTest.php
  • tests/Integration/Redis/RedisResetIntegrationTest.php
  • tests/Integration/Redis/Subscriber/SubscriberIntegrationTest.php
  • tests/Redis/ConcurrencyLimiterBuilderTest.php
  • tests/Redis/ConcurrencyLimiterTest.php
  • tests/Redis/Fixtures/PhpRedisClusterConnectionStub.php
  • tests/Redis/Fixtures/PhpRedisConnectionStub.php
  • tests/Redis/Fixtures/RespServer.php
  • tests/Redis/Fixtures/Tls/server.crt
  • tests/Redis/Fixtures/Tls/server.key
  • tests/Redis/MultiExecTest.php
  • tests/Redis/PackageMetadataTest.php
  • tests/Redis/PhpRedisClusterConnectionTest.php
  • tests/Redis/RedisConfigTest.php
  • tests/Redis/RedisConnectionTest.php
  • tests/Redis/RedisEventsTest.php
  • tests/Redis/RedisManagerTest.php
  • tests/Redis/RedisPoolHeartbeatTest.php
  • tests/Redis/RedisProxyNonCoroutineTest.php
  • tests/Redis/RedisProxyTest.php
  • tests/Redis/RedisSentinelFactoryTest.php
  • tests/Redis/Subscriber/CommandInvokerTest.php
  • tests/Redis/Subscriber/ConnectionTest.php
  • tests/Redis/Subscriber/SubscriberTest.php
  • tests/Reverb/Protocols/Pusher/MetricsHandlerTest.php
  • tests/Reverb/Servers/Hypervel/Scaling/RedisPubSubProviderTest.php
  • tests/Sentry/Features/RedisIntegrationTest.php
  • tests/Telescope/Watchers/DisabledWatcherTest.php
  • tests/Telescope/Watchers/RedisWatcherTest.php
💤 Files with no reviewable changes (2)
  • src/redis/src/Subscriber/Constants.php
  • tests/Redis/Fixtures/PhpRedisClusterConnectionStub.php

Comment thread docs/plans/2026-07-25-1811-redis-pooling-subscriber-and-laravel-parity.md Outdated
Comment thread src/redis/README.md
Comment thread src/redis/src/RedisProxy.php
Comment thread src/testing/src/PHPUnit/AfterEachTestSubscriber.php
Comment thread tests/Redis/RedisConnectionTest.php
Comment thread tests/Redis/RedisProxyTest.php
Make Horizon's shipped configuration the sole owner of its application-scoped Redis prefix default. The runtime consumer now uses the typed config getter without a second fallback, while regression coverage proves both missing and blank environment values and verifies Cluster-safe publication at every effective prefix location.

Consolidate exact RESP test reads in the shared server fixture, resolve Cluster test endpoints before starting handler coroutines, and derive the subscriber message-channel timeout and diagnostics from one typed constant. Update the Redis implementation plan and lifecycle ledger to record the final boundaries.
@binaryfire

Copy link
Copy Markdown
Collaborator Author

@greptile-apps @coderabbitai I went through every inline comment, main-thread finding, and nitpick against the current source, the phpredis implementation, and the regression suite.

Follow-up commit ac665c5f6 makes the bounded changes that held up:

  • Horizon config is now the single owner of the application-scoped prefix default. Missing and blank environment values are covered, and Horizon::use() reads typed config without a second fallback. Cluster publication is asserted at all three effective prefix locations.
  • The duplicate exact-read helpers now live on RespServer, including the third same-family copy.
  • Cluster test endpoint resolution happens before the handler coroutine starts.
  • The subscriber message-channel timeout and both diagnostics derive from one typed constant.

The transport findings do not require changes. phpredis itself selects SSL for a bare host with a non-empty context; this was verified in its C source and with a real TLS ClientHello. The subscriber context conversion accepts the three documented phpredis shapes and wraps the selected SSL options exactly once for PHP streams. Cluster-discovered nodes inherit cluster.context, not a seed scheme, so forwarding a seed/config scheme would diverge from the pooled client.

The event override already documents and tests its boot-only, future-pool boundary. Existing pools intentionally keep their snapshotted configuration. The root and Testing dependency suggestions do not match the monorepo or framework cleanup dependency model. Redis macro cleanup is already failure-safe in the centralized after-each subscriber; per-class teardown would duplicate that owner. The remaining heading, helper-namespace, suppressed-cleanup logging, blanket docstring, and source-comment suggestions either conflict with repository conventions or add ceremony without a supported failure. Reverb queue removal and its worker-memory benefit are already recorded in the plan and ledger.

The authoritative composer fix gate is green on the final commit: formatting, both static-analysis configurations, the complete parallel suite, and both Testbench gates all pass.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- follow-up commit ---'
git show --stat --oneline ac665c5f6
git show --format=fuller --name-status --find-renames ac665c5f6 -- \
  src/horizon \
  tests/Redis/Fixtures/RespServer.php \
  tests/Redis \
  src/redis \
  | sed -n '1,260p'

echo '--- candidate files ---'
fd -a -t f 'Horizon.php|RespServer.php|RedisProxyTest.php|CommandInvoker.php|Constants.php|RedisConfig.php|Connection.php|PhpRedisClusterConnection.php' . \
  | sort

echo '--- structural outlines ---'
for file in \
  src/horizon/src/Horizon.php \
  tests/Redis/Fixtures/RespServer.php \
  src/redis/src/Subscriber/CommandInvoker.php \
  src/redis/src/RedisConfig.php \
  src/redis/src/RedisProxy.php \
  src/redis/src/Subscriber/Connection.php \
  src/redis/src/PhpRedisClusterConnection.php
do
  if [ -f "$file" ]; then
    echo "### $file"
    ast-grep outline "$file" --items all 2>/dev/null | sed -n '1,220p'
  fi
done

echo '--- relevant symbols and assertions ---'
rg -n -C 5 -P \
  'Horizon::use|function use\s*\(|prefix|readExact|MESSAGE_CHANNEL|messageChannel|endpoint\(|createSubscriber|cluster\.context|eventsOverride|enableEvents|disableEvents|normalizeContext' \
  src/horizon/src/Horizon.php \
  tests/Integration/Horizon/Feature/RedisPrefixTest.php \
  tests/Redis/Fixtures/RespServer.php \
  tests/Redis/RedisProxyTest.php \
  tests/Redis/Subscriber/CommandInvokerTest.php \
  src/redis/src/Subscriber/CommandInvoker.php \
  src/redis/src/Subscriber/Constants.php \
  src/redis/src/RedisProxy.php \
  src/redis/src/RedisConfig.php \
  src/redis/src/Subscriber/Connection.php \
  src/redis/src/PhpRedisClusterConnection.php \
  2>/dev/null | sed -n '1,900p'

Length of output: 55449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- exact follow-up diff ---'
git diff ac665c5f6^ ac665c5f6 -- \
  src/horizon/config/horizon.php \
  src/horizon/src/Horizon.php \
  src/redis/src/Subscriber/CommandInvoker.php \
  tests/Horizon/HorizonConfigTest.php \
  tests/Integration/Horizon/Feature/RedisPrefixTest.php \
  tests/Redis/Fixtures/RespServer.php \
  tests/Redis/PhpRedisClusterConnectionTest.php \
  tests/Redis/RedisProxyTest.php \
  tests/Redis/Subscriber/CommandInvokerTest.php \
  tests/Redis/Subscriber/SubscriberTest.php \
  | sed -n '1,1000p'

echo '--- shipped Horizon configuration and config test ---'
sed -n '1,180p' src/horizon/config/horizon.php
sed -n '1,240p' tests/Horizon/HorizonConfigTest.php

echo '--- timeout diagnostic implementation ---'
sed -n '315,355p' src/redis/src/Subscriber/CommandInvoker.php

echo '--- all exact-read helper definitions and fixture use sites in Redis tests ---'
rg -n -P 'function\s+readExact\s*\(|::readExact\s*\(' tests/Redis -g '*.php'

echo '--- endpoint ordering in follow-up test contexts ---'
sed -n '1,260p' tests/Redis/PhpRedisClusterConnectionTest.php
sed -n '1060,1140p' tests/Redis/RedisProxyTest.php

echo '--- current authored CodeRabbit review comments on this PR ---'
gh api repos/hypervel/components/pulls/453/comments \
  --paginate \
  --jq '.[] | select(.user.login == "coderabbitai") | {id, path, line, body}' \
  | sed -n '1,1200p'

Length of output: 37457


@binaryfire Verified against ac665c5f6 and the current PR source.

The follow-up correctly:

  • makes the shipped Horizon config the owner of the non-empty application-scoped prefix default, with missing/blank environment coverage;
  • removes Horizon’s second runtime fallback and verifies all three effective cluster prefix locations;
  • centralizes the Redis test exact-read helper in RespServer;
  • resolves the Cluster test endpoint before its handler starts;
  • derives the subscriber bounded-push wait and both timeout diagnostics from MESSAGE_PUSH_TIMEOUT.

I also confirmed the documented event override boundary, and that Cluster-discovered subscriber endpoints receive cluster.context without inheriting a seed scheme. The current CodeRabbit inline-comment query returned no authored comments to resolve. No remaining action is needed from my side.

🐇 ✅


✏️ Learnings added
Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/453

Timestamp: 2026-07-26T01:31:23.872Z
Learning: In the Hypervel Redis package, `RedisConfig::enableEvents()` and `RedisConfig::disableEvents()` are intentionally boot-only worker-wide overrides: they affect subsequently assembled Redis connection configurations, while already-created pools retain their snapshotted event configuration.

Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/453

Timestamp: 2026-07-26T01:31:23.872Z
Learning: In `src/redis/src/RedisProxy.php`, subscribers created for Redis Cluster-discovered master nodes must use `cluster.context` and no inherited seed/config scheme, matching the pooled PhpRedis Cluster client's discovered-node transport behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

The follow-up commit addresses everything cleanly. Summary of what landed:

Horizon prefix ownershiphorizon.php now applies the null || '' guard in the config file itself, so the default is set before any consumer reads it. Horizon::use() reads config()->string('horizon.prefix') without a second fallback — single owner, no drift. HorizonConfigTest covers both missing and blank env values and properly restores global env state.

CommandInvoker constant consolidationMESSAGE_PUSH_TIMEOUT = 30.0 is now the sole source of truth for the push timeout, the channel-full exception message, and the logger diagnostic. The three were previously inline literals.

Cluster test endpoint resolution — moved before the handler coroutine starts, as flagged.

RespServer deduplication — exact-read helpers consolidated into the shared fixture, eliminating the third copy.

The rationale for the unchanged transport findings holds up: phpredis selecting SSL from a non-empty context (not scheme) is documented behavior confirmed in the C source, the three-shape context conversion matches the documented phpredis API surface, and Cluster nodes inheriting cluster.context rather than seed scheme is the correct model for the pooled client. The event override, monorepo dependency model, and centralized macro teardown explanations are all consistent with the codebase's architecture.

Everything that was actionable is resolved. The PR is clear to merge.

Exercise the boot-only Redis event override against a live RedisPool so the documented non-retrofit boundary is enforced directly.

Build the pool from a real configuration repository, change the worker-wide override, and prove the existing pool retains its captured setting while subsequent configuration assembly receives the new value.
@binaryfire
binaryfire merged commit c489fac into 0.4 Jul 26, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant