vendor/symfony/cache/Traits/RedisTrait.php line 94

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Cache\Traits;
  11. use Predis\Command\Redis\UNLINK;
  12. use Predis\Connection\Aggregate\ClusterInterface;
  13. use Predis\Connection\Aggregate\RedisCluster;
  14. use Predis\Connection\Aggregate\ReplicationInterface;
  15. use Predis\Connection\Cluster\ClusterInterface as Predis2ClusterInterface;
  16. use Predis\Connection\Cluster\RedisCluster as Predis2RedisCluster;
  17. use Predis\Response\ErrorInterface;
  18. use Predis\Response\Status;
  19. use Symfony\Component\Cache\Exception\CacheException;
  20. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  21. use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
  22. use Symfony\Component\Cache\Marshaller\MarshallerInterface;
  23. /**
  24.  * @author Aurimas Niekis <aurimas@niekis.lt>
  25.  * @author Nicolas Grekas <p@tchwork.com>
  26.  *
  27.  * @internal
  28.  */
  29. trait RedisTrait
  30. {
  31.     private static $defaultConnectionOptions = [
  32.         'class' => null,
  33.         'persistent' => 0,
  34.         'persistent_id' => null,
  35.         'timeout' => 30,
  36.         'read_timeout' => 0,
  37.         'retry_interval' => 0,
  38.         'tcp_keepalive' => 0,
  39.         'lazy' => null,
  40.         'redis_cluster' => false,
  41.         'redis_sentinel' => null,
  42.         'dbindex' => 0,
  43.         'failover' => 'none',
  44.         'ssl' => null// see https://php.net/context.ssl
  45.     ];
  46.     private $redis;
  47.     private $marshaller;
  48.     /**
  49.      * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis
  50.      */
  51.     private function init($redisstring $namespaceint $defaultLifetime, ?MarshallerInterface $marshaller)
  52.     {
  53.         parent::__construct($namespace$defaultLifetime);
  54.         if (preg_match('#[^-+_.A-Za-z0-9]#'$namespace$match)) {
  55.             throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.'$match[0]));
  56.         }
  57.         if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \RedisCluster && !$redis instanceof \Predis\ClientInterface && !$redis instanceof RedisProxy && !$redis instanceof RedisClusterProxy) {
  58.             throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.'__METHOD__get_debug_type($redis)));
  59.         }
  60.         if ($redis instanceof \Predis\ClientInterface && $redis->getOptions()->exceptions) {
  61.             $options = clone $redis->getOptions();
  62.             \Closure::bind(function () { $this->options['exceptions'] = false; }, $options$options)();
  63.             $redis = new $redis($redis->getConnection(), $options);
  64.         }
  65.         $this->redis $redis;
  66.         $this->marshaller $marshaller ?? new DefaultMarshaller();
  67.     }
  68.     /**
  69.      * Creates a Redis connection using a DSN configuration.
  70.      *
  71.      * Example DSN:
  72.      *   - redis://localhost
  73.      *   - redis://example.com:1234
  74.      *   - redis://secret@example.com/13
  75.      *   - redis:///var/run/redis.sock
  76.      *   - redis://secret@/var/run/redis.sock/13
  77.      *
  78.      * @param array $options See self::$defaultConnectionOptions
  79.      *
  80.      * @return \Redis|\RedisArray|\RedisCluster|RedisClusterProxy|RedisProxy|\Predis\ClientInterface According to the "class" option
  81.      *
  82.      * @throws InvalidArgumentException when the DSN is invalid
  83.      */
  84.     public static function createConnection(string $dsn, array $options = [])
  85.     {
  86.         if (str_starts_with($dsn'redis:')) {
  87.             $scheme 'redis';
  88.         } elseif (str_starts_with($dsn'rediss:')) {
  89.             $scheme 'rediss';
  90.         } else {
  91.             throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');
  92.         }
  93.         if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
  94.             throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');
  95.         }
  96.         $params preg_replace_callback('#^'.$scheme.':(//)?(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
  97.             if (isset($m[2])) {
  98.                 $auth rawurldecode($m[2]);
  99.                 if ('' === $auth) {
  100.                     $auth null;
  101.                 }
  102.             }
  103.             return 'file:'.($m[1] ?? '');
  104.         }, $dsn);
  105.         if (false === $params parse_url($params)) {
  106.             throw new InvalidArgumentException('Invalid Redis DSN.');
  107.         }
  108.         $query $hosts = [];
  109.         $tls 'rediss' === $scheme;
  110.         $tcpScheme $tls 'tls' 'tcp';
  111.         if (isset($params['query'])) {
  112.             parse_str($params['query'], $query);
  113.             if (isset($query['host'])) {
  114.                 if (!\is_array($hosts $query['host'])) {
  115.                     throw new InvalidArgumentException('Invalid Redis DSN: query parameter "host" must be an array.');
  116.                 }
  117.                 foreach ($hosts as $host => $parameters) {
  118.                     if (\is_string($parameters)) {
  119.                         parse_str($parameters$parameters);
  120.                     }
  121.                     if (false === $i strrpos($host':')) {
  122.                         $hosts[$host] = ['scheme' => $tcpScheme'host' => $host'port' => 6379] + $parameters;
  123.                     } elseif ($port = (int) substr($host$i)) {
  124.                         $hosts[$host] = ['scheme' => $tcpScheme'host' => substr($host0$i), 'port' => $port] + $parameters;
  125.                     } else {
  126.                         $hosts[$host] = ['scheme' => 'unix''path' => substr($host0$i)] + $parameters;
  127.                     }
  128.                 }
  129.                 $hosts array_values($hosts);
  130.             }
  131.         }
  132.         if (isset($params['host']) || isset($params['path'])) {
  133.             if (!isset($params['dbindex']) && isset($params['path'])) {
  134.                 if (preg_match('#/(\d+)?$#'$params['path'], $m)) {
  135.                     $params['dbindex'] = $m[1] ?? $query['dbindex'] ?? '0';
  136.                     $params['path'] = substr($params['path'], 0, -\strlen($m[0]));
  137.                 } elseif (isset($params['host'])) {
  138.                     throw new InvalidArgumentException('Invalid Redis DSN: parameter "dbindex" must be a number.');
  139.                 }
  140.             }
  141.             if (isset($params['host'])) {
  142.                 array_unshift($hosts, ['scheme' => $tcpScheme'host' => $params['host'], 'port' => $params['port'] ?? 6379]);
  143.             } else {
  144.                 array_unshift($hosts, ['scheme' => 'unix''path' => $params['path']]);
  145.             }
  146.         }
  147.         if (!$hosts) {
  148.             throw new InvalidArgumentException('Invalid Redis DSN: missing host.');
  149.         }
  150.         if (isset($params['dbindex'], $query['dbindex']) && $params['dbindex'] !== $query['dbindex']) {
  151.             throw new InvalidArgumentException('Invalid Redis DSN: path and query "dbindex" parameters mismatch.');
  152.         }
  153.         $params += $query $options self::$defaultConnectionOptions;
  154.         if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class)) {
  155.             throw new CacheException('Redis Sentinel support requires the "predis/predis" package or the "redis" extension v5.2 or higher.');
  156.         }
  157.         if (isset($params['lazy'])) {
  158.             $params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
  159.         }
  160.         $params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);
  161.         if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
  162.             throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
  163.         }
  164.         if (null === $params['class'] && \extension_loaded('redis')) {
  165.             $class $params['redis_cluster'] ? \RedisCluster::class : (\count($hosts) && !isset($params['redis_sentinel']) ? \RedisArray::class : \Redis::class);
  166.         } else {
  167.             $class $params['class'] ?? \Predis\Client::class;
  168.             if (isset($params['redis_sentinel']) && !is_a($class\Predis\Client::class, true) && !class_exists(\RedisSentinel::class)) {
  169.                 throw new CacheException(sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and ext-redis >= 5.2 not found.'$class));
  170.             }
  171.         }
  172.         if (is_a($class\Redis::class, true)) {
  173.             $connect $params['persistent'] || $params['persistent_id'] ? 'pconnect' 'connect';
  174.             $redis = new $class();
  175.             $initializer = static function ($redis) use ($connect$params$auth$hosts$tls) {
  176.                 $hostIndex 0;
  177.                 do {
  178.                     $host $hosts[$hostIndex]['host'] ?? $hosts[$hostIndex]['path'];
  179.                     $port $hosts[$hostIndex]['port'] ?? 0;
  180.                     $passAuth \defined('Redis::OPT_NULL_MULTIBULK_AS_NULL') && isset($params['auth']);
  181.                     $address false;
  182.                     if (isset($hosts[$hostIndex]['host']) && $tls) {
  183.                         $host 'tls://'.$host;
  184.                     }
  185.                     if (!isset($params['redis_sentinel'])) {
  186.                         break;
  187.                     }
  188.                     if (version_compare(phpversion('redis'), '6.0.0''>=')) {
  189.                         $options = [
  190.                             'host' => $host,
  191.                             'port' => $port,
  192.                             'connectTimeout' => (float) $params['timeout'],
  193.                             'persistent' => $params['persistent_id'],
  194.                             'retryInterval' => (int) $params['retry_interval'],
  195.                             'readTimeout' => (float) $params['read_timeout'],
  196.                         ];
  197.                         if ($passAuth) {
  198.                             $options['auth'] = $params['auth'];
  199.                         }
  200.                         $sentinel = new \RedisSentinel($options);
  201.                     } else {
  202.                         $extra $passAuth ? [$params['auth']] : [];
  203.                         $sentinel = new \RedisSentinel($host$port$params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra);
  204.                     }
  205.                     try {
  206.                         if ($address $sentinel->getMasterAddrByName($params['redis_sentinel'])) {
  207.                             [$host$port] = $address;
  208.                         }
  209.                     } catch (\RedisException $e) {
  210.                     }
  211.                 } while (++$hostIndex \count($hosts) && !$address);
  212.                 if (isset($params['redis_sentinel']) && !$address) {
  213.                     throw new InvalidArgumentException(sprintf('Failed to retrieve master information from sentinel "%s".'$params['redis_sentinel']));
  214.                 }
  215.                 try {
  216.                     $extra = [
  217.                         'stream' => $params['ssl'] ?? null,
  218.                     ];
  219.                     $booleanStreamOptions = [
  220.                         'allow_self_signed',
  221.                         'capture_peer_cert',
  222.                         'capture_peer_cert_chain',
  223.                         'disable_compression',
  224.                         'SNI_enabled',
  225.                         'verify_peer',
  226.                         'verify_peer_name',
  227.                     ];
  228.                     foreach ($extra['stream'] ?? [] as $streamOption => $value) {
  229.                         if (\in_array($streamOption$booleanStreamOptionstrue) && \is_string($value)) {
  230.                             $extra['stream'][$streamOption] = filter_var($value\FILTER_VALIDATE_BOOL);
  231.                         }
  232.                     }
  233.                     if (isset($params['auth'])) {
  234.                         $extra['auth'] = $params['auth'];
  235.                     }
  236.                     @$redis->{$connect}($host$port$params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [$extra] : []);
  237.                     set_error_handler(function ($type$msg) use (&$error) { $error $msg; });
  238.                     try {
  239.                         $isConnected $redis->isConnected();
  240.                     } finally {
  241.                         restore_error_handler();
  242.                     }
  243.                     if (!$isConnected) {
  244.                         $error preg_match('/^Redis::p?connect\(\): (.*)/'$error ?? $redis->getLastError() ?? ''$error) ? sprintf(' (%s)'$error[1]) : '';
  245.                         throw new InvalidArgumentException('Redis connection failed: '.$error.'.');
  246.                     }
  247.                     if ((null !== $auth && !$redis->auth($auth))
  248.                         // Due to a bug in phpredis we must always select the dbindex if persistent pooling is enabled
  249.                         // @see https://github.com/phpredis/phpredis/issues/1920
  250.                         // @see https://github.com/symfony/symfony/issues/51578
  251.                         || (($params['dbindex'] || ('pconnect' === $connect && '0' !== \ini_get('redis.pconnect.pooling_enabled'))) && !$redis->select($params['dbindex']))
  252.                     ) {
  253.                         $e preg_replace('/^ERR /'''$redis->getLastError());
  254.                         throw new InvalidArgumentException('Redis connection failed: '.$e.'.');
  255.                     }
  256.                     if ($params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
  257.                         $redis->setOption(\Redis::OPT_TCP_KEEPALIVE$params['tcp_keepalive']);
  258.                     }
  259.                 } catch (\RedisException $e) {
  260.                     throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
  261.                 }
  262.                 return true;
  263.             };
  264.             if ($params['lazy']) {
  265.                 $redis = new RedisProxy($redis$initializer);
  266.             } else {
  267.                 $initializer($redis);
  268.             }
  269.         } elseif (is_a($class\RedisArray::class, true)) {
  270.             foreach ($hosts as $i => $host) {
  271.                 switch ($host['scheme']) {
  272.                     case 'tcp'$hosts[$i] = $host['host'].':'.$host['port']; break;
  273.                     case 'tls'$hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break;
  274.                     default: $hosts[$i] = $host['path'];
  275.                 }
  276.             }
  277.             $params['lazy_connect'] = $params['lazy'] ?? true;
  278.             $params['connect_timeout'] = $params['timeout'];
  279.             try {
  280.                 $redis = new $class($hosts$params);
  281.             } catch (\RedisClusterException $e) {
  282.                 throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
  283.             }
  284.             if ($params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
  285.                 $redis->setOption(\Redis::OPT_TCP_KEEPALIVE$params['tcp_keepalive']);
  286.             }
  287.         } elseif (is_a($class\RedisCluster::class, true)) {
  288.             $initializer = static function () use ($class$params$hosts) {
  289.                 foreach ($hosts as $i => $host) {
  290.                     switch ($host['scheme']) {
  291.                         case 'tcp'$hosts[$i] = $host['host'].':'.$host['port']; break;
  292.                         case 'tls'$hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break;
  293.                         default: $hosts[$i] = $host['path'];
  294.                     }
  295.                 }
  296.                 try {
  297.                     $redis = new $class(null$hosts$params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
  298.                 } catch (\RedisClusterException $e) {
  299.                     throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
  300.                 }
  301.                 if ($params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
  302.                     $redis->setOption(\Redis::OPT_TCP_KEEPALIVE$params['tcp_keepalive']);
  303.                 }
  304.                 switch ($params['failover']) {
  305.                     case 'error'$redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER\RedisCluster::FAILOVER_ERROR); break;
  306.                     case 'distribute'$redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER\RedisCluster::FAILOVER_DISTRIBUTE); break;
  307.                     case 'slaves'$redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER\RedisCluster::FAILOVER_DISTRIBUTE_SLAVES); break;
  308.                 }
  309.                 return $redis;
  310.             };
  311.             $redis $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer();
  312.         } elseif (is_a($class\Predis\ClientInterface::class, true)) {
  313.             if ($params['redis_cluster']) {
  314.                 $params['cluster'] = 'redis';
  315.             } elseif (isset($params['redis_sentinel'])) {
  316.                 $params['replication'] = 'sentinel';
  317.                 $params['service'] = $params['redis_sentinel'];
  318.             }
  319.             $params += ['parameters' => []];
  320.             $params['parameters'] += [
  321.                 'persistent' => $params['persistent'],
  322.                 'timeout' => $params['timeout'],
  323.                 'read_write_timeout' => $params['read_timeout'],
  324.                 'tcp_nodelay' => true,
  325.             ];
  326.             if ($params['dbindex']) {
  327.                 $params['parameters']['database'] = $params['dbindex'];
  328.             }
  329.             if (null !== $auth) {
  330.                 $params['parameters']['password'] = $auth;
  331.             }
  332.             if (isset($params['ssl'])) {
  333.                 foreach ($hosts as $i => $host) {
  334.                     if (!isset($host['ssl'])) {
  335.                         $hosts[$i]['ssl'] = $params['ssl'];
  336.                     }
  337.                 }
  338.             }
  339.             if (=== \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) {
  340.                 $hosts $hosts[0];
  341.             } elseif (\in_array($params['failover'], ['slaves''distribute'], true) && !isset($params['replication'])) {
  342.                 $params['replication'] = true;
  343.                 $hosts[0] += ['alias' => 'master'];
  344.             }
  345.             $params['exceptions'] = false;
  346.             $redis = new $class($hostsarray_diff_key($paramsself::$defaultConnectionOptions));
  347.             if (isset($params['redis_sentinel'])) {
  348.                 $redis->getConnection()->setSentinelTimeout($params['timeout']);
  349.             }
  350.         } elseif (class_exists($classfalse)) {
  351.             throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis", "RedisArray", "RedisCluster" nor "Predis\ClientInterface".'$class));
  352.         } else {
  353.             throw new InvalidArgumentException(sprintf('Class "%s" does not exist.'$class));
  354.         }
  355.         return $redis;
  356.     }
  357.     protected function doFetch(array $ids)
  358.     {
  359.         if (!$ids) {
  360.             return [];
  361.         }
  362.         $result = [];
  363.         if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) {
  364.             $values $this->pipeline(function () use ($ids) {
  365.                 foreach ($ids as $id) {
  366.                     yield 'get' => [$id];
  367.                 }
  368.             });
  369.         } else {
  370.             $values $this->redis->mget($ids);
  371.             if (!\is_array($values) || \count($values) !== \count($ids)) {
  372.                 return [];
  373.             }
  374.             $values array_combine($ids$values);
  375.         }
  376.         foreach ($values as $id => $v) {
  377.             if ($v) {
  378.                 $result[$id] = $this->marshaller->unmarshall($v);
  379.             }
  380.         }
  381.         return $result;
  382.     }
  383.     protected function doHave(string $id)
  384.     {
  385.         return (bool) $this->redis->exists($id);
  386.     }
  387.     protected function doClear(string $namespace)
  388.     {
  389.         if ($this->redis instanceof \Predis\ClientInterface) {
  390.             $prefix $this->redis->getOptions()->prefix $this->redis->getOptions()->prefix->getPrefix() : '';
  391.             $prefixLen \strlen($prefix ?? '');
  392.         }
  393.         $cleared true;
  394.         $hosts $this->getHosts();
  395.         $host reset($hosts);
  396.         if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
  397.             // Predis supports info command only on the master in replication environments
  398.             $hosts = [$host->getClientFor('master')];
  399.         }
  400.         foreach ($hosts as $host) {
  401.             if (!isset($namespace[0])) {
  402.                 $cleared $host->flushDb() && $cleared;
  403.                 continue;
  404.             }
  405.             $info $host->info('Server');
  406.             $info = !$info instanceof ErrorInterface $info['Server'] ?? $info : ['redis_version' => '2.0'];
  407.             if (!$host instanceof \Predis\ClientInterface) {
  408.                 $prefix \defined('Redis::SCAN_PREFIX') && (\Redis::SCAN_PREFIX $host->getOption(\Redis::OPT_SCAN)) ? '' $host->getOption(\Redis::OPT_PREFIX);
  409.                 $prefixLen \strlen($host->getOption(\Redis::OPT_PREFIX) ?? '');
  410.             }
  411.             $pattern $prefix.$namespace.'*';
  412.             if (!version_compare($info['redis_version'], '2.8''>=')) {
  413.                 // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
  414.                 // can hang your server when it is executed against large databases (millions of items).
  415.                 // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above.
  416.                 $unlink version_compare($info['redis_version'], '4.0''>=') ? 'UNLINK' 'DEL';
  417.                 $args $this->redis instanceof \Predis\ClientInterface ? [0$pattern] : [[$pattern], 0];
  418.                 $cleared $host->eval("local keys=redis.call('KEYS',ARGV[1]) for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1"$args[0], $args[1]) && $cleared;
  419.                 continue;
  420.             }
  421.             $cursor null;
  422.             do {
  423.                 $keys $host instanceof \Predis\ClientInterface $host->scan($cursor'MATCH'$pattern'COUNT'1000) : $host->scan($cursor$pattern1000);
  424.                 if (isset($keys[1]) && \is_array($keys[1])) {
  425.                     $cursor $keys[0];
  426.                     $keys $keys[1];
  427.                 }
  428.                 if ($keys) {
  429.                     if ($prefixLen) {
  430.                         foreach ($keys as $i => $key) {
  431.                             $keys[$i] = substr($key$prefixLen);
  432.                         }
  433.                     }
  434.                     $this->doDelete($keys);
  435.                 }
  436.             } while ($cursor);
  437.         }
  438.         return $cleared;
  439.     }
  440.     protected function doDelete(array $ids)
  441.     {
  442.         if (!$ids) {
  443.             return true;
  444.         }
  445.         if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) {
  446.             static $del;
  447.             $del $del ?? (class_exists(UNLINK::class) ? 'unlink' 'del');
  448.             $this->pipeline(function () use ($ids$del) {
  449.                 foreach ($ids as $id) {
  450.                     yield $del => [$id];
  451.                 }
  452.             })->rewind();
  453.         } else {
  454.             static $unlink true;
  455.             if ($unlink) {
  456.                 try {
  457.                     $unlink false !== $this->redis->unlink($ids);
  458.                 } catch (\Throwable $e) {
  459.                     $unlink false;
  460.                 }
  461.             }
  462.             if (!$unlink) {
  463.                 $this->redis->del($ids);
  464.             }
  465.         }
  466.         return true;
  467.     }
  468.     protected function doSave(array $valuesint $lifetime)
  469.     {
  470.         if (!$values $this->marshaller->marshall($values$failed)) {
  471.             return $failed;
  472.         }
  473.         $results $this->pipeline(function () use ($values$lifetime) {
  474.             foreach ($values as $id => $value) {
  475.                 if (>= $lifetime) {
  476.                     yield 'set' => [$id$value];
  477.                 } else {
  478.                     yield 'setEx' => [$id$lifetime$value];
  479.                 }
  480.             }
  481.         });
  482.         foreach ($results as $id => $result) {
  483.             if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
  484.                 $failed[] = $id;
  485.             }
  486.         }
  487.         return $failed;
  488.     }
  489.     private function pipeline(\Closure $generator, ?object $redis null): \Generator
  490.     {
  491.         $ids = [];
  492.         $redis $redis ?? $this->redis;
  493.         if ($redis instanceof RedisClusterProxy || $redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
  494.             // phpredis & predis don't support pipelining with RedisCluster
  495.             // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
  496.             // see https://github.com/nrk/predis/issues/267#issuecomment-123781423
  497.             $results = [];
  498.             foreach ($generator() as $command => $args) {
  499.                 $results[] = $redis->{$command}(...$args);
  500.                 $ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface $args[2] : $args[1][0]) : $args[0];
  501.             }
  502.         } elseif ($redis instanceof \Predis\ClientInterface) {
  503.             $results $redis->pipeline(static function ($redis) use ($generator, &$ids) {
  504.                 foreach ($generator() as $command => $args) {
  505.                     $redis->{$command}(...$args);
  506.                     $ids[] = 'eval' === $command $args[2] : $args[0];
  507.                 }
  508.             });
  509.         } elseif ($redis instanceof \RedisArray) {
  510.             $connections $results $ids = [];
  511.             foreach ($generator() as $command => $args) {
  512.                 $id 'eval' === $command $args[1][0] : $args[0];
  513.                 if (!isset($connections[$h $redis->_target($id)])) {
  514.                     $connections[$h] = [$redis->_instance($h), -1];
  515.                     $connections[$h][0]->multi(\Redis::PIPELINE);
  516.                 }
  517.                 $connections[$h][0]->{$command}(...$args);
  518.                 $results[] = [$h, ++$connections[$h][1]];
  519.                 $ids[] = $id;
  520.             }
  521.             foreach ($connections as $h => $c) {
  522.                 $connections[$h] = $c[0]->exec();
  523.             }
  524.             foreach ($results as $k => [$h$c]) {
  525.                 $results[$k] = $connections[$h][$c];
  526.             }
  527.         } else {
  528.             $redis->multi(\Redis::PIPELINE);
  529.             foreach ($generator() as $command => $args) {
  530.                 $redis->{$command}(...$args);
  531.                 $ids[] = 'eval' === $command $args[1][0] : $args[0];
  532.             }
  533.             $results $redis->exec();
  534.         }
  535.         if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) {
  536.             $e = new \RedisException($redis->getLastError());
  537.             $results array_map(function ($v) use ($e) { return false === $v $e $v; }, (array) $results);
  538.         }
  539.         if (\is_bool($results)) {
  540.             return;
  541.         }
  542.         foreach ($ids as $k => $id) {
  543.             yield $id => $results[$k];
  544.         }
  545.     }
  546.     private function getHosts(): array
  547.     {
  548.         $hosts = [$this->redis];
  549.         if ($this->redis instanceof \Predis\ClientInterface) {
  550.             $connection $this->redis->getConnection();
  551.             if (($connection instanceof ClusterInterface || $connection instanceof Predis2ClusterInterface) && $connection instanceof \Traversable) {
  552.                 $hosts = [];
  553.                 foreach ($connection as $c) {
  554.                     $hosts[] = new \Predis\Client($c);
  555.                 }
  556.             }
  557.         } elseif ($this->redis instanceof \RedisArray) {
  558.             $hosts = [];
  559.             foreach ($this->redis->_hosts() as $host) {
  560.                 $hosts[] = $this->redis->_instance($host);
  561.             }
  562.         } elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) {
  563.             $hosts = [];
  564.             foreach ($this->redis->_masters() as $host) {
  565.                 $hosts[] = new RedisClusterNodeProxy($host$this->redis);
  566.             }
  567.         }
  568.         return $hosts;
  569.     }
  570. }