feat: add Redis\Multiplexing adapter over Swoole TCP#70
Conversation
Greptile SummaryThis PR adds a
Confidence Score: 5/5The adapter is safe to merge; the core send-lock + FIFO-channel invariant is correct and all previously raised defects have been resolved in this revision. The send/enqueue atomicity, per-request read timeout, coroutine-safe retry, and teardown/drain logic are all implemented correctly. The two remaining notes are a test-only blocking sleep and a theoretical channel-closed edge case that is unreachable under current usage. No files require special attention; Multiplexing.php and Client.php are the most complex additions but both hold up under close scrutiny. Important Files Changed
Reviews (10): Last reviewed commit: "fix: throw InvalidArgumentException for ..." | Re-trigger Greptile |
- Move every test under tests/Cache/E2E so e2e and unit suites can be selected independently. - Group RedisTest, RedisClusterTest under tests/Cache/E2E/Redis/. - Update phpunit.xml to expose 'unit' and 'e2e' testsuites; the default test command no longer needs to enumerate files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A coroutine-aware Redis cache adapter that multiplexes many concurrent
Swoole coroutines over a single Redis TCP connection. The architecture
is documented in docs/multiplexing.md.
Key components under src/Cache/Adapter/Redis/:
- Client — Swoole TCP socket wrapper plus the RESP2 codec. Owns the
socket and any unread byte buffer.
- Multiplexing — the cache adapter. Holds at most one ConnectionContext
at a time; a single send lock serialises pending->push + send so the
FIFO invariant matches inbound RESP frames to outbound commands.
- ConnectionContext — value object pairing a Client with its pending
queue. Per-context state means a context can be torn down on timeout
without poisoning a replacement context.
- Envelope — shared {time, data} cache JSON envelope (encode/decode/
touch with TTL semantics).
- ConnectionException, ConnectionError, RedisError — typed signals so
the retry loop can distinguish reconnectable errors from server-side
Redis errors.
Implements Cache\Feature\Telemetry; emits a
redis_multiplexing.pending.depth gauge after each enqueue, the single
most useful health signal for this adapter.
Tests: 23 RESP codec + unwrap unit tests, 14 envelope unit tests, and
10 e2e tests covering basic ops, large/nested JSON payloads, and 50-way
concurrent multiplexing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace per-version Dockerfiles with a single Dockerfile.php-8.4 built on appwrite/utopia-base:php-8.4-1.0.0 (Swoole 6.1 ships Coroutine\Lock, which Multiplexing needs). - Collapse the CI matrix to 8.4 and bump composer.json to >=8.4. - Add swoole/ide-helper for phpstan stub coverage. - Add phpstan.neon scanning the swoole stubs. - Add docs/multiplexing.md, a consumer-oriented guide. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
d58a7bc to
ce01efb
Compare
Replace the inline {time, data} JSON encode/decode/touch in
Adapter\Redis with calls to Adapter\Redis\Envelope. Same logic as
before, except load() now applies the envelope-shape guard the
multiplexing adapter already used — previously Redis::load would
crash on missing keys or non-int 'time'.
Also fix the empty()-based auth check so a literal '0' password is
preserved (same fix Multiplexing already had).
Drop a stray testGetSize pollution in RedisTest by flushing first.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 2.0.0 utopia-base images all ship Swoole 6.2 with Coroutine\Lock plus ext-redis, so we no longer need a custom Swoole image. Drop 8.2 (no longer published in the 2.0.0 line), add 8.5, and bump the php-memcached extension to v3.4.0 (8.5-compatible). Lower the composer.json floor to >=8.3 to match the matrix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous draft posed the choice as multiplexing vs. a non-coroutine PhpRedis adapter, but the realistic alternative for Swoole apps is a pool of phpredis connections (Adapter\Pool wrapping Adapter\Redis). Reframe accordingly and trim to the parts a consumer actually needs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rename redis_multiplexing.pending.depth → cache.redis_multiplexing.pending.depth to match the cache.* prefix the existing operation-duration histogram already uses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| if (empty($key) || empty($data)) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
The same
empty('0') footgun fixed in the auth check also affects save(). Use a concrete emptiness check for strings so the value '0' is not silently discarded. Redis.php::save() has the identical pattern and should receive the same fix.
| if (empty($key) || empty($data)) { | |
| return false; | |
| } | |
| if ($key === '' || $data === '' || $data === []) { | |
| return false; | |
| } |
The previous default of 0.0 with a 'timeout > 0 ? timeout : 1.0' fallback was confusing — the documented default and the actual default disagreed. Use 1.0s directly so the constructor signature matches behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously readTimeout <= 0 was silently coerced to 0.25s. Better to fail loudly so misconfiguration surfaces during testing rather than masking the user's intent at runtime. Apply the same rule to the connect timeout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
A new
Utopia\Cache\Adapter\Redis\Multiplexingadapter that lets many concurrent Swoole coroutines share a single Redis TCP connection. Designed for Swoole-based servers that want one Redis socket serving thousands of cooperative coroutines instead of one socket per request.Includes a small refactor that pulls the Redis cache envelope out into a shared
Redis\Envelopecodec so the existingAdapter\Redisand the newAdapter\Redis\Multiplexingagree on storage format.What's in
src/Cache/Adapter/Redis/Multiplexing— the cache adapter. Holds at most one publishedConnectionContext; a single send-lock serialisespending->push+client->sendso the FIFO order of registered response channels matches the order of bytes on the wire and Redis's guaranteed in-order replies pair them correctly.Client— Swoole TCP wrapper plus the RESP2 codec (encode/parse). Owns the socket and any unread byte buffer; handshake leftovers stay in its buffer and are drained by the reader viatakeBuffer().ConnectionContext— value object pairing aClientwith its pending queue. Per-context state means a context can be torn down on timeout without poisoning a replacement context.Envelope— shared{time, data}JSON envelope (encode/decode/touch with TTL semantics). Used by bothAdapter\RedisandAdapter\Redis\Multiplexing.ConnectionException,ConnectionError,RedisError— typed signals so the retry loop can distinguish reconnectable transport errors from server-side Redis errors.Behaviour highlights
Cache\Feature\Telemetry— emits aredis_multiplexing.pending.depthgauge after each enqueue, the single most useful health signal for this adapter.readTimeoutdefaults to 250 ms — caches should fail fast and let callers fall through to the source of truth.ConnectionExceptionand the next command publishes a fresh context. By design — alternative is per-request resync logic, much more complex.'0'password bug — bothAdapter\RedisandAdapter\Redis\Multiplexingpreviously skippedAUTHwhen the password literal was'0'(becauseempty('0') === true). Both now use a strict null check.Documentation
docs/multiplexing.md— consumer-facing guide: when to use, constructor options, error model, lifecycle, limitations.Test plan
vendor/bin/phpunit --testsuite unit— 46 tests / 77 assertions, no Docker neededClienttests covering RESP encode/parse edge cases,unwraprethrow at top level and nested in arraysEnvelopetests covering shape, TTL boundary, malformed JSON, nested data,touchsemanticsvendor/bin/phpunit --testsuite e2e --filter Multiplexing— 10 tests / 28 assertionsappwrite/utopia-base:*-2.0.0)Notes for reviewers
tests/Cache/Unit/andtests/Cache/E2E/; existing tests moved unchanged underE2E/. The twophpunittestsuites (unit,e2e) let CI run the fast unit suite without Docker if needed.RedisCluster.phpandRedis.phpwere intentionally not renamed in this PR; the new code lives atAdapter\Redis\*(subnamespace) and coexists with the existing top-levelAdapter\Redisclass.['8.3', '8.4', '8.5'], dropping 8.1/8.2.🤖 Generated with Claude Code