8889841cEvents/CommandExecuted.php000066600000002245150544634600011604 0ustar00time = $time; $this->command = $command; $this->parameters = $parameters; $this->connection = $connection; $this->connectionName = $connection->getName(); } } LICENSE.md000066600000002063150544634600006164 0ustar00The MIT License (MIT) Copyright (c) Taylor Otwell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. RedisServiceProvider.php000066600000001616150544634600011376 0ustar00app->singleton('redis', function ($app) { $config = $app->make('config')->get('database.redis', []); return new RedisManager($app, Arr::pull($config, 'client', 'phpredis'), $config); }); $this->app->bind('redis.connection', function ($app) { return $app['redis']->connection(); }); } /** * Get the services provided by the provider. * * @return array */ public function provides() { return ['redis', 'redis.connection']; } } RedisManager.php000066600000015015150544634600007633 0ustar00app = $app; $this->driver = $driver; $this->config = $config; } /** * Get a Redis connection by name. * * @param string|null $name * @return \Illuminate\Redis\Connections\Connection */ public function connection($name = null) { $name = $name ?: 'default'; if (isset($this->connections[$name])) { return $this->connections[$name]; } return $this->connections[$name] = $this->configure( $this->resolve($name), $name ); } /** * Resolve the given connection by name. * * @param string|null $name * @return \Illuminate\Redis\Connections\Connection * * @throws \InvalidArgumentException */ public function resolve($name = null) { $name = $name ?: 'default'; $options = $this->config['options'] ?? []; if (isset($this->config[$name])) { return $this->connector()->connect( $this->parseConnectionConfiguration($this->config[$name]), array_merge(Arr::except($options, 'parameters'), ['parameters' => Arr::get($options, 'parameters.'.$name, Arr::get($options, 'parameters', []))]) ); } if (isset($this->config['clusters'][$name])) { return $this->resolveCluster($name); } throw new InvalidArgumentException("Redis connection [{$name}] not configured."); } /** * Resolve the given cluster connection by name. * * @param string $name * @return \Illuminate\Redis\Connections\Connection */ protected function resolveCluster($name) { return $this->connector()->connectToCluster( array_map(function ($config) { return $this->parseConnectionConfiguration($config); }, $this->config['clusters'][$name]), $this->config['clusters']['options'] ?? [], $this->config['options'] ?? [] ); } /** * Configure the given connection to prepare it for commands. * * @param \Illuminate\Redis\Connections\Connection $connection * @param string $name * @return \Illuminate\Redis\Connections\Connection */ protected function configure(Connection $connection, $name) { $connection->setName($name); if ($this->events && $this->app->bound('events')) { $connection->setEventDispatcher($this->app->make('events')); } return $connection; } /** * Get the connector instance for the current driver. * * @return \Illuminate\Contracts\Redis\Connector */ protected function connector() { $customCreator = $this->customCreators[$this->driver] ?? null; if ($customCreator) { return $customCreator(); } switch ($this->driver) { case 'predis': return new PredisConnector; case 'phpredis': return new PhpRedisConnector; } } /** * Parse the Redis connection configuration. * * @param mixed $config * @return array */ protected function parseConnectionConfiguration($config) { $parsed = (new ConfigurationUrlParser)->parseConfiguration($config); $driver = strtolower($parsed['driver'] ?? ''); if (in_array($driver, ['tcp', 'tls'])) { $parsed['scheme'] = $driver; } return array_filter($parsed, function ($key) { return ! in_array($key, ['driver'], true); }, ARRAY_FILTER_USE_KEY); } /** * Return all of the created connections. * * @return array */ public function connections() { return $this->connections; } /** * Enable the firing of Redis command events. * * @return void */ public function enableEvents() { $this->events = true; } /** * Disable the firing of Redis command events. * * @return void */ public function disableEvents() { $this->events = false; } /** * Set the default driver. * * @param string $driver * @return void */ public function setDriver($driver) { $this->driver = $driver; } /** * Disconnect the given connection and remove from local cache. * * @param string|null $name * @return void */ public function purge($name = null) { $name = $name ?: 'default'; unset($this->connections[$name]); } /** * Register a custom driver creator Closure. * * @param string $driver * @param \Closure $callback * @return $this */ public function extend($driver, Closure $callback) { $this->customCreators[$driver] = $callback->bindTo($this, $this); return $this; } /** * Pass methods onto the default Redis connection. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { return $this->connection()->{$method}(...$parameters); } } Connections/PhpRedisConnection.php000066600000034216150544634600013316 0ustar00client = $client; $this->config = $config; $this->connector = $connector; } /** * Returns the value of the given key. * * @param string $key * @return string|null */ public function get($key) { $result = $this->command('get', [$key]); return $result !== false ? $result : null; } /** * Get the values of all the given keys. * * @param array $keys * @return array */ public function mget(array $keys) { return array_map(function ($value) { return $value !== false ? $value : null; }, $this->command('mget', [$keys])); } /** * Set the string value in the argument as the value of the key. * * @param string $key * @param mixed $value * @param string|null $expireResolution * @param int|null $expireTTL * @param string|null $flag * @return bool */ public function set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null) { return $this->command('set', [ $key, $value, $expireResolution ? [$flag, $expireResolution => $expireTTL] : null, ]); } /** * Set the given key if it doesn't exist. * * @param string $key * @param string $value * @return int */ public function setnx($key, $value) { return (int) $this->command('setnx', [$key, $value]); } /** * Get the value of the given hash fields. * * @param string $key * @param mixed $dictionary * @return array */ public function hmget($key, ...$dictionary) { if (count($dictionary) === 1) { $dictionary = $dictionary[0]; } return array_values($this->command('hmget', [$key, $dictionary])); } /** * Set the given hash fields to their respective values. * * @param string $key * @param mixed $dictionary * @return int */ public function hmset($key, ...$dictionary) { if (count($dictionary) === 1) { $dictionary = $dictionary[0]; } else { $input = collect($dictionary); $dictionary = $input->nth(2)->combine($input->nth(2, 1))->toArray(); } return $this->command('hmset', [$key, $dictionary]); } /** * Set the given hash field if it doesn't exist. * * @param string $hash * @param string $key * @param string $value * @return int */ public function hsetnx($hash, $key, $value) { return (int) $this->command('hsetnx', [$hash, $key, $value]); } /** * Removes the first count occurrences of the value element from the list. * * @param string $key * @param int $count * @param mixed $value * @return int|false */ public function lrem($key, $count, $value) { return $this->command('lrem', [$key, $value, $count]); } /** * Removes and returns the first element of the list stored at key. * * @param mixed $arguments * @return array|null */ public function blpop(...$arguments) { $result = $this->command('blpop', $arguments); return empty($result) ? null : $result; } /** * Removes and returns the last element of the list stored at key. * * @param mixed $arguments * @return array|null */ public function brpop(...$arguments) { $result = $this->command('brpop', $arguments); return empty($result) ? null : $result; } /** * Removes and returns a random element from the set value at key. * * @param string $key * @param int|null $count * @return mixed|false */ public function spop($key, $count = 1) { return $this->command('spop', func_get_args()); } /** * Add one or more members to a sorted set or update its score if it already exists. * * @param string $key * @param mixed $dictionary * @return int */ public function zadd($key, ...$dictionary) { if (is_array(end($dictionary))) { foreach (array_pop($dictionary) as $member => $score) { $dictionary[] = $score; $dictionary[] = $member; } } $options = []; foreach (array_slice($dictionary, 0, 3) as $i => $value) { if (in_array($value, ['nx', 'xx', 'ch', 'incr', 'gt', 'lt', 'NX', 'XX', 'CH', 'INCR', 'GT', 'LT'], true)) { $options[] = $value; unset($dictionary[$i]); } } return $this->command('zadd', array_merge([$key], [$options], array_values($dictionary))); } /** * Return elements with score between $min and $max. * * @param string $key * @param mixed $min * @param mixed $max * @param array $options * @return array */ public function zrangebyscore($key, $min, $max, $options = []) { if (isset($options['limit']) && Arr::isAssoc($options['limit'])) { $options['limit'] = [ $options['limit']['offset'], $options['limit']['count'], ]; } return $this->command('zRangeByScore', [$key, $min, $max, $options]); } /** * Return elements with score between $min and $max. * * @param string $key * @param mixed $min * @param mixed $max * @param array $options * @return array */ public function zrevrangebyscore($key, $min, $max, $options = []) { if (isset($options['limit']) && Arr::isAssoc($options['limit'])) { $options['limit'] = [ $options['limit']['offset'], $options['limit']['count'], ]; } return $this->command('zRevRangeByScore', [$key, $min, $max, $options]); } /** * Find the intersection between sets and store in a new set. * * @param string $output * @param array $keys * @param array $options * @return int */ public function zinterstore($output, $keys, $options = []) { return $this->command('zinterstore', [$output, $keys, $options['weights'] ?? null, $options['aggregate'] ?? 'sum', ]); } /** * Find the union between sets and store in a new set. * * @param string $output * @param array $keys * @param array $options * @return int */ public function zunionstore($output, $keys, $options = []) { return $this->command('zunionstore', [$output, $keys, $options['weights'] ?? null, $options['aggregate'] ?? 'sum', ]); } /** * Scans all keys based on options. * * @param mixed $cursor * @param array $options * @return mixed */ public function scan($cursor, $options = []) { $result = $this->client->scan($cursor, $options['match'] ?? '*', $options['count'] ?? 10 ); if ($result === false) { $result = []; } return $cursor === 0 && empty($result) ? false : [$cursor, $result]; } /** * Scans the given set for all values based on options. * * @param string $key * @param mixed $cursor * @param array $options * @return mixed */ public function zscan($key, $cursor, $options = []) { $result = $this->client->zscan($key, $cursor, $options['match'] ?? '*', $options['count'] ?? 10 ); if ($result === false) { $result = []; } return $cursor === 0 && empty($result) ? false : [$cursor, $result]; } /** * Scans the given hash for all values based on options. * * @param string $key * @param mixed $cursor * @param array $options * @return mixed */ public function hscan($key, $cursor, $options = []) { $result = $this->client->hscan($key, $cursor, $options['match'] ?? '*', $options['count'] ?? 10 ); if ($result === false) { $result = []; } return $cursor === 0 && empty($result) ? false : [$cursor, $result]; } /** * Scans the given set for all values based on options. * * @param string $key * @param mixed $cursor * @param array $options * @return mixed */ public function sscan($key, $cursor, $options = []) { $result = $this->client->sscan($key, $cursor, $options['match'] ?? '*', $options['count'] ?? 10 ); if ($result === false) { $result = []; } return $cursor === 0 && empty($result) ? false : [$cursor, $result]; } /** * Execute commands in a pipeline. * * @param callable|null $callback * @return \Redis|array */ public function pipeline(callable $callback = null) { $pipeline = $this->client()->pipeline(); return is_null($callback) ? $pipeline : tap($pipeline, $callback)->exec(); } /** * Execute commands in a transaction. * * @param callable|null $callback * @return \Redis|array */ public function transaction(callable $callback = null) { $transaction = $this->client()->multi(); return is_null($callback) ? $transaction : tap($transaction, $callback)->exec(); } /** * Evaluate a LUA script serverside, from the SHA1 hash of the script instead of the script itself. * * @param string $script * @param int $numkeys * @param mixed $arguments * @return mixed */ public function evalsha($script, $numkeys, ...$arguments) { return $this->command('evalsha', [ $this->script('load', $script), $arguments, $numkeys, ]); } /** * Evaluate a script and return its result. * * @param string $script * @param int $numberOfKeys * @param dynamic $arguments * @return mixed */ public function eval($script, $numberOfKeys, ...$arguments) { return $this->command('eval', [$script, $arguments, $numberOfKeys]); } /** * Subscribe to a set of given channels for messages. * * @param array|string $channels * @param \Closure $callback * @return void */ public function subscribe($channels, Closure $callback) { $this->client->subscribe((array) $channels, function ($redis, $channel, $message) use ($callback) { $callback($message, $channel); }); } /** * Subscribe to a set of given channels with wildcards. * * @param array|string $channels * @param \Closure $callback * @return void */ public function psubscribe($channels, Closure $callback) { $this->client->psubscribe((array) $channels, function ($redis, $pattern, $channel, $message) use ($callback) { $callback($message, $channel); }); } /** * Subscribe to a set of given channels for messages. * * @param array|string $channels * @param \Closure $callback * @param string $method * @return void */ public function createSubscription($channels, Closure $callback, $method = 'subscribe') { // } /** * Flush the selected Redis database. * * @return mixed */ public function flushdb() { $arguments = func_get_args(); if (strtoupper((string) ($arguments[0] ?? null)) === 'ASYNC') { return $this->command('flushdb', [true]); } return $this->command('flushdb'); } /** * Execute a raw command. * * @param array $parameters * @return mixed */ public function executeRaw(array $parameters) { return $this->command('rawCommand', $parameters); } /** * Run a command against the Redis database. * * @param string $method * @param array $parameters * @return mixed * * @throws \RedisException */ public function command($method, array $parameters = []) { try { return parent::command($method, $parameters); } catch (RedisException $e) { if (Str::contains($e->getMessage(), 'went away')) { $this->client = $this->connector ? call_user_func($this->connector) : $this->client; } throw $e; } } /** * Disconnects from the Redis instance. * * @return void */ public function disconnect() { $this->client->close(); } /** * Apply a prefix to the given key if necessary. * * @param string $key * @return string */ private function applyPrefix($key) { $prefix = (string) $this->client->getOption(Redis::OPT_PREFIX); return $prefix.$key; } /** * Pass other method calls down to the underlying client. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { return parent::__call(strtolower($method), $parameters); } } Connections/PredisConnection.php000066600000002262150544634600013022 0ustar00client = $client; } /** * Subscribe to a set of given channels for messages. * * @param array|string $channels * @param \Closure $callback * @param string $method * @return void */ public function createSubscription($channels, Closure $callback, $method = 'subscribe') { $loop = $this->pubSubLoop(); $loop->{$method}(...array_values((array) $channels)); foreach ($loop as $message) { if ($message->kind === 'message' || $message->kind === 'pmessage') { call_user_func($callback, $message->payload, $message->channel); } } unset($loop); } } Connections/PacksPhpRedisValues.php000066600000011766150544634600013445 0ustar00 $values * @return array */ public function pack(array $values): array { if (empty($values)) { return $values; } if ($this->supportsPacking()) { return array_map([$this->client, '_pack'], $values); } if ($this->compressed()) { if ($this->supportsLzf() && $this->lzfCompressed()) { if (! function_exists('lzf_compress')) { throw new RuntimeException("'lzf' extension required to call 'lzf_compress'."); } $processor = function ($value) { return \lzf_compress($this->client->_serialize($value)); }; } elseif ($this->supportsZstd() && $this->zstdCompressed()) { if (! function_exists('zstd_compress')) { throw new RuntimeException("'zstd' extension required to call 'zstd_compress'."); } $compressionLevel = $this->client->getOption(Redis::OPT_COMPRESSION_LEVEL); $processor = function ($value) use ($compressionLevel) { return \zstd_compress( $this->client->_serialize($value), $compressionLevel === 0 ? Redis::COMPRESSION_ZSTD_DEFAULT : $compressionLevel ); }; } else { throw new UnexpectedValueException(sprintf( 'Unsupported phpredis compression in use [%d].', $this->client->getOption(Redis::OPT_COMPRESSION) )); } } else { $processor = function ($value) { return $this->client->_serialize($value); }; } return array_map($processor, $values); } /** * Determine if compression is enabled. * * @return bool */ public function compressed(): bool { return defined('Redis::OPT_COMPRESSION') && $this->client->getOption(Redis::OPT_COMPRESSION) !== Redis::COMPRESSION_NONE; } /** * Determine if LZF compression is enabled. * * @return bool */ public function lzfCompressed(): bool { return defined('Redis::COMPRESSION_LZF') && $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; } /** * Determine if ZSTD compression is enabled. * * @return bool */ public function zstdCompressed(): bool { return defined('Redis::COMPRESSION_ZSTD') && $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; } /** * Determine if LZ4 compression is enabled. * * @return bool */ public function lz4Compressed(): bool { return defined('Redis::COMPRESSION_LZ4') && $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; } /** * Determine if the current PhpRedis extension version supports packing. * * @return bool */ protected function supportsPacking(): bool { if ($this->supportsPacking === null) { $this->supportsPacking = $this->phpRedisVersionAtLeast('5.3.5'); } return $this->supportsPacking; } /** * Determine if the current PhpRedis extension version supports LZF compression. * * @return bool */ protected function supportsLzf(): bool { if ($this->supportsLzf === null) { $this->supportsLzf = $this->phpRedisVersionAtLeast('4.3.0'); } return $this->supportsLzf; } /** * Determine if the current PhpRedis extension version supports Zstd compression. * * @return bool */ protected function supportsZstd(): bool { if ($this->supportsZstd === null) { $this->supportsZstd = $this->phpRedisVersionAtLeast('5.1.0'); } return $this->supportsZstd; } /** * Determine if the PhpRedis extension version is at least the given version. * * @param string $version * @return bool */ protected function phpRedisVersionAtLeast(string $version): bool { $phpredisVersion = phpversion('redis'); return $phpredisVersion !== false && version_compare($phpredisVersion, $version, '>='); } } Connections/Connection.php000066600000011606150544634600011655 0ustar00client; } /** * Subscribe to a set of given channels for messages. * * @param array|string $channels * @param \Closure $callback * @return void */ public function subscribe($channels, Closure $callback) { return $this->createSubscription($channels, $callback, __FUNCTION__); } /** * Subscribe to a set of given channels with wildcards. * * @param array|string $channels * @param \Closure $callback * @return void */ public function psubscribe($channels, Closure $callback) { return $this->createSubscription($channels, $callback, __FUNCTION__); } /** * Run a command against the Redis database. * * @param string $method * @param array $parameters * @return mixed */ public function command($method, array $parameters = []) { $start = microtime(true); $result = $this->client->{$method}(...$parameters); $time = round((microtime(true) - $start) * 1000, 2); if (isset($this->events)) { $this->event(new CommandExecuted($method, $parameters, $time, $this)); } return $result; } /** * Fire the given event if possible. * * @param mixed $event * @return void */ protected function event($event) { if (isset($this->events)) { $this->events->dispatch($event); } } /** * Register a Redis command listener with the connection. * * @param \Closure $callback * @return void */ public function listen(Closure $callback) { if (isset($this->events)) { $this->events->listen(CommandExecuted::class, $callback); } } /** * Get the connection name. * * @return string|null */ public function getName() { return $this->name; } /** * Set the connections name. * * @param string $name * @return $this */ public function setName($name) { $this->name = $name; return $this; } /** * Get the event dispatcher used by the connection. * * @return \Illuminate\Contracts\Events\Dispatcher */ public function getEventDispatcher() { return $this->events; } /** * Set the event dispatcher instance on the connection. * * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void */ public function setEventDispatcher(Dispatcher $events) { $this->events = $events; } /** * Unset the event dispatcher instance on the connection. * * @return void */ public function unsetEventDispatcher() { $this->events = null; } /** * Pass other method calls down to the underlying client. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); } return $this->command($method, $parameters); } } Connections/PhpRedisClusterConnection.php000066600000001122150544634600014646 0ustar00client->_masters() as $master) { $async ? $this->command('rawCommand', [$master, 'flushdb', 'async']) : $this->command('flushdb', [$master]); } } } Connections/PredisClusterConnection.php000066600000000651150544634600014364 0ustar00client->executeCommandOnNodes( tap(new ServerFlushDatabase)->setArguments(func_get_args()) ); } } Connectors/PredisConnector.php000066600000003050150544634600012504 0ustar00 10.0], $options, Arr::pull($config, 'options', []) ); if (isset($config['prefix'])) { $formattedOptions['prefix'] = $config['prefix']; } return new PredisConnection(new Client($config, $formattedOptions)); } /** * Create a new clustered Predis connection. * * @param array $config * @param array $clusterOptions * @param array $options * @return \Illuminate\Redis\Connections\PredisClusterConnection */ public function connectToCluster(array $config, array $clusterOptions, array $options) { $clusterSpecificOptions = Arr::pull($config, 'options', []); if (isset($config['prefix'])) { $clusterSpecificOptions['prefix'] = $config['prefix']; } return new PredisClusterConnection(new Client(array_values($config), array_merge( $options, $clusterOptions, $clusterSpecificOptions ))); } } Connectors/PhpRedisConnector.php000066600000016320150544634600013000 0ustar00createClient(array_merge( $config, $options, Arr::pull($config, 'options', []) )); }; return new PhpRedisConnection($connector(), $connector, $config); } /** * Create a new clustered PhpRedis connection. * * @param array $config * @param array $clusterOptions * @param array $options * @return \Illuminate\Redis\Connections\PhpRedisClusterConnection */ public function connectToCluster(array $config, array $clusterOptions, array $options) { $options = array_merge($options, $clusterOptions, Arr::pull($config, 'options', [])); return new PhpRedisClusterConnection($this->createRedisClusterInstance( array_map([$this, 'buildClusterConnectionString'], $config), $options )); } /** * Build a single cluster seed string from an array. * * @param array $server * @return string */ protected function buildClusterConnectionString(array $server) { return $this->formatHost($server).':'.$server['port'].'?'.Arr::query(Arr::only($server, [ 'database', 'password', 'prefix', 'read_timeout', ])); } /** * Create the Redis client instance. * * @param array $config * @return \Redis * * @throws \LogicException */ protected function createClient(array $config) { return tap(new Redis, function ($client) use ($config) { if ($client instanceof RedisFacade) { throw new LogicException( extension_loaded('redis') ? 'Please remove or rename the Redis facade alias in your "app" configuration file in order to avoid collision with the PHP Redis extension.' : 'Please make sure the PHP Redis extension is installed and enabled.' ); } $this->establishConnection($client, $config); if (! empty($config['password'])) { $client->auth($config['password']); } if (isset($config['database'])) { $client->select((int) $config['database']); } if (! empty($config['prefix'])) { $client->setOption(Redis::OPT_PREFIX, $config['prefix']); } if (! empty($config['read_timeout'])) { $client->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']); } if (! empty($config['scan'])) { $client->setOption(Redis::OPT_SCAN, $config['scan']); } if (! empty($config['name'])) { $client->client('SETNAME', $config['name']); } if (array_key_exists('serializer', $config)) { $client->setOption(Redis::OPT_SERIALIZER, $config['serializer']); } if (array_key_exists('compression', $config)) { $client->setOption(Redis::OPT_COMPRESSION, $config['compression']); } if (array_key_exists('compression_level', $config)) { $client->setOption(Redis::OPT_COMPRESSION_LEVEL, $config['compression_level']); } }); } /** * Establish a connection with the Redis host. * * @param \Redis $client * @param array $config * @return void */ protected function establishConnection($client, array $config) { $persistent = $config['persistent'] ?? false; $parameters = [ $this->formatHost($config), $config['port'], Arr::get($config, 'timeout', 0.0), $persistent ? Arr::get($config, 'persistent_id', null) : null, Arr::get($config, 'retry_interval', 0), ]; if (version_compare(phpversion('redis'), '3.1.3', '>=')) { $parameters[] = Arr::get($config, 'read_timeout', 0.0); } if (version_compare(phpversion('redis'), '5.3.0', '>=')) { if (! is_null($context = Arr::get($config, 'context'))) { $parameters[] = $context; } } $client->{($persistent ? 'pconnect' : 'connect')}(...$parameters); } /** * Create a new redis cluster instance. * * @param array $servers * @param array $options * @return \RedisCluster */ protected function createRedisClusterInstance(array $servers, array $options) { $parameters = [ null, array_values($servers), $options['timeout'] ?? 0, $options['read_timeout'] ?? 0, isset($options['persistent']) && $options['persistent'], ]; if (version_compare(phpversion('redis'), '4.3.0', '>=')) { $parameters[] = $options['password'] ?? null; } if (version_compare(phpversion('redis'), '5.3.2', '>=')) { if (! is_null($context = Arr::get($options, 'context'))) { $parameters[] = $context; } } return tap(new RedisCluster(...$parameters), function ($client) use ($options) { if (! empty($options['prefix'])) { $client->setOption(RedisCluster::OPT_PREFIX, $options['prefix']); } if (! empty($options['scan'])) { $client->setOption(RedisCluster::OPT_SCAN, $options['scan']); } if (! empty($options['failover'])) { $client->setOption(RedisCluster::OPT_SLAVE_FAILOVER, $options['failover']); } if (! empty($options['name'])) { $client->client('SETNAME', $options['name']); } if (array_key_exists('serializer', $options)) { $client->setOption(RedisCluster::OPT_SERIALIZER, $options['serializer']); } if (array_key_exists('compression', $options)) { $client->setOption(RedisCluster::OPT_COMPRESSION, $options['compression']); } if (array_key_exists('compression_level', $options)) { $client->setOption(RedisCluster::OPT_COMPRESSION_LEVEL, $options['compression_level']); } }); } /** * Format the host using the scheme if available. * * @param array $options * @return string */ protected function formatHost(array $options) { if (isset($options['scheme'])) { return Str::start($options['host'], "{$options['scheme']}://"); } return $options['host']; } } composer.json000066600000002061150544634600007300 0ustar00{ "name": "illuminate/redis", "description": "The Illuminate Redis package.", "license": "MIT", "homepage": "https://laravel.com", "support": { "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, "authors": [ { "name": "Taylor Otwell", "email": "taylor@laravel.com" } ], "require": { "php": "^7.3|^8.0", "illuminate/collections": "^8.0", "illuminate/contracts": "^8.0", "illuminate/macroable": "^8.0", "illuminate/support": "^8.0" }, "autoload": { "psr-4": { "Illuminate\\Redis\\": "" } }, "suggest": { "ext-redis": "Required to use the phpredis connector (^4.0|^5.0).", "predis/predis": "Required to use the predis connector (^1.1.9)." }, "extra": { "branch-alias": { "dev-master": "8.x-dev" } }, "config": { "sort-packages": true }, "minimum-stability": "dev" } Limiters/ConcurrencyLimiterBuilder.php000066600000005201150544634600014205 0ustar00name = $name; $this->connection = $connection; } /** * Set the maximum number of locks that can be obtained per time window. * * @param int $maxLocks * @return $this */ public function limit($maxLocks) { $this->maxLocks = $maxLocks; return $this; } /** * Set the number of seconds until the lock will be released. * * @param int $releaseAfter * @return $this */ public function releaseAfter($releaseAfter) { $this->releaseAfter = $this->secondsUntil($releaseAfter); return $this; } /** * Set the amount of time to block until a lock is available. * * @param int $timeout * @return $this */ public function block($timeout) { $this->timeout = $timeout; return $this; } /** * Execute the given callback if a lock is obtained, otherwise call the failure callback. * * @param callable $callback * @param callable|null $failure * @return mixed * * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException */ public function then(callable $callback, callable $failure = null) { try { return (new ConcurrencyLimiter( $this->connection, $this->name, $this->maxLocks, $this->releaseAfter ))->block($this->timeout, $callback); } catch (LimiterTimeoutException $e) { if ($failure) { return $failure($e); } throw $e; } } } Limiters/ConcurrencyLimiter.php000066600000007341150544634600012705 0ustar00name = $name; $this->redis = $redis; $this->maxLocks = $maxLocks; $this->releaseAfter = $releaseAfter; } /** * Attempt to acquire the lock for the given number of seconds. * * @param int $timeout * @param callable|null $callback * @return bool * * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException * @throws \Throwable */ public function block($timeout, $callback = null) { $starting = time(); $id = Str::random(20); while (! $slot = $this->acquire($id)) { if (time() - $timeout >= $starting) { throw new LimiterTimeoutException; } usleep(250 * 1000); } if (is_callable($callback)) { try { return tap($callback(), function () use ($slot, $id) { $this->release($slot, $id); }); } catch (Throwable $exception) { $this->release($slot, $id); throw $exception; } } return true; } /** * Attempt to acquire the lock. * * @param string $id A unique identifier for this lock * @return mixed */ protected function acquire($id) { $slots = array_map(function ($i) { return $this->name.$i; }, range(1, $this->maxLocks)); return $this->redis->eval(...array_merge( [$this->lockScript(), count($slots)], array_merge($slots, [$this->name, $this->releaseAfter, $id]) )); } /** * Get the Lua script for acquiring a lock. * * KEYS - The keys that represent available slots * ARGV[1] - The limiter name * ARGV[2] - The number of seconds the slot should be reserved * ARGV[3] - The unique identifier for this lock * * @return string */ protected function lockScript() { return <<<'LUA' for index, value in pairs(redis.call('mget', unpack(KEYS))) do if not value then redis.call('set', KEYS[index], ARGV[3], "EX", ARGV[2]) return ARGV[1]..index end end LUA; } /** * Release the lock. * * @param string $key * @param string $id * @return void */ protected function release($key, $id) { $this->redis->eval($this->releaseScript(), 1, $key, $id); } /** * Get the Lua script to atomically release a lock. * * KEYS[1] - The name of the lock * ARGV[1] - The unique identifier for this lock * * @return string */ protected function releaseScript() { return <<<'LUA' if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end LUA; } } Limiters/DurationLimiterBuilder.php000066600000005076150544634600013512 0ustar00name = $name; $this->connection = $connection; } /** * Set the maximum number of locks that can be obtained per time window. * * @param int $maxLocks * @return $this */ public function allow($maxLocks) { $this->maxLocks = $maxLocks; return $this; } /** * Set the amount of time the lock window is maintained. * * @param \DateTimeInterface|\DateInterval|int $decay * @return $this */ public function every($decay) { $this->decay = $this->secondsUntil($decay); return $this; } /** * Set the amount of time to block until a lock is available. * * @param int $timeout * @return $this */ public function block($timeout) { $this->timeout = $timeout; return $this; } /** * Execute the given callback if a lock is obtained, otherwise call the failure callback. * * @param callable $callback * @param callable|null $failure * @return mixed * * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException */ public function then(callable $callback, callable $failure = null) { try { return (new DurationLimiter( $this->connection, $this->name, $this->maxLocks, $this->decay ))->block($this->timeout, $callback); } catch (LimiterTimeoutException $e) { if ($failure) { return $failure($e); } throw $e; } } } Limiters/DurationLimiter.php000066600000011033150544634600012171 0ustar00name = $name; $this->decay = $decay; $this->redis = $redis; $this->maxLocks = $maxLocks; } /** * Attempt to acquire the lock for the given number of seconds. * * @param int $timeout * @param callable|null $callback * @return mixed * * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException */ public function block($timeout, $callback = null) { $starting = time(); while (! $this->acquire()) { if (time() - $timeout >= $starting) { throw new LimiterTimeoutException; } usleep(750 * 1000); } if (is_callable($callback)) { return $callback(); } return true; } /** * Attempt to acquire the lock. * * @return bool */ public function acquire() { $results = $this->redis->eval( $this->luaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks ); $this->decaysAt = $results[1]; $this->remaining = max(0, $results[2]); return (bool) $results[0]; } /** * Determine if the key has been "accessed" too many times. * * @return bool */ public function tooManyAttempts() { [$this->decaysAt, $this->remaining] = $this->redis->eval( $this->tooManyAttemptsLuaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks ); return $this->remaining <= 0; } /** * Clear the limiter. * * @return void */ public function clear() { $this->redis->del($this->name); } /** * Get the Lua script for acquiring a lock. * * KEYS[1] - The limiter name * ARGV[1] - Current time in microseconds * ARGV[2] - Current time in seconds * ARGV[3] - Duration of the bucket * ARGV[4] - Allowed number of tasks * * @return string */ protected function luaScript() { return <<<'LUA' local function reset() redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1) return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2) end if redis.call('EXISTS', KEYS[1]) == 0 then return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1} end if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then return { tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]), redis.call('HGET', KEYS[1], 'end'), ARGV[4] - redis.call('HGET', KEYS[1], 'count') } end return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1} LUA; } /** * Get the Lua script to determine if the key has been "accessed" too many times. * * KEYS[1] - The limiter name * ARGV[1] - Current time in microseconds * ARGV[2] - Current time in seconds * ARGV[3] - Duration of the bucket * ARGV[4] - Allowed number of tasks * * @return string */ protected function tooManyAttemptsLuaScript() { return <<<'LUA' if redis.call('EXISTS', KEYS[1]) == 0 then return {0, ARGV[2] + ARGV[3]} end if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then return { redis.call('HGET', KEYS[1], 'end'), ARGV[4] - redis.call('HGET', KEYS[1], 'count') } end return {0, ARGV[2] + ARGV[3]} LUA; } }