Вход Регистрация
Файл: vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php
Строк: 377
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SymfonyComponentCacheAdapter;

use 
PredisConnectionAggregateClusterInterface;
use 
PredisConnectionAggregatePredisCluster;
use 
PredisConnectionAggregateReplicationInterface;
use 
PredisResponseErrorInterface;
use 
PredisResponseStatus;
use 
RelayRelay;
use 
SymfonyComponentCacheCacheItem;
use 
SymfonyComponentCacheExceptionInvalidArgumentException;
use 
SymfonyComponentCacheExceptionLogicException;
use 
SymfonyComponentCacheMarshallerDeflateMarshaller;
use 
SymfonyComponentCacheMarshallerMarshallerInterface;
use 
SymfonyComponentCacheMarshallerTagAwareMarshaller;
use 
SymfonyComponentCacheTraitsRedisTrait;

/**
 * Stores tag id <> cache id relationship as a Redis Set.
 *
 * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even
 * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache
 * relationship survives eviction (cache cleanup when Redis runs out of memory).
 *
 * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up
 *
 * Design limitations:
 *  - Max 4 billion cache keys per cache tag as limited by Redis Set datatype.
 *    E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also.
 *
 * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies.
 * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author André Rømcke <andre.romcke+symfony@gmail.com>
 */
class RedisTagAwareAdapter extends AbstractTagAwareAdapter
{
    use 
RedisTrait;

    
/**
     * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are
     * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements.
     */
    
private const DEFAULT_CACHE_TTL 8640000;

    
/**
     * detected eviction policy used on Redis server.
     */
    
private string $redisEvictionPolicy;
    private 
string $namespace;

    public function 
__construct(Redis|Relay|RedisArray|RedisCluster|PredisClientInterface $redisstring $namespace ''int $defaultLifetime 0, ?MarshallerInterface $marshaller null)
    {
        if (
$redis instanceof PredisClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) {
            throw new 
InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.'PredisCluster::class, get_debug_type($redis->getConnection())));
        }

        
$isRelay $redis instanceof Relay;
        if (
$isRelay || defined('Redis::OPT_COMPRESSION') && in_array($redis::class, [Redis::class, RedisArray::class, RedisCluster::class], true)) {
            
$compression $redis->getOption($isRelay Relay::OPT_COMPRESSION Redis::OPT_COMPRESSION);

            foreach (
is_array($compression) ? $compression : [$compression] as $c) {
                if (
$isRelay Relay::COMPRESSION_NONE Redis::COMPRESSION_NONE !== $c) {
                    throw new 
InvalidArgumentException(sprintf('redis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class));
                }
            }
        }

        
$this->init($redis$namespace$defaultLifetime, new TagAwareMarshaller($marshaller));
        
$this->namespace $namespace;
    }

    protected function 
doSave(array $valuesint $lifetime, array $addTagData = [], array $delTagData = []): array
    {
        
$eviction $this->getRedisEvictionPolicy();
        if (
'noeviction' !== $eviction && !str_starts_with($eviction'volatile-')) {
            throw new 
LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.'$eviction));
        }

        
// serialize values
        
if (!$serialized $this->marshaller->marshall($values$failed)) {
            return 
$failed;
        }

        
// While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op
        
$results $this->pipeline(static function () use ($serialized$lifetime$addTagData$delTagData$failed) {
            
// Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one
            
foreach ($serialized as $id => $value) {
                yield 
'setEx' => [
                    
$id,
                    
>= $lifetime self::DEFAULT_CACHE_TTL $lifetime,
                    
$value,
                ];
            }

            
// Add and Remove Tags
            
foreach ($addTagData as $tagId => $ids) {
                if (!
$failed || $ids array_diff($ids$failed)) {
                    yield 
'sAdd' => array_merge([$tagId], $ids);
                }
            }

            foreach (
$delTagData as $tagId => $ids) {
                if (!
$failed || $ids array_diff($ids$failed)) {
                    yield 
'sRem' => array_merge([$tagId], $ids);
                }
            }
        });

        foreach (
$results as $id => $result) {
            
// Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not
            
if (is_numeric($result)) {
                continue;
            }
            
// setEx results
            
if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
                
$failed[] = $id;
            }
        }

        return 
$failed;
    }

    protected function 
doDeleteYieldTags(array $ids): iterable
    
{
        
$lua = <<<'EOLUA'
            local v = redis.call('GET', KEYS[1])
            local e = redis.pcall('UNLINK', KEYS[1])

            if type(e) ~= 'number' then
                redis.call('DEL', KEYS[1])
            end

            if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then
                return ''
            end

            return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536)
EOLUA;

        
$results $this->pipeline(function () use ($ids$lua) {
            foreach (
$ids as $id) {
                yield 
'eval' => $this->redis instanceof PredisClientInterface ? [$lua1$id] : [$lua, [$id], 1];
            }
        });

        foreach (
$results as $id => $result) {
            if (
$result instanceof RedisException || $result instanceof RelayException || $result instanceof ErrorInterface) {
                
CacheItem::log($this->logger'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($idstrlen($this->namespace)), 'exception' => $result]);

                continue;
            }

            try {
                yield 
$id => !is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
            } catch (
Exception) {
                yield 
$id => [];
            }
        }
    }

    protected function 
doDeleteTagRelations(array $tagData): bool
    
{
        
$results $this->pipeline(static function () use ($tagData) {
            foreach (
$tagData as $tagId => $idList) {
                
array_unshift($idList$tagId);
                yield 
'sRem' => $idList;
            }
        });
        foreach (
$results as $result) {
            
// no-op
        
}

        return 
true;
    }

    protected function 
doInvalidate(array $tagIds): bool
    
{
        
// This script scans the set of items linked to tag: it empties the set
        // and removes the linked items. When the set is still not empty after
        // the scan, it means we're in cluster mode and that the linked items
        // are on other nodes: we move the links to a temporary set and we
        // garbage collect that set from the client side.

        
$lua = <<<'EOLUA'
            redis.replicate_commands()

            local cursor = '0'
            local id = KEYS[1]
            repeat
                local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000);
                cursor = result[1];
                local rems = {}

                for _, v in ipairs(result[2]) do
                    local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v)
                    if ok then
                        table.insert(rems, v)
                    end
                end
                if 0 < #rems then
                    redis.call('SREM', id, unpack(rems))
                end
            until '0' == cursor;

            redis.call('SUNIONSTORE', '{'..id..'}'..id, id)
            redis.call('DEL', id)

            return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000)
EOLUA;

        
$results $this->pipeline(function () use ($tagIds$lua) {
            if (
$this->redis instanceof PredisClientInterface) {
                
$prefix $this->redis->getOptions()->prefix $this->redis->getOptions()->prefix->getPrefix() : '';
            } elseif (
is_array($prefix $this->redis->getOption($this->redis instanceof Relay Relay::OPT_PREFIX Redis::OPT_PREFIX) ?? '')) {
                
$prefix current($prefix);
            }

            foreach (
$tagIds as $id) {
                yield 
'eval' => $this->redis instanceof PredisClientInterface ? [$lua1$id$prefix] : [$lua, [$id$prefix], 1];
            }
        });

        
$lua = <<<'EOLUA'
            redis.replicate_commands()

            local id = KEYS[1]
            local cursor = table.remove(ARGV)
            redis.call('SREM', '{'..id..'}'..id, unpack(ARGV))

            return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000)
EOLUA;

        
$success true;
        foreach (
$results as $id => $values) {
            if (
$values instanceof RedisException || $values instanceof RelayException || $values instanceof ErrorInterface) {
                
CacheItem::log($this->logger'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($idstrlen($this->namespace)), 'exception' => $values]);
                
$success false;

                continue;
            }

            [
$cursor$ids] = $values;

            while (
$ids || '0' !== $cursor) {
                
$this->doDelete($ids);

                
$evalArgs = [$id$cursor];
                
array_splice($evalArgs10$ids);

                if (
$this->redis instanceof PredisClientInterface) {
                    
array_unshift($evalArgs$lua1);
                } else {
                    
$evalArgs = [$lua$evalArgs1];
                }

                
$results $this->pipeline(function () use ($evalArgs) {
                    yield 
'eval' => $evalArgs;
                });

                foreach (
$results as [$cursor$ids]) {
                    
// no-op
                
}
            }
        }

        return 
$success;
    }

    private function 
getRedisEvictionPolicy(): string
    
{
        if (isset(
$this->redisEvictionPolicy)) {
            return 
$this->redisEvictionPolicy;
        }

        
$hosts $this->getHosts();
        
$host reset($hosts);
        if (
$host instanceof PredisClient && $host->getConnection() instanceof ReplicationInterface) {
            
// Predis supports info command only on the master in replication environments
            
$hosts = [$host->getClientFor('master')];
        }

        foreach (
$hosts as $host) {
            
$info $host->info('Memory');

            if (
false === $info || null === $info || $info instanceof ErrorInterface) {
                continue;
            }

            
$info $info['Memory'] ?? $info;

            return 
$this->redisEvictionPolicy $info['maxmemory_policy'] ?? '';
        }

        return 
$this->redisEvictionPolicy '';
    }
}
Онлайн: 0
Реклама