|
6 | 6 |
|
7 | 7 | use Generator; |
8 | 8 | use Hyperf\Redis\RedisConnection as HyperfRedisConnection; |
| 9 | +use Hypervel\Redis\Exceptions\LuaScriptException; |
9 | 10 | use Hypervel\Redis\Operations\FlushByPattern; |
10 | 11 | use Hypervel\Redis\Operations\SafeScan; |
11 | 12 | use Hypervel\Support\Arr; |
@@ -801,6 +802,61 @@ public function client(): mixed |
801 | 802 | return $this->connection; |
802 | 803 | } |
803 | 804 |
|
| 805 | + /** |
| 806 | + * Execute a Lua script using evalSha with automatic fallback to eval. |
| 807 | + * |
| 808 | + * Redis caches compiled Lua scripts by SHA1 hash. This method tries evalSha |
| 809 | + * first (uses cached compiled script), and falls back to eval if the script |
| 810 | + * isn't cached yet (NOSCRIPT error). |
| 811 | + * |
| 812 | + * Unlike naive implementations that treat any `false` return as NOSCRIPT, |
| 813 | + * this method properly distinguishes NOSCRIPT errors from other failures |
| 814 | + * (syntax errors, OOM, WRONGTYPE, etc.) and throws on non-NOSCRIPT errors. |
| 815 | + * |
| 816 | + * @param string $script The Lua script to execute |
| 817 | + * @param array<string> $keys Redis keys (passed as KEYS[] in Lua) |
| 818 | + * @param array<mixed> $args Additional arguments (passed as ARGV[] in Lua) |
| 819 | + * @return mixed The script's return value |
| 820 | + * |
| 821 | + * @throws LuaScriptException If script execution fails (non-NOSCRIPT error) |
| 822 | + */ |
| 823 | + public function evalWithShaCache(string $script, array $keys = [], array $args = []): mixed |
| 824 | + { |
| 825 | + $sha = sha1($script); |
| 826 | + $numKeys = count($keys); |
| 827 | + |
| 828 | + // phpredis signature: evalSha(sha, combined_args, num_keys) |
| 829 | + // combined_args = keys first, then other args |
| 830 | + $combinedArgs = [...$keys, ...$args]; |
| 831 | + |
| 832 | + // Try evalSha first - uses cached compiled script |
| 833 | + $result = $this->connection->evalSha($sha, $combinedArgs, $numKeys); |
| 834 | + |
| 835 | + if ($result === false) { |
| 836 | + $error = $this->connection->getLastError(); |
| 837 | + |
| 838 | + // NOSCRIPT means script not cached yet - fall back to eval |
| 839 | + if ($error !== null && str_contains($error, 'NOSCRIPT')) { |
| 840 | + $this->connection->clearLastError(); |
| 841 | + $result = $this->connection->eval($script, $combinedArgs, $numKeys); |
| 842 | + |
| 843 | + if ($result === false) { |
| 844 | + $evalError = $this->connection->getLastError(); |
| 845 | + if ($evalError !== null) { |
| 846 | + throw new LuaScriptException('Lua script execution failed: ' . $evalError); |
| 847 | + } |
| 848 | + // If no error, script legitimately returned nil (which becomes false) |
| 849 | + } |
| 850 | + } elseif ($error !== null) { |
| 851 | + // Some other error (syntax, OOM, WRONGTYPE, etc.) |
| 852 | + throw new LuaScriptException('Lua script execution failed: ' . $error); |
| 853 | + } |
| 854 | + // If $error is null and $result is false, the script legitimately returned false |
| 855 | + } |
| 856 | + |
| 857 | + return $result; |
| 858 | + } |
| 859 | + |
804 | 860 | /** |
805 | 861 | * Safely scan the Redis keyspace for keys matching a pattern. |
806 | 862 | * |
|
0 commit comments