Skip to content

feat(redis): add pool heartbeat and lifetime recycling#401

Merged
binaryfire merged 9 commits into
0.4from
feat/redis-pool-heartbeat-lifetime
Jun 24, 2026
Merged

feat(redis): add pool heartbeat and lifetime recycling#401
binaryfire merged 9 commits into
0.4from
feat/redis-pool-heartbeat-lifetime

Conversation

@binaryfire

@binaryfire binaryfire commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add optional Redis pool heartbeat sweeps for idle pooled connections
  • add Redis pooled connection generation tracking and max-lifetime recycling
  • validate held Redis pool wrappers before withConnection and withPinnedConnection callbacks run
  • keep borrowed Redis connections safe from mid-use recycling
  • fix Redis release reset failures so invalid wrappers return to the pool instead of leaking capacity
  • document the Redis pool heartbeat, timeout, idle, and lifetime options
  • remove the completed Boost todo item

This PR also includes the prior local keepalive fix commit because it was already on the local 0.4 branch and is part of the same pool heartbeat line of work.

Notes

  • Redis heartbeat and max-lifetime recycling remain disabled by default.
  • The new env vars are optional config lookups only; they are not added to committed .env.example files.
  • The Redis implementation mirrors the database pool where the behavior should match, while keeping Redis-specific details local.
  • Active borrowed connections are not recycled. Recycling happens on idle heartbeat sweeps or before reuse when a wrapper is borrowed again.

Tests

  • ./vendor/bin/phpunit --no-progress tests/Redis/RedisPoolHeartbeatTest.php
  • ./vendor/bin/phpunit --no-progress tests/Integration/Redis/RedisPoolHeartbeatIntegrationTest.php
  • ./vendor/bin/phpunit --no-progress tests/Redis/RedisProxyTest.php
  • ./vendor/bin/phpunit --no-progress tests/Redis/RedisConnectionTest.php
  • ./vendor/bin/phpunit --no-progress tests/Redis/RedisPoolTest.php
  • ./vendor/bin/phpunit --no-progress tests/Redis/PoolFactoryTest.php
  • composer fix

Summary by CodeRabbit

  • Documentation

    • Updated Redis configuration documentation with new connection pool lifecycle options.
  • New Features

    • Added configurable heartbeat support for Redis connection pools with heartbeat, heartbeat_timeout, and max_lifetime environment variables.
    • Implemented automatic background health checks to maintain active connections and evict expired idle connections.

Respect PoolOption heartbeat values on KeepaliveConnection by only registering a timer when the configured heartbeat is positive.

Pass the raw heartbeat float to Timer::tick instead of truncating it through an integer helper, which avoids turning sub-second heartbeat intervals into near-busy-loop timers.

Document that KeepaliveConnection max_idle_time eviction is driven by the heartbeat timer, clean up the stale inherited idle-close comment, and remove the completed Boost todo entry.

Add regression coverage for disabled heartbeat creating no timer, for enabled heartbeat still closing idle connections, and update the existing heartbeat test to opt into a positive interval.

Verification: ./vendor/bin/phpunit tests/Pool/HeartbeatConnectionTest.php; composer fix.
Add Redis connection generation timestamps and reuse-state tracking so pooled wrappers can distinguish active borrows from idle reusable connections.

Centralize reconnect bookkeeping through markReconnected and make release failures return an invalid wrapper to the pool, preventing pool-slot leaks while forcing a clean reconnect on the next borrow.
Start an optional Redis pool heartbeat timer when configured and sweep only idle connections in the pool channel.

Recycle expired idle generations, discard failed heartbeat checks, guard against late heartbeat completions after flushAll, and keep active borrowed connections untouched.
Validate pooled Redis wrappers before withConnection and withPinnedConnection callbacks run so expired or invalid idle generations reconnect before user code receives them.

Keep Redis connection-only lifecycle helpers out of the proxy and facade command surfaces.
Expose optional Redis pool heartbeat_timeout and max_lifetime settings in the source database config while keeping heartbeat and lifetime recycling disabled by default.

Update the Redis docs to describe checkout recycling, optional background heartbeat sweeps, heartbeat timeouts, idle recycling, and generation lifetime behavior.
Add Redis pool heartbeat coverage for disabled timers, idle and lifetime recycling, active-borrow safety, release reset failures, timeout cancellation, flush generation guards, proxy validation, and cluster master pings.

Add a real Redis integration check for heartbeat pings and update existing Redis pool/proxy tests for the new lifecycle methods.
Remove the Redis pool heartbeat and max-lifetime item now that the feature, docs, config, and regression coverage have been added.
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@binaryfire, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 24 minutes and 27 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d4e4f59-4190-452a-9c38-1d136cb64863

📥 Commits

Reviewing files that changed from the base of the PR and between 148d0c0 and 7854168.

📒 Files selected for processing (4)
  • src/boost/todo.md
  • src/redis/src/RedisConnection.php
  • tests/Pool/HeartbeatConnectionTest.php
  • tests/Redis/RedisPoolHeartbeatTest.php
📝 Walkthrough

Walkthrough

Adds a background heartbeat timer to RedisPool that periodically sweeps idle connections, evicting those that exceed max_lifetime or heartbeat_timeout, and health-checking the rest via an async heartbeatCheck() coroutine on RedisConnection. Introduces createdAt/availableForReuse state, markReconnected(), and public eviction helpers on RedisConnection. Fixes KeepaliveConnection to skip timer setup when heartbeat is disabled. Updates PhpRedisConnection/PhpRedisClusterConnection reconnect to use markReconnected(), expands RedisProxy connection-bound method guarding, and makes all five Redis client pool configs env-driven.

Changes

Redis Pool Heartbeat & Lifetime Recycling

Layer / File(s) Summary
RedisConnection state, eviction helpers, and heartbeat check
src/redis/src/RedisConnection.php
Adds createdAt and availableForReuse state properties; implements check() with idle/lifetime gating; adds markReconnected() to reset generation state; adds heartbeatCheck(float $timeout) running an async coroutine ping via Channel with cancellation on timeout; adds isIdleExpired(), isLifetimeExpired(), getCreatedAt() eviction helpers; adds pingForHeartbeat() for standalone and cluster; wraps release() in try/catch/finally to invalidate on error and set availableForReuse.
KeepaliveConnection heartbeat disable fix
src/pool/src/KeepaliveConnection.php
addHeartbeat() reads interval directly from pool options and returns early when heartbeat <= 0; removes getHeartbeatSeconds() helper that previously defaulted disabled heartbeat to 10 seconds.
PhpRedis reconnect → markReconnected
src/redis/src/PhpRedisConnection.php, src/redis/src/PhpRedisClusterConnection.php
Both reconnect() implementations replace the manual markValid() + lastUseTime assignment with a single markReconnected() call.
RedisPool heartbeat scheduler and sweep
src/redis/src/Pool/RedisPool.php
Adds heartbeatTimer, heartbeatTimerId, and heartbeatGeneration fields; constructor instantiates timer and calls startHeartbeat(); destructor calls clearHeartbeat(); flushAll() clears heartbeat before flushing; startHeartbeat()/clearHeartbeat() manage a periodic tick; heartbeat() drains idle connections; heartbeatConnection() applies lifetime/idle eviction, calls heartbeatCheck(), and releases or discards based on generation stability; discardHeartbeatConnection() closes and decrements currentConnections; logHeartbeatError()/getLogger() provide optional logging.
RedisProxy wiring and facade ignore list
src/redis/src/RedisProxy.php, src/support/src/Facades/Redis.php
CONNECTION_BOUND_METHODS expanded with getCreatedAt, heartbeatCheck, isIdleExpired, isLifetimeExpired, masters; withConnection and withPinnedConnection now explicitly call $connection->getConnection(); facade ignoredFacadeDocumenterMethods() updated to exclude the new methods.
Pool config and documentation
src/foundation/config/database.php, src/boost/docs/redis.md, src/boost/todo.md
All five Redis pool configs expose env-driven heartbeat, heartbeat_timeout, and max_lifetime; redis.md pool examples and narrative document the new options and recycling semantics; todo.md Pool section removed.
Tests
tests/Redis/RedisPoolHeartbeatTest.php, tests/Redis/RedisPoolTest.php, tests/Redis/RedisProxyTest.php, tests/Pool/HeartbeatConnectionTest.php, tests/Pool/Fixtures/KeepaliveConnectionStub.php, tests/Integration/Redis/RedisPoolHeartbeatIntegrationTest.php
Adds RedisPoolHeartbeatTest covering timer lifecycle, eviction rules, heartbeat timeout/flush interaction, withConnection/pinned-connection reconnect, and cluster master pinging; updates HeartbeatConnectionTest with explicit pool config and closeCount assertions; adds closeCount tracking to KeepaliveConnectionStub; adds integration test asserting heartbeatCheck(1.0) returns true against a real Redis connection; updates RedisPoolTest and RedisProxyTest for new bound methods and logger mock.

Sequence Diagram(s)

sequenceDiagram
  participant Timer
  participant RedisPool
  participant RedisConnection
  participant Coroutine
  participant Redis

  rect rgba(100, 149, 237, 0.5)
    note over Timer,RedisPool: Background heartbeat tick
    Timer->>RedisPool: tick()
    RedisPool->>RedisPool: heartbeat()
    loop for each idle connection
      RedisPool->>RedisConnection: isLifetimeExpired()?
      alt expired
        RedisPool->>RedisConnection: close()
      else not expired
        RedisPool->>RedisConnection: isIdleExpired()?
        alt idle expired
          RedisPool->>RedisConnection: close()
        else healthy candidate
          RedisPool->>RedisConnection: heartbeatCheck(timeout)
          RedisConnection->>Coroutine: go(pingForHeartbeat)
          Coroutine->>Redis: ping()
          Redis-->>Coroutine: pong
          Coroutine-->>RedisConnection: true
          RedisConnection-->>RedisPool: true
          RedisPool->>RedisPool: check generation stable?
          alt generation unchanged
            RedisPool->>RedisConnection: release()
          else generation changed
            RedisPool->>RedisConnection: close()
          end
        end
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hippity-hop, the pool stays alive,
With heartbeats and timers to help it survive!
Expired connections get swept clean away,
While fresh ones get pinged and returned to the fray.
Max-lifetime and idle, now tracked with great care—
A rabbit-approved pool, beyond compare! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.70% 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 pull request title 'feat(redis): add pool heartbeat and lifetime recycling' clearly and concisely summarizes the primary changes, directly corresponding to the main features added: pool heartbeat sweeps and lifetime recycling for Redis connections.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/redis-pool-heartbeat-lifetime

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.

@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: 2

🤖 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 `@src/redis/src/RedisConnection.php`:
- Around line 684-689: When an exception occurs during the release process in
the catch block that catches Throwable, the $this->database property retains the
previously selected DB value, causing issues when the connection is later
reconnected. Fix this by resetting $this->database to its default value (null or
the configured default DB) inside the catch block before or alongside the
$this->markInvalid() call, ensuring the connection will select the correct
default DB on the next reconnection instead of using the stale DB value.

In `@tests/Pool/HeartbeatConnectionTest.php`:
- Line 53: In the test setup, the `$conn` variable assignment in the
setActiveConnection method call is never used after being assigned. Remove the
unused variable assignment by passing the anonymous class instance directly to
setActiveConnection without storing it in `$conn`, which will eliminate the
PHPMD warning while maintaining the same functionality.
🪄 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: 984a58ef-d416-4c3f-9c4d-90c711d2f1d1

📥 Commits

Reviewing files that changed from the base of the PR and between 8c4de4f and 148d0c0.

📒 Files selected for processing (16)
  • src/boost/docs/redis.md
  • src/boost/todo.md
  • src/foundation/config/database.php
  • src/pool/src/KeepaliveConnection.php
  • src/redis/src/PhpRedisClusterConnection.php
  • src/redis/src/PhpRedisConnection.php
  • src/redis/src/Pool/RedisPool.php
  • src/redis/src/RedisConnection.php
  • src/redis/src/RedisProxy.php
  • src/support/src/Facades/Redis.php
  • tests/Integration/Redis/RedisPoolHeartbeatIntegrationTest.php
  • tests/Pool/Fixtures/KeepaliveConnectionStub.php
  • tests/Pool/HeartbeatConnectionTest.php
  • tests/Redis/RedisPoolHeartbeatTest.php
  • tests/Redis/RedisPoolTest.php
  • tests/Redis/RedisProxyTest.php
💤 Files with no reviewable changes (1)
  • src/boost/todo.md

Comment thread src/redis/src/RedisConnection.php
Comment thread tests/Pool/HeartbeatConnectionTest.php Outdated
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds opt-in heartbeat sweeps and max-lifetime recycling to the Redis connection pool, fixes a capacity leak in release() when a database-reset fails, and makes KeepaliveConnection honor heartbeat = -1 correctly. Both new features are disabled by default and mirror the existing database pool design.

  • Heartbeat sweep (RedisPool): a Timer fires on the configured interval, pops idle connections from the channel, evicts lifetime- or idle-expired ones, pings the rest with a coroutine timeout, and uses a generation counter to discard any late results that arrive after flushAll().
  • Lifetime/idle recycling (RedisConnection): check() is overridden to gate lifecycle checks behind availableForReuse, so borrowed connections are never recycled mid-use; markReconnected() stamps createdAt at reconnect time; release() moves parent::release() to finally so a failed select() no longer leaks the pool slot.
  • Pre-callback validation (RedisProxy): withConnection and withPinnedConnection now call getConnection() before the user callback, ensuring lifetime-expired context connections are reconnected before the caller sees them.

Confidence Score: 5/5

Safe to merge. The new features are disabled by default and the capacity-leak fix improves correctness for all pools regardless of heartbeat configuration.

The three-path eviction model is internally consistent: isIdleExpired() uses only lastReleaseTime while check() uses max(lastReleaseTime, lastUseTime), and the separation is documented. The release() fix correctly places parent::release() in finally so a failed DB-reset no longer drops the pool slot. The generation counter in heartbeatConnection() cleanly handles the race between a successful ping and a concurrent flushAll(). Test coverage is comprehensive across all eviction paths, timeout cancellation, cluster master-ping, and the release-failure scenario.

No files require special attention.

Important Files Changed

Filename Overview
src/redis/src/Pool/RedisPool.php Adds heartbeat timer, sweep loop, idle/lifetime eviction, and generation-based post-flush discard. Logic is correct; the pool-level release() (channel push) is used for heartbeat re-queue, preserving lastReleaseTime and availableForReuse semantics.
src/redis/src/RedisConnection.php Adds check() override, heartbeatCheck(), isIdleExpired(), isLifetimeExpired(), markReconnected(), and pingForHeartbeat(). Fixes release() capacity leak by moving database = null and parent::release() to finally, correctly handling failed select() without losing the slot.
src/redis/src/RedisProxy.php Adds getConnection() validation call before withConnection and withPinnedConnection callbacks; adds lifecycle methods to the proxy block-list. Changes are minimal and correct.
src/pool/src/KeepaliveConnection.php Fixes heartbeat timer to respect heartbeat <= 0 (disabled), removing the implicit 10-second fallback. Removes the getHeartbeatSeconds() helper that masked the misconfiguration.
tests/Redis/RedisPoolHeartbeatTest.php Comprehensive new test file covering disabled heartbeat, lifetime/idle eviction, borrowed-connection safety, cluster master-ping, timeout discard with generation check, and the release-failure fix. All key behaviours are verified.
src/foundation/config/database.php Adds heartbeat_timeout and max_lifetime env-var backed config keys for all five Redis pool connection groups (default, cache, session, queue, reverb). Consistent with the existing env-var pattern.

Reviews (2): Last reviewed commit: "refactor(redis): simplify release databa..." | Re-trigger Greptile

Comment thread src/redis/src/Pool/RedisPool.php
Comment thread src/redis/src/Pool/RedisPool.php
Comment thread src/redis/src/Pool/RedisPool.php
Comment thread src/redis/src/RedisConnection.php
Comment thread src/redis/src/RedisConnection.php
Reset the selected Redis database marker on every release path so a failed reset cannot leak a stale selected DB into the next reconnect.

Add regression coverage for the marker reset, remove an unused heartbeat test variable, document the non-obvious request-idle heartbeat semantics, and broaden the max_lifetime jitter todo to cover Redis pools as well as database pools.
Remove the redundant selected database reset inside the release try block now that release normalizes the marker from a single finally block.
@binaryfire binaryfire merged commit bbd11e3 into 0.4 Jun 24, 2026
35 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