diff --git a/src/Commands/QueueClear.php b/src/Commands/QueueClear.php index c57ba24..98e476e 100644 --- a/src/Commands/QueueClear.php +++ b/src/Commands/QueueClear.php @@ -68,7 +68,13 @@ public function run(array $params) return EXIT_ERROR; } - service('queue')->clear($queue); + $result = service('queue')->clear($queue); + + if (! $result) { + CLI::error('Failed to clear the queue: ' . $queue . '.'); + + return EXIT_ERROR; + } CLI::print('Queue ', 'yellow'); CLI::print($queue, 'light_yellow'); diff --git a/src/Handlers/PredisHandler.php b/src/Handlers/PredisHandler.php index 62ee23e..aeaa23f 100644 --- a/src/Handlers/PredisHandler.php +++ b/src/Handlers/PredisHandler.php @@ -25,6 +25,7 @@ use CodeIgniter\Queue\QueuePushResult; use Exception; use Predis\Client; +use Predis\Command\Processor\KeyPrefixProcessor; use RuntimeException; use Throwable; @@ -206,22 +207,61 @@ public function done(QueueJob $queueJob): bool */ public function clear(?string $queue = null): bool { - if ($queue !== null) { - $keys = $this->predis->keys("queues:{$queue}:*"); - $result = $keys !== [] ? $this->predis->del($keys) > 0 : true; - } else { - $keys = $this->predis->keys('queues:*'); - $result = $keys !== [] ? $this->predis->del($keys) > 0 : true; + try { + if ($queue !== null) { + // Reject glob metacharacters before building the key pattern. + $this->validateQueue($queue); + } + + $pattern = $queue === null ? 'queues:*' : "queues:{$queue}:*"; + $keys = $this->predis->keys($pattern); + + if ($keys !== []) { + // KEYS returns full key names, including the client prefix. + // DEL would apply the prefix again, so strip it first. + $prefix = $this->getKeyPrefix(); + + if ($prefix !== '') { + $keys = array_map( + static fn (string $key): string => str_starts_with($key, $prefix) ? substr($key, strlen($prefix)) : $key, + $keys, + ); + } + + $this->predis->del($keys); + } + } catch (Throwable $e) { + log_message('error', 'Predis clear error: ' . $e->getMessage()); + + return false; } - if ($result) { - // Emit queue cleared event - QueueEventManager::queueCleared( - handler: $this->name(), - queue: $queue, - ); + // Emit queue cleared event + QueueEventManager::queueCleared( + handler: $this->name(), + queue: $queue, + ); + + return true; + } + + /** + * Get the key prefix configured for the Predis client. + */ + private function getKeyPrefix(): string + { + $options = $this->predis->getOptions(); + + if (! $options->defined('prefix')) { + return ''; + } + + $prefix = $options->prefix; + + if ($prefix instanceof KeyPrefixProcessor) { + return (string) $prefix->getPrefix(); } - return $result; + return ''; } } diff --git a/src/Handlers/RedisHandler.php b/src/Handlers/RedisHandler.php index 767de31..aa7d56c 100644 --- a/src/Handlers/RedisHandler.php +++ b/src/Handlers/RedisHandler.php @@ -247,22 +247,61 @@ public function done(QueueJob $queueJob): bool */ public function clear(?string $queue = null): bool { - if ($queue !== null) { - $result = ($keys = $this->redis->keys("queues:{$queue}:*")) ? (int) $this->redis->del($keys) > 0 : true; - } elseif ($keys = $this->redis->keys('queues:*')) { - $result = (int) $this->redis->del($keys) > 0; - } else { - $result = true; - } + try { + if ($queue !== null) { + // Reject glob metacharacters before building the key pattern. + $this->validateQueue($queue); + } - if ($result) { - // Emit queue cleared event - QueueEventManager::queueCleared( - handler: $this->name(), - queue: $queue, - ); + $pattern = $queue === null ? 'queues:*' : "queues:{$queue}:*"; + $keys = $this->redis->keys($pattern); + + if ($keys === false) { + log_message('error', 'Redis clear error: unable to list keys for pattern ' . $pattern . '.'); + + return false; + } + + if ($keys !== []) { + // KEYS returns full key names, including the client prefix. + // DEL would apply the prefix again, so strip it first. + $prefix = $this->normalizePrefix($this->redis->getOption(Redis::OPT_PREFIX)); + + if ($prefix !== '') { + $prefixLength = strlen($prefix); + + $keys = array_map( + static fn (string $key): string => str_starts_with($key, $prefix) ? substr($key, $prefixLength) : $key, + $keys, + ); + } + + if ($this->redis->del($keys) === false) { + log_message('error', 'Redis clear error: unable to delete keys for pattern ' . $pattern . '.'); + + return false; + } + } + } catch (Throwable $e) { + log_message('error', 'Redis clear error: ' . $e->getMessage()); + + return false; } - return $result; + // Emit queue cleared event + QueueEventManager::queueCleared( + handler: $this->name(), + queue: $queue, + ); + + return true; + } + + /** + * Normalize the value returned by Redis::getOption() to a string prefix. + */ + private function normalizePrefix(mixed $prefix): string + { + return is_string($prefix) ? $prefix : ''; } } diff --git a/tests/Commands/QueueClearTest.php b/tests/Commands/QueueClearTest.php index 6024837..5fc54ca 100644 --- a/tests/Commands/QueueClearTest.php +++ b/tests/Commands/QueueClearTest.php @@ -13,7 +13,9 @@ namespace Tests\Commands; +use CodeIgniter\Queue\Interfaces\QueueInterface; use CodeIgniter\Test\Filters\CITestStreamFilter; +use Config\Services; use Tests\Support\CLITestCase; /** @@ -46,4 +48,25 @@ public function testRun(): void $this->assertSame('Queue test has been cleared.', $output); } + + public function testRunFailedWhenClearReturnsFalse(): void + { + $queueMock = $this->createMock(QueueInterface::class); + $queueMock->expects($this->once()) + ->method('clear') + ->with('test') + ->willReturn(false); + + Services::injectMock('queue', $queueMock); + + CITestStreamFilter::registration(); + CITestStreamFilter::addErrorFilter(); + + $this->assertNotFalse(command('queue:clear test')); + $output = $this->parseOutput(CITestStreamFilter::$buffer); + + CITestStreamFilter::removeErrorFilter(); + + $this->assertSame('Failed to clear the queue: test.', $output); + } } diff --git a/tests/PredisHandlerTest.php b/tests/PredisHandlerTest.php index 8fcf4e2..2ac6b34 100644 --- a/tests/PredisHandlerTest.php +++ b/tests/PredisHandlerTest.php @@ -375,6 +375,37 @@ public function testClearAll(): void $this->assertTrue($result); } + public function testClearWithPrefix(): void + { + $config = clone $this->config; + $config->predis['prefix'] = 'app:'; + + $handler = new PredisHandler($config); + $handler->push('queue1', 'success', ['key' => 'value']); + + $predis = self::getPrivateProperty($handler, 'predis'); + $this->assertGreaterThan(0, $predis->zcard('queues:queue1:default')); + + $result = $handler->clear('queue1'); + $this->assertTrue($result); + $this->assertSame(0, $predis->zcard('queues:queue1:default')); + + // Clear all queues with prefix + $handler->push('queue1', 'success', ['key' => 'value']); + $this->assertTrue($handler->clear()); + $this->assertCount(0, $predis->keys('queues:*')); + } + + public function testClearWithInvalidQueueNameReturnsFalse(): void + { + $handler = new PredisHandler($this->config); + + $this->assertFalse($handler->clear('queue*')); + $this->assertFalse($handler->clear('queue?')); + $this->assertFalse($handler->clear('queue!invalid')); + $this->assertFalse($handler->clear(str_repeat('a', 65))); + } + /** * @throws ReflectionException */ diff --git a/tests/RedisHandlerTest.php b/tests/RedisHandlerTest.php index 0b64d0b..fbe9e02 100644 --- a/tests/RedisHandlerTest.php +++ b/tests/RedisHandlerTest.php @@ -348,6 +348,37 @@ public function testClearAll(): void $this->assertTrue($result); } + public function testClearWithPrefix(): void + { + $config = clone $this->config; + $config->redis['prefix'] = 'app:'; + + $handler = new RedisHandler($config); + $handler->push('queue1', 'success', ['key' => 'value']); + + $redis = self::getPrivateProperty($handler, 'redis'); + $this->assertGreaterThan(0, $redis->zCard('queues:queue1:default')); + + $result = $handler->clear('queue1'); + $this->assertTrue($result); + $this->assertSame(0, $redis->zCard('queues:queue1:default')); + + // Clear all queues with prefix + $handler->push('queue1', 'success', ['key' => 'value']); + $this->assertTrue($handler->clear()); + $this->assertCount(0, $redis->keys('queues:*')); + } + + public function testClearWithInvalidQueueNameReturnsFalse(): void + { + $handler = new RedisHandler($this->config); + + $this->assertFalse($handler->clear('queue*')); + $this->assertFalse($handler->clear('queue?')); + $this->assertFalse($handler->clear('queue!invalid')); + $this->assertFalse($handler->clear(str_repeat('a', 65))); + } + public function testRetry(): void { $handler = new RedisHandler($this->config);