Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Redis and Predis saves can apply expiration even when HMSET fails, changing an existing key’s TTL or potentially deleting it.
1 open issue
What changed in this PR
This PR reduces network round trips when saving expiring cache entries through CodeIgniter’s Redis and Predis handlers.
Changes:
- Pipelines
HMSETandEXPIREATfor nonzero TTLs. - Retains the existing hash format and single-command path for zero TTLs.
Compatibility was assessed under the stricter develop policy because the base branch was unspecified. Runtime validation was left to CI.
| File | Description |
|---|---|
system/Cache/Handlers/RedisHandler.php |
Uses a phpredis pipeline for writes with expiration and checks the write result. |
system/Cache/Handlers/PredisHandler.php |
Uses a Predis pipeline for writes with expiration and checks the returned status. |
Review effort: Balanced
7de6a6d to
92844d1
Compare
paulbalandan
left a comment
There was a problem hiding this comment.
Please update the title and description to reflect the Lua implementation. The current description still describes pipelining and says Lua was deliberately avoided. Please also rerun the benchmark against the final implementation; the pipeline measurements do not establish the performance of this version.
Please add regression coverage for the EVAL-denied fallback in both handlers.
Lastly, please also consider whether using cached EVALs makes sense here (i.e. EVALSHA).
Send HMSET and EXPIREAT together in a single pipeline round trip rather than issuing two sequential network requests on every cache save. For normal cache usage, behavior and data structures remain identical. In error scenarios where HMSET fails (e.g. key contains a non-hash type), the pipelined EXPIREAT will still execute on the existing key, while save() returns false as expected.
92844d1 to
adaee6c
Compare
|
Thanks @paulbalandan for the review and the detailed pointers! 1. Title & description updatedThe PR title and description now reflect the actual implementation (atomic Lua script with 2. Benchmark rerun against the final implementationMeasured on 5,000
Local speedup: 1.30x (22.8% latency reduction). On distributed deployments where Redis sits on a separate host, the saved round trip translates to roughly a 50% reduction of network latency per save. 3. Regression coverage for the
|
…is handlers Execute HMSET and EXPIREAT atomically via a Lua script so that EXPIREAT is never executed when HMSET fails (e.g. WRONGTYPE error on non-hash keys). Falls back to sequential execution if EVAL is unsupported or blocked. Adds regression tests verifying non-hash keys preserve value and TTL.
adaee6c to
061a1f3
Compare
| return (bool) $this->redis->eval( | ||
| self::SAVE_SCRIPT, | ||
| [$key, $expiresAt, $dataType, (string) $value], | ||
| 1, | ||
| ); |
There was a problem hiding this comment.
When EVAL is disabled via rename-command, phpredis returns false rather than throwing, so the fallback is skipped. Could you handle this case and add a regression test where the mocked eval() returns false? Please distinguish it from integer 0, which indicates a failed HMSET inside the script.
| local res = redis.pcall('HMSET', KEYS[1], '__ci_type', ARGV[2], '__ci_value', ARGV[3]) | ||
| if type(res) == 'table' and res.err then | ||
| return 0 |
There was a problem hiding this comment.
This changes Predis error handling: a WRONGTYPE error previously raised a ServerException, but now returns false for non-zero TTLs. TTL 0 still throws. Please preserve the existing exception behavior.
…avior RedisHandler: treat `false` from phpredis EVAL (e.g. EVAL disabled via `rename-command`) as unavailable and fall back to sequential hMset and expireAt. Integer 0 still means HMSET failed inside the script and must not trigger the fallback. PredisHandler: call HMSET and EXPIREAT with redis.call so a WRONGTYPE error aborts the script before EXPIREAT and is propagated to the client as ServerException again. Add regression tests for both behaviors.

Description
In both
RedisHandler::save()andPredisHandler::save(), saving a cache item with a TTL previously executed two sequential network round trips:hMset(hmset)expireAt(expireat)This PR executes both commands atomically in a single network round trip using a small, self-contained Lua script (
EVAL):redis.pcall('HMSET', ...)inside Lua. If the existing key is not a hash (WRONGTYPE), the script immediately returns0without executingEXPIREAT, leaving the existing key's value and TTL intact.EVALis blocked or unsupported in a specific environment, atry/catchblock seamlessly falls back to sequential execution (hMsetfollowed byexpireAt).EVAL-deniedfallback path in both handlers.Benchmark (5,000 save() operations, local Redis instance)
Checklist: