<?php
/**
 * Copyright © 2019-2025 Rhubarb Tech Inc. All Rights Reserved.
 *
 * The Object Cache Pro Software and its related materials are property and confidential
 * information of Rhubarb Tech Inc. Any reproduction, use, distribution, or exploitation
 * of the Object Cache Pro Software and its related materials, in whole or in part,
 * is strictly forbidden unless prior permission is obtained from Rhubarb Tech Inc.
 *
 * In addition, any reproduction, use, distribution, or exploitation of the Object Cache Pro
 * Software and its related materials, in whole or in part, is subject to the End-User License
 * Agreement accessible in the included `LICENSE` file, or at: https://objectcache.pro/eula
 */

declare(strict_types=1);

namespace RedisCachePro\ObjectCaches;

use Throwable;
use ReflectionClass;

use RedisCachePro\Configuration\Configuration;
use RedisCachePro\Connections\PhpRedisConnection;

class PhpRedisObjectCache extends ObjectCache implements MeasuredObjectCacheInterface
{
    use Concerns\KeepsMetadata,
        Concerns\PrunesQueries,
        Concerns\PrefetchesKeys,
        Concerns\FlushesNetworks,
        Concerns\TakesMeasurements,
        Concerns\SplitsAllOptionsIntoHash;

    /**
     * The connection instance.
     *
     * @var \RedisCachePro\Connections\PhpRedisConnection
     */
    protected $connection;

    /**
     * Create new PhpRedis object cache instance.
     *
     * @param  \RedisCachePro\Connections\PhpRedisConnection  $connection
     * @param  \RedisCachePro\Configuration\Configuration  $config
     * @param  ?\RedisCachePro\ObjectCaches\ObjectCacheMetrics  $metrics
     */
    public function __construct(
        PhpRedisConnection $connection,
        Configuration $config,
        ?ObjectCacheMetrics $metrics = null
    ) {
        $this->setup($config, $connection, $metrics);
    }

    /**
     * Adds data to the cache, if the cache key doesn't already exist.
     *
     * @param  int|string  $key
     * @param  mixed  $data
     * @param  string  $group
     * @param  int  $expire
     * @return bool
     */
    public function add($key, $data, string $group = 'default', int $expire = 0): bool
    {
        if (function_exists('wp_suspend_cache_addition') && \wp_suspend_cache_addition()) {
            return false;
        }

        if (! $id = $this->id($key, $group)) {
            return false;
        }

        if ($this->hasInMemory($id, $group)) {
            return false;
        }

        if ($this->isNonPersistentGroup($group)) {
            $this->storeInMemory($id, $data, $group);

            return true;
        }

        try {
            if ($this->isAllOptionsId($id)) {
                return $this->syncAllOptions($id, $data);
            }

            $result = (bool) $this->write($id, $data, $group, $expire, 'NX');

            $this->metrics->write($group);

            if ($result) {
                $this->storeInMemory($id, $data, $group);
            }

            return $result;
        } catch (Throwable $exception) {
            $this->error($exception);

            return false;
        }
    }

    /**
     * Adds multiple values to the cache in one call, if the cache keys doesn't already exist.
     *
     * @param  array<int|string, mixed>  $data
     * @param  string  $group
     * @param  int  $expire
     * @return array<int|string, bool>
     */
    public function add_multiple(array $data, string $group = 'default', int $expire = 0): array
    {
        $ids = [];

        if (empty($data)) {
            return [];
        }

        $data = array_filter($data, function ($key) use ($group, &$ids) {
            return ($id = $this->id($key, $group)) ? (bool) $ids[$key] = $id : false;
        }, ARRAY_FILTER_USE_KEY);

        if (function_exists('wp_suspend_cache_addition') && \wp_suspend_cache_addition()) {
            return array_combine(array_keys($data), array_fill(0, count($data), false));
        }

        $results = [];

        if ($this->isNonPersistentGroup($group)) {
            foreach ($data as $key => $value) {
                $results[$key] = ! $this->hasInMemory($ids[$key], $group);

                if ($results[$key]) {
                    $this->storeInMemory($ids[$key], $value, $group);
                }
            }

            return $results;
        }

        foreach ($data as $key => $value) {
            if ($this->hasInMemory($ids[$key], $group)) {
                $results[$key] = false;
            }
        }

        $remainingData = array_diff_key($data, $results);

        if (empty($remainingData)) {
            return $results;
        }

        try {
            $response = $this->multiwrite($remainingData, $group, $expire, 'NX');
        } catch (Throwable $exception) {
            $this->error($exception);

            return array_combine(array_keys($data), array_fill(0, count($data), false));
        }

        $this->metrics->write($group);

        foreach ($response as $key => $result) {
            if ($result['id'] && $result['response']) {
                $this->storeInMemory($result['id'], $data[$key], $group);
            }

            $results[$key] = $result['response'];
        }

        $order = array_flip(array_keys($data));

        uksort($results, static function ($a, $b) use ($order) {
            return $order[$a] - $order[$b];
        });

        return $results;
    }

    /**
     * Boots the cache.
     *
     * @return bool
     */
    public function boot(): bool
    {
        $this->bootMetadata();

        if (! $this->isMultisite()) {
            $this->prefetch();
        }

        return true;
    }

    /**
     * Closes the cache.
     *
     * @return bool
     */
    public function close(): bool
    {
        $this->storePrefetches();
        $this->storeMeasurements();

        return true;
    }

    /**
     * Decrements numeric cache item's value.
     *
     * To preserve the key's expiry Redis 6.0 and PhpRedis 5.3 or newer is required.
     *
     * @param  int|string  $key
     * @param  int  $offset
     * @param  string  $group
     * @return int|false
     */
    public function decr($key, int $offset = 1, string $group = 'default')
    {
        if (! $id = $this->id($key, $group)) {
            return false;
        }

        if ($this->isNonPersistentGroup($group)) {
            if (! $this->hasInMemory($id, $group)) {
                return false;
            }

            $value = $this->getFromMemory($id, $group);
            $value = $this->decrement($value, $offset);

            $this->storeInMemory($id, $value, $group);

            return $value;
        }

        try {
            $value = $this->connection->get($id);

            $this->metrics->read($group);

            if ($value === false) {
                return false;
            }

            $value = $this->decrement($value, $offset);
            $result = $this->connection->set($id, $value, ['KEEPTTL']);

            if ($result === false && $this->connection->lastErrorWasSyntaxError()) {
                $result = $this->connection->set($id, $value);
            }

            $this->metrics->write($group);

            if ($result) {
                $this->storeInMemory($id, $value, $group);
            }

            return $value;
        } catch (Throwable $exception) {
            $this->error($exception);

            return false;
        }
    }

    /**
     * Removes the cache contents matching key and group from the runtime memory cache.
     *
     * @param  int|string  $key
     * @param  string  $group
     * @return bool
     */
    public function deleteFromMemory($key, string $group = 'default')
    {
        if ($deleted = parent::deleteFromMemory($key, $group)) {
            unset($this->prefetch[$group][$key]);
        }

        return $deleted;
    }

    /**
     * Removes the cache contents matching key and group.
     *
     * @param  int|string  $key
     * @param  string  $group
     * @return bool
     */
    public function delete($key, string $group = 'default'): bool
    {
        if (! $id = $this->id($key, $group)) {
            return false;
        }

        $deletedFromMemory = $this->deleteFromMemory($key, $group);

        if ($this->isNonPersistentGroup($group)) {
            return $deletedFromMemory;
        }

        try {
            if ($this->isAllOptionsId($id)) {
                return $this->deleteAllOptions($id);
            }

            $method = $this->config->async_flush ? 'unlink' : 'del';
            $result = (bool) $this->connection->{$method}($id);

            $this->metrics->write($group);

            return $result;
        } catch (Throwable $exception) {
            $this->error($exception);
        }

        return false;
    }

    /**
     * Deletes multiple values from the cache in one call.
     *
     * @param  array<int|string>  $keys
     * @param  string  $group
     * @return array<int|string, bool>
     */
    public function delete_multiple(array $keys, string $group = 'default'): array
    {
        $ids = [];

        if (empty($keys)) {
            return [];
        }

        $keys = array_filter($keys, function ($key) use ($group, &$ids) {
            return ($id = $this->id($key, $group)) ? (bool) $ids[$key] = $id : false;
        });

        $results = [];

        if ($this->isNonPersistentGroup($group)) {
            foreach ($keys as $key) {
                $results[$key] = $this->deleteFromMemory($key, $group);
            }

            return $results;
        }

        foreach ($keys as $key) {
            $results[$key] = $ids[$key];
        }

        $deletes = [];
        $command = $this->config->async_flush ? 'unlink' : 'del';

        try {
            $pipe = $this->connection->pipeline();

            foreach ($results as $key => $id) {
                unset($this->cache[$group][$id]);
                unset($this->prefetch[$group][$key]);

                $deletes[] = $id;
                $pipe->{$command}($id);
            }

            $deletes = array_combine($deletes, array_map('boolval', $pipe->exec()));
        } catch (Throwable $exception) {
            $this->error($exception);

            return array_combine($keys, array_fill(0, count($keys), false));
        }

        $this->metrics->write($group);

        return array_map(static function ($id) use ($deletes) {
            return $deletes[$id];
        }, $results);
    }

    /**
     * Removes all items from Redis and the runtime cache.
     *
     * Will write metadata after flushing.
     * Will dump+restore analytics when enabled.
     *
     * @return bool
     */
    public function flush(): bool
    {
        $this->flush_runtime();

        if ($this->config->analytics->enabled && $this->config->analytics->persist) {
            $measurements = $this->dumpMeasurements();
        }

        try {
            $result = $this->connection->flushdb();

            $this->metrics->flush();
            $this->writeMetadata();
        } catch (Throwable $exception) {
            $result = false;

            $this->error($exception);
        }

        if (! empty($measurements)) {
            $this->restoreMeasurements($measurements);
            unset($measurements);
        }

        return $result;
    }

    /**
     * Removes all cache items from the in-memory runtime cache.
     *
     * @return bool
     */
    public function flush_runtime(): bool
    {
        $this->cache = [];
        $this->prefetch = [];

        return true;
    }

    /**
     * Removes all cache items in given group.
     *
     * @param  string  $group
     * @return bool
     */
    public function flush_group(string $group): bool
    {
        if ($this->config->group_flush === Configuration::GROUP_FLUSH_FULL) {
            return $this->flush();
        }

        unset($this->cache[$group]);
        unset($this->prefetch[$group]);

        if ($this->isNonPersistentGroup($group)) {
            return true;
        }

        $pattern = $this->id('*', $group);

        if ($pattern === false) {
            return false;
        }

        if ($this->isMultisite && ! $this->isGlobalGroup($group)) {
            $groupId = array_reverse(explode(':', $pattern))[1];
            $pattern = str_replace("{$this->blogId}:{$groupId}", "*:{$groupId}", (string) $pattern);
        }

        try {
            $this->deleteByPattern($pattern, $group);
        } catch (Throwable $exception) {
            $this->error($exception);

            return false;
        }

        return true;
    }

    /**
     * Retrieves the cache contents from the cache by key and group.
     *
     * Using `$found` to disambiguate a return value of `false`
     * requires PhpRedis 6.2.0 or Relay v0.10.1 or newer.
     *
     * @param  int|string  $key
     * @param  string  $group
     * @param  bool  $force
     * @param  bool  &$found
     * @return mixed|false
     */
    public function get($key, string $group = 'default', bool $force = false, &$found = false)
    {
        if (! $id = $this->id($key, $group)) {
            $found = false;

            return false;
        }

        $cachedInMemory = $this->hasInMemory($id, $group);

        if ($this->isNonPersistentGroup($group)) {
            if (! $cachedInMemory) {
                $found = false;
                $this->metrics->misses += 1;

                return false;
            }

            $found = true;
            $this->metrics->hits += 1;

            return $this->getFromMemory($id, $group);
        }

        if ($this->prefetched) {
            $this->prefetch[$group][$key] = true;
        }

        if ($cachedInMemory && ! $force) {
            $found = true;
            $this->metrics->hits += 1;

            return $this->getFromMemory($id, $group);
        }

        $found = false;
        $length = -1;

        try {
            if ($this->isAllOptionsId($id)) {
                $data = $this->getAllOptions($id);
            } elseif ($this->connection->supportsGetWithMeta) {
                $data = false;
                $result = $this->connection->getWithMeta($id);

                if (is_array($result)) {
                    $data = $result[0];
                    $length = $result[1]['length'];
                }
            } else {
                $data = $this->connection->get($id);
            }
        } catch (Throwable $exception) {
            $this->error($exception);

            return false;
        }

        $this->metrics->read($group);

        if ($data === false && $length < 0) {
            $this->metrics->misses += 1;
            $this->metrics->storeMisses += 1;

            return false;
        }

        $found = true;
        $this->metrics->hits += 1;
        $this->metrics->storeHits += 1;

        $this->storeInMemory($id, $data, $group);

        return $data;
    }

    /**
     * Retrieves multiple values from the cache in one call.
     *
     * @param  array<int|string>  $keys
     * @param  string  $group
     * @param  bool  $force
     * @return array<int|string, mixed>
     */
    public function get_multiple(array $keys, string $group = 'default', bool $force = false)
    {
        $ids = [];

        if (empty($keys)) {
            return [];
        }

        $keys = array_filter($keys, function ($key) use ($group, &$ids) {
            return ($id = $this->id($key, $group)) ? (bool) $ids[$key] = $id : false;
        });

        $values = [];

        if ($this->isNonPersistentGroup($group)) {
            foreach ($keys as $key) {
                if ($this->hasInMemory($ids[$key], $group)) {
                    $this->metrics->hits += 1;
                    $values[$key] = $this->getFromMemory($ids[$key], $group);
                } else {
                    $this->metrics->misses += 1;
                    $values[$key] = false;
                }
            }

            return $values;
        }

        if ($this->prefetched) {
            foreach ($keys as $key) {
                $this->prefetch[$group][$key] = true;
            }
        }

        $remainingKeys = [];

        foreach ($keys as $key) {
            $values[$key] = false;

            if (! $force && $this->hasInMemory($ids[$key], $group)) {
                $this->metrics->hits += 1;
                $values[$key] = $this->getFromMemory($ids[$key], $group);
            } else {
                $remainingKeys[] = $key;
            }
        }

        if (empty($remainingKeys)) {
            return $values;
        }

        $payload = array_map(static function ($key) use ($ids) {
            return $ids[$key];
        }, $remainingKeys);

        try {
            /** @var array<int|string, mixed>|false $data */
            $data = $this->connection->mget($payload);

            $this->metrics->read($group);

            if ($data === false) {
                $data = array_fill_keys(array_keys($payload), false);
            }

            foreach ($remainingKeys as $index => $key) {
                $values[$key] = $data[$index];

                if ($data[$index] === false) {
                    $this->metrics->misses += 1;
                    $this->metrics->storeMisses += 1;

                    continue;
                }

                $this->metrics->hits += 1;
                $this->metrics->storeHits += 1;

                if ($this->config->prefetch && ! $this->prefetched) {
                    $this->metrics->prefetches++;
                }

                $this->storeInMemory($payload[$index], $data[$index], $group);
            }
        } catch (Throwable $exception) {
            $this->error($exception);
        }

        return $values;
    }

    /**
     * Whether the key exists in the cache.
     *
     * @param  int|string  $key
     * @param  string  $group
     * @return bool
     */
    public function has($key, string $group = 'default'): bool
    {
        if (! $id = $this->id($key, $group)) {
            return false;
        }

        if ($this->hasInMemory($id, $group)) {
            return true;
        }

        if ($this->isNonPersistentGroup($group)) {
            return false;
        }

        try {
            $result = (bool) $this->connection->exists($id);

            $this->metrics->read($group);

            return $result;
        } catch (Throwable $exception) {
            $this->error($exception);
        }

        return false;
    }

    /**
     * Increment numeric cache item's value.
     *
     * To preserve the key's expiry Redis 6.0 and PhpRedis 5.3 or newer is required.
     *
     * @param  int|string  $key
     * @param  int  $offset
     * @param  string  $group
     * @return int|false
     */
    public function incr($key, int $offset = 1, string $group = 'default')
    {
        if (! $id = $this->id($key, $group)) {
            return false;
        }

        if ($this->isNonPersistentGroup($group)) {
            if (! $this->hasInMemory($id, $group)) {
                return false;
            }

            $value = $this->getFromMemory($id, $group);
            $value = $this->increment($value, $offset);

            $this->storeInMemory($id, $value, $group);

            return $value;
        }

        try {
            $value = $this->connection->get($id);

            $this->metrics->read($group);

            if ($value === false) {
                return false;
            }

            $value = $this->increment($value, $offset);
            $result = $this->connection->set($id, $value, ['KEEPTTL']);

            if ($result === false && $this->connection->lastErrorWasSyntaxError()) {
                $result = $this->connection->set($id, $value);
            }

            $this->metrics->write($group);

            if ($result) {
                $this->storeInMemory($id, $value, $group);
            }

            return $value;
        } catch (Throwable $exception) {
            $this->error($exception);

            return false;
        }
    }

    /**
     * Replaces the contents of the cache with new data.
     *
     * @param  int|string  $key
     * @param  mixed  $data
     * @param  string  $group
     * @param  int  $expire
     * @return bool
     */
    public function replace($key, $data, string $group = 'default', int $expire = 0): bool
    {
        if (! $id = $this->id($key, $group)) {
            return false;
        }

        if ($this->isNonPersistentGroup($group)) {
            if (! $this->hasInMemory($id, $group)) {
                return false;
            }

            $this->storeInMemory($id, $data, $group);

            return true;
        }

        try {
            $result = (bool) $this->write($id, $data, $group, $expire, 'XX');

            $this->metrics->write($group);

            if ($result) {
                $this->storeInMemory($id, $data, $group);
            }

            return $result;
        } catch (Throwable $exception) {
            $this->error($exception);

            return false;
        }
    }

    /**
     * Saves the data to the cache.
     *
     * @param  int|string  $key
     * @param  mixed  $data
     * @param  string  $group
     * @param  int  $expire
     * @return bool
     */
    public function set($key, $data, string $group = 'default', int $expire = 0): bool
    {
        if (! $id = $this->id($key, $group)) {
            return false;
        }

        if ($this->isNonPersistentGroup($group)) {
            $this->storeInMemory($id, $data, $group);

            return true;
        }

        try {
            if ($this->isAllOptionsId($id)) {
                return $this->syncAllOptions($id, $data);
            }

            $result = (bool) $this->write($id, $data, $group, $expire);

            $this->metrics->write($group);

            if ($result) {
                $this->storeInMemory($id, $data, $group);
            }

            return $result;
        } catch (Throwable $exception) {
            $this->error($exception);

            return false;
        }
    }

    /**
     * Sets multiple values to the cache in one call.
     *
     * @param  array<int|string, mixed>  $data
     * @param  string  $group
     * @param  int  $expire
     * @return array<int|string, bool>
     */
    public function set_multiple(array $data, string $group = 'default', int $expire = 0): array
    {
        $ids = [];

        if (empty($data)) {
            return [];
        }

        $data = array_filter($data, function ($key) use ($group, &$ids) {
            return ($id = $this->id($key, $group)) ? (bool) $ids[$key] = $id : false;
        }, ARRAY_FILTER_USE_KEY);

        if ($this->isNonPersistentGroup($group)) {
            $results = [];

            foreach ($data as $key => $value) {
                $results[$key] = true;
                $this->storeInMemory($ids[$key], $value, $group);
            }

            return $results;
        }

        try {
            $results = $this->multiwrite($data, $group, $expire);
        } catch (Throwable $exception) {
            $this->error($exception);

            return array_combine(array_keys($data), array_fill(0, count($data), false));
        }

        $this->metrics->write($group);

        foreach ($results as $key => $result) {
            if ($result['id'] && $result['response']) {
                $this->storeInMemory($result['id'], $data[$key], $group);
            }

            $results[$key] = $result['response'];
        }

        return $results;
    }

    /**
     * Switches the internal blog ID.
     *
     * @param  int $blog_id
     * @return bool
     */
    public function switch_to_blog(int $blog_id): bool
    {
        if ($this->isMultisite) {
            $this->setBlogId($blog_id);

            return true;
        }

        return false;
    }

    /**
     * Writes the given key to Redis and enforces the `maxttl` and `queryttl` options.
     *
     * @param  string  $id
     * @param  mixed  $data
     * @param  string  $group
     * @param  int  $expire
     * @param  string  $option
     * @return bool
     */
    protected function write(string $id, $data, string $group, int $expire = 0, $option = null): bool
    {
        if ($expire < 0) {
            $expire = 0;
        }

        $maxttl = array_key_exists($group, $this->queryGroups)
            ? min($this->config->queryttl, $this->config->maxttl)
            : $this->config->maxttl;

        if ($maxttl && ($expire === 0 || $expire > $maxttl)) {
            $expire = $maxttl;
        }

        if ($expire && $option) {
            return $this->connection->set($id, $data, [$option, 'EX' => $expire]);
        }

        if ($expire) {
            return $this->connection->set($id, $data, ['EX' => $expire]);
        }

        if ($option) {
            return $this->connection->set($id, $data, [$option]);
        }

        return $this->connection->set($id, $data);
    }

    /**
     * Writes the given keys to Redis and enforces the `maxttl` configuration option.
     *
     * @param  array<int|string, mixed>  $data
     * @param  int  $expire
     * @param  string  $option
     * @return array<int|string, array{id: string|false, response: mixed}>
     */
    protected function multiwrite(array $data, string $group, int $expire = 0, $option = null): array
    {
        if ($expire < 0) {
            $expire = 0;
        }

        $maxttl = array_key_exists($group, $this->queryGroups)
            ? $this->config->queryttl
            : $this->config->maxttl;

        if ($maxttl && ($expire === 0 || $expire > $maxttl)) {
            $expire = $maxttl;
        }

        $results = [];

        $pipe = $this->connection->pipeline();

        foreach ($data as $key => $value) {
            if (! $id = $this->id($key, $group)) {
                $results[$key] = ['id' => false, 'response' => false];
                continue;
            }

            $results[$key] = ['id' => $id, 'response' => false];

            if ($expire && $option) {
                $pipe->set($id, $value, [$option, 'EX' => $expire]);
                continue;
            }

            if ($expire) {
                $pipe->set($id, $value, ['EX' => $expire]);
                continue;
            }

            if ($option) {
                $pipe->set($id, $value, [$option]);
                continue;
            }

            $pipe->set($id, $value);
        }

        $keys = array_keys($results);

        foreach ($pipe->exec() as $i => $result) {
            $results[$keys[$i]]['response'] = $result;
        }

        return $results;
    }

    /**
     * Returns various information about the object cache.
     *
     * @return \RedisCachePro\Support\ObjectCacheInfo
     */
    public function info()
    {
        $server = $this->connection->memoize('info');

        $info = parent::info();
        $info->status = (bool) $this->connection->memoize('ping');
        $info->meta = array_filter([
            'Redis Version' => $server['redis_version'],
            'Redis Memory' => size_format($server['used_memory'] ?? 0, 2),
            'Redis Eviction' => $server['maxmemory_policy'] ?? null,
            'Cache' => (new ReflectionClass($this))->getShortName(),
            'Client' => (new ReflectionClass($this->client()))->getShortName(),
            'Connector' => (new ReflectionClass($this->config->connector))->getShortName(),
            'Connection' => (new ReflectionClass($this->connection))->getShortName(),
            'Logger' => (new ReflectionClass($this->log))->getShortName(),
        ]);

        return $info;
    }

    /**
     * Deletes keys matching given patterns.
     *
     * @internal
     * @param  string|string[]  $patterns
     * @param  ?string  $group
     * @return void
     */
    protected function deleteByPattern($patterns, ?string $group = null)
    {
        if ($this->config->group_flush === Configuration::GROUP_FLUSH_INCREMENTAL) {
            $this->deleteIncrementallyByPattern($patterns);

            return;
        }

        if (! is_array($patterns)) {
            $patterns = [$patterns];
        }

        $command = $this->config->async_flush ? 'unlink' : 'del';
        $script = file_get_contents(__DIR__ . "/scripts/{$this->config->group_flush}.lua");

        $results = $this->connection->withoutTimeout(function ($connection) use ($script, $patterns, $command) {
            return $connection->eval($script, array_merge($patterns, [$command]), count($patterns));
        });

        $writes = array_sum(array_map(static function ($result) {
            return is_array($result) ? array_sum($result) : $result;
        }, is_array($results) ? $results : [$results]));

        $this->metrics->write($group);
        $this->metrics->storeWrites += (int) --$writes;
    }

    /**
     * Deletes keys matching given patterns non-atomically.
     *
     * @internal
     * @param  string|string[]  $patterns
     * @param  ?string  $group
     * @return void
     */
    protected function deleteIncrementallyByPattern($patterns, ?string $group = null)
    {
        if (! is_array($patterns)) {
            $patterns = [$patterns];
        }

        $command = $this->config->async_flush ? 'unlink' : 'del';

        foreach ($patterns as $pattern) {
            foreach ($this->connection->listKeys($pattern) as $keys) {
                $this->connection->{$command}($keys);

                $this->metrics->write($group);
            }
        }
    }
}
