├── .gitignore ├── redis.links.menu.yml ├── redis.info.yml ├── redis.routing.yml ├── composer.json ├── src ├── Queue │ ├── ReliableQueueBase.php │ ├── ReliableQueueRedisFactory.php │ ├── QueueRedisFactory.php │ ├── QueueBase.php │ ├── PhpRedis.php │ ├── Predis.php │ ├── ReliablePhpRedis.php │ └── ReliablePredis.php ├── ClientInterface.php ├── PersistentLock │ ├── Predis.php │ └── PhpRedis.php ├── Lock │ ├── LockFactory.php │ ├── Predis.php │ └── PhpRedis.php ├── Flood │ ├── FloodFactory.php │ ├── PhpRedis.php │ └── Predis.php ├── Client │ ├── Predis.php │ └── PhpRedis.php ├── Cache │ ├── CacheBackendFactory.php │ ├── RedisCacheTagsChecksum.php │ ├── PhpRedis.php │ ├── Predis.php │ └── CacheBase.php ├── RedisPrefixTrait.php ├── ClientFactory.php └── Controller │ └── ReportController.php ├── .travis-before-script.sh ├── redis.services.yml ├── redis.module ├── tests └── src │ ├── Traits │ └── RedisTestInterfaceTrait.php │ ├── Kernel │ ├── RedisCacheTest.php │ ├── RedisFloodTest.php │ ├── RedisLockTest.php │ └── RedisQueueTest.php │ └── Functional │ ├── Lock │ └── RedisLockFunctionalTest.php │ └── WebTest.php ├── redis.install ├── example.services.yml ├── README.PhpRedis.txt ├── README.Predis.txt ├── .travis.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | predis 2 | -------------------------------------------------------------------------------- /redis.links.menu.yml: -------------------------------------------------------------------------------- 1 | redis.statistics_overview: 2 | title: 'Redis' 3 | parent: system.admin_reports 4 | description: 'Redis usage statistics' 5 | route_name: redis.report 6 | -------------------------------------------------------------------------------- /redis.info.yml: -------------------------------------------------------------------------------- 1 | name: Redis 2 | description: Provide a module placeholder, for using as dependency for module that needs Redis. 3 | package: Performance 4 | type: module 5 | core_version_requirement: ^8.8 || ^9 6 | configure: redis.admin_display 7 | -------------------------------------------------------------------------------- /redis.routing.yml: -------------------------------------------------------------------------------- 1 | redis.report: 2 | path: '/admin/reports/redis' 3 | defaults: 4 | _controller: '\Drupal\redis\Controller\ReportController::overview' 5 | _title: 'Redis' 6 | requirements: 7 | _permission: 'access site reports' 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/redis", 3 | "type": "drupal-module", 4 | "suggest": { 5 | "predis/predis": "^1.1.1" 6 | }, 7 | "license": "GPL-2.0", 8 | "autoload": { 9 | "psr-4": { 10 | "Drupal\\redis\\": "src" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Queue/ReliableQueueBase.php: -------------------------------------------------------------------------------- 1 | > ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -------------------------------------------------------------------------------- /src/ClientInterface.php: -------------------------------------------------------------------------------- 1 | ' . t("Current connected client uses the @name library.", ['@name' => ClientFactory::getClientName()]) . '
'; 22 | } 23 | else { 24 | $messages = '' . t('No redis connection configured.') . '
'; 25 | } 26 | return $messages; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PersistentLock/Predis.php: -------------------------------------------------------------------------------- 1 | client = $factory->getClient(); 19 | // Set the lockId to a fixed string to make the lock ID the same across 20 | // multiple requests. The lock ID is used as a page token to relate all the 21 | // locks set during a request to each other. 22 | // @see \Drupal\Core\Lock\LockBackendInterface::getLockId() 23 | $this->lockId = 'persistent'; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/PersistentLock/PhpRedis.php: -------------------------------------------------------------------------------- 1 | client = $factory->getClient(); 19 | // Set the lockId to a fixed string to make the lock ID the same across 20 | // multiple requests. The lock ID is used as a page token to relate all the 21 | // locks set during a request to each other. 22 | // @see \Drupal\Core\Lock\LockBackendInterface::getLockId() 23 | $this->lockId = 'persistent'; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Lock/LockFactory.php: -------------------------------------------------------------------------------- 1 | clientFactory = $client_factory; 22 | } 23 | 24 | /** 25 | * Get actual lock backend. 26 | * 27 | * @param bool $persistent 28 | * (optional) Whether to return a persistent lock implementation or not. 29 | * 30 | * @return \Drupal\Core\Lock\LockBackendInterface 31 | * Return lock backend instance. 32 | */ 33 | public function get($persistent = FALSE) { 34 | $class_name = $this->clientFactory->getClass($persistent ? ClientFactory::REDIS_IMPL_PERSISTENT_LOCK : ClientFactory::REDIS_IMPL_LOCK); 35 | return new $class_name($this->clientFactory); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/src/Traits/RedisTestInterfaceTrait.php: -------------------------------------------------------------------------------- 1 | "Redis", 26 | 'value' => t("Connected, using the @name client.", ['@name' => ClientFactory::getClientName()]), 27 | 'severity' => REQUIREMENT_OK, 28 | ]; 29 | } 30 | else { 31 | $requirements['redis'] = [ 32 | 'title' => "Redis", 33 | 'value' => t("Not connected."), 34 | 'severity' => REQUIREMENT_WARNING, 35 | 'description' => t("No Redis client connected, this module is useless thereof. Ensure that you enabled module using it or disable it."), 36 | ]; 37 | } 38 | 39 | return $requirements; 40 | } 41 | -------------------------------------------------------------------------------- /example.services.yml: -------------------------------------------------------------------------------- 1 | # This file contains example services overrides. 2 | # 3 | # Enable with this line in settings.php 4 | # $settings['container_yamls'][] = 'modules/redis/example.services.yml'; 5 | # 6 | # Or copy & paste the desired services into sites/default/services.yml. 7 | # 8 | # Note that the redis module must be enabled for this to work. 9 | 10 | services: 11 | # Cache tag checksum backend. Used by redis and most other cache backend 12 | # to deal with cache tag invalidations. 13 | cache_tags.invalidator.checksum: 14 | class: Drupal\redis\Cache\RedisCacheTagsChecksum 15 | arguments: ['@redis.factory'] 16 | tags: 17 | - { name: cache_tags_invalidator } 18 | 19 | # Replaces the default lock backend with a redis implementation. 20 | lock: 21 | class: Drupal\Core\Lock\LockBackendInterface 22 | factory: ['@redis.lock.factory', get] 23 | 24 | # Replaces the default persistent lock backend with a redis implementation. 25 | lock.persistent: 26 | class: Drupal\Core\Lock\LockBackendInterface 27 | factory: ['@redis.lock.factory', get] 28 | arguments: [true] 29 | 30 | # Replaces the default flood backend with a redis implementation. 31 | flood: 32 | class: Drupal\Core\Flood\FloodInterface 33 | factory: ['@redis.flood.factory', get] 34 | -------------------------------------------------------------------------------- /src/Flood/FloodFactory.php: -------------------------------------------------------------------------------- 1 | clientFactory = $client_factory; 36 | $this->requestStack = $request_stack; 37 | } 38 | 39 | /** 40 | * Get actual flood backend. 41 | * 42 | * @return \Drupal\Core\Flood\FloodInterface 43 | * Return flood instance. 44 | */ 45 | public function get() { 46 | $class_name = $this->clientFactory->getClass(ClientFactory::REDIS_IMPL_FLOOD); 47 | return new $class_name($this->clientFactory, $this->requestStack); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/src/Kernel/RedisCacheTest.php: -------------------------------------------------------------------------------- 1 | has('redis.factory')) { 31 | $container->register('cache_tags.invalidator.checksum', 'Drupal\redis\Cache\RedisCacheTagsChecksum') 32 | ->addArgument(new Reference('redis.factory')) 33 | ->addTag('cache_tags_invalidator'); 34 | } 35 | } 36 | 37 | /** 38 | * Creates a new instance of PhpRedis cache backend. 39 | * 40 | * @return \Drupal\redis\Cache\PhpRedis 41 | * A new PhpRedis cache backend. 42 | */ 43 | protected function createCacheBackend($bin) { 44 | $cache = \Drupal::service('cache.backend.redis')->get($bin); 45 | $cache->setMinTtl(10); 46 | return $cache; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/src/Kernel/RedisFloodTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($flood->isAllowed($name, $threshold)); 40 | 41 | // Register event. 42 | $flood->register($name, $window); 43 | 44 | // The event is still allowed. 45 | $this->assertTrue($flood->isAllowed($name, $threshold)); 46 | 47 | $flood->register($name, $window); 48 | 49 | // Verify event is not allowed. 50 | $this->assertFalse($flood->isAllowed($name, $threshold)); 51 | 52 | // "Sleep" two seconds, then the event is allowed again. 53 | $_SERVER['REQUEST_TIME'] += 2; 54 | $this->assertTrue($flood->isAllowed($name, $threshold)); 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Queue/QueueRedisFactory.php: -------------------------------------------------------------------------------- 1 | clientFactory = $client_factory; 38 | $this->settings = $settings; 39 | } 40 | 41 | /** 42 | * Constructs a new queue object for a given name. 43 | * 44 | * @param string $name 45 | * The name of the collection holding key and value pairs. 46 | * 47 | * @return \Drupal\Core\Queue\DatabaseQueue 48 | * A key/value store implementation for the given $collection. 49 | */ 50 | public function get($name) { 51 | $settings = $this->settings->get('redis_queue_' . $name, ['reserve_timeout' => NULL]); 52 | $class_name = $this->clientFactory->getClass(static::CLASS_NAMESPACE); 53 | return new $class_name($name, $settings, $this->clientFactory->getClient()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /README.PhpRedis.txt: -------------------------------------------------------------------------------- 1 | PhpRedis cache backend 2 | ====================== 3 | 4 | This client, for now, is only able to use the PhpRedis extension. 5 | 6 | Get PhpRedis 7 | ------------ 8 | 9 | You can download this library at: 10 | 11 | https://github.com/nicolasff/phpredis 12 | 13 | This is PHP extension, too recent for being packaged in most distribution, you 14 | will probably need to compile it yourself. 15 | 16 | Default behavior is to connect via tcp://localhost:6379 but you might want to 17 | connect differently. 18 | 19 | Use the Sentinel high availability mode 20 | --------------------------------------- 21 | 22 | Redis can provide a master/slave mode with sentinels server monitoring them. 23 | More information about setting it : https://redis.io/topics/sentinel. 24 | 25 | This mode needs the following settings: 26 | 27 | Modify the host as follow: 28 | // Sentinels instances list with hostname:port format. 29 | $settings['redis.connection']['host'] = ['1.2.3.4:5000','1.2.3.5:5000','1.2.3.6:5000']; 30 | 31 | Add the new instance setting: 32 | 33 | // Redis instance name. 34 | $settings['redis.connection']['instance'] = 'instance_name'; 35 | 36 | Connect via UNIX socket 37 | ----------------------- 38 | 39 | Just add this line to your settings.php file: 40 | 41 | $conf['redis_cache_socket'] = '/tmp/redis.sock'; 42 | 43 | Don't forget to change the path depending on your operating system and Redis 44 | server configuration. 45 | 46 | Connect to a remote host and database 47 | ------------------------------------- 48 | 49 | See README.md file. 50 | 51 | For this particular implementation, host settings are overridden by the 52 | UNIX socket parameter. 53 | -------------------------------------------------------------------------------- /README.Predis.txt: -------------------------------------------------------------------------------- 1 | Predis cache backend 2 | ==================== 3 | 4 | Using Predis for the Drupal 8 version of this module is still experimental. 5 | 6 | Get Predis 7 | ---------- 8 | 9 | Predis can be installed to the vendor directory using composer like so: 10 | 11 | composer require predis/predis 12 | 13 | 14 | Configuration of module for use with Predis 15 | ---------------------------- 16 | 17 | There is not much different to configure about Predis. 18 | Adding this to settings.php should suffice for basic usage: 19 | 20 | $settings['redis.connection']['interface'] = 'Predis'; 21 | $settings['redis.connection']['host'] = '1.2.3.4'; // Your Redis instance hostname. 22 | $settings['cache']['default'] = 'cache.backend.redis'; 23 | 24 | To add more magic with a primary/replica setup you can use a config like this: 25 | 26 | $settings['redis.connection']['interface'] = 'Predis'; // Use predis library. 27 | $settings['redis.connection']['replication'] = TRUE; // Turns on replication. 28 | $settings['redis.connection']['replication.host'][1]['host'] = '1.2.3.4'; // Your Redis instance hostname. 29 | $settings['redis.connection']['replication.host'][1]['port'] = '6379'; // Only required if using non-standard port. 30 | $settings['redis.connection']['replication.host'][1]['role'] = 'primary'; // The redis instance role. 31 | $settings['redis.connection']['replication.host'][2]['host'] = '1.2.3.5'; 32 | $settings['redis.connection']['replication.host'][2]['port'] = '6379'; 33 | $settings['redis.connection']['replication.host'][2]['role'] = 'replica'; 34 | $settings['redis.connection']['replication.host'][3]['host'] = '1.2.3.6'; 35 | $settings['redis.connection']['replication.host'][3]['port'] = '6379'; 36 | $settings['redis.connection']['replication.host'][3]['role'] = 'replica'; 37 | $settings['cache']['default'] = 'cache.backend.redis'; -------------------------------------------------------------------------------- /src/Client/Predis.php: -------------------------------------------------------------------------------- 1 | $password, 17 | 'host' => $host, 18 | 'port' => $port, 19 | 'database' => $base 20 | ]; 21 | 22 | foreach ($connectionInfo as $key => $value) { 23 | if (!isset($value)) { 24 | unset($connectionInfo[$key]); 25 | } 26 | } 27 | 28 | // I'm not sure why but the error handler is driven crazy if timezone 29 | // is not set at this point. 30 | // Hopefully Drupal will restore the right one this once the current 31 | // account has logged in. 32 | date_default_timezone_set(@date_default_timezone_get()); 33 | 34 | // If we are passed in an array of $replicationHosts, we should attempt a clustered client connection. 35 | if ($replicationHosts !== NULL) { 36 | $parameters = []; 37 | 38 | foreach ($replicationHosts as $replicationHost) { 39 | // Configure master. 40 | if ($replicationHost['role'] === 'primary') { 41 | $parameters[] = 'tcp://' . $replicationHost['host'] . ':' . $replicationHost['port'] . '?alias=master'; 42 | } 43 | else { 44 | $parameters[] = 'tcp://' . $replicationHost['host'] . ':' . $replicationHost['port']; 45 | } 46 | } 47 | 48 | $options = ['replication' => true]; 49 | $client = new Client($parameters, $options); 50 | } 51 | else { 52 | $client = new Client($connectionInfo); 53 | } 54 | return $client; 55 | 56 | } 57 | 58 | public function getName() { 59 | return 'Predis'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/src/Kernel/RedisLockTest.php: -------------------------------------------------------------------------------- 1 | register('lock', LockBackendInterface::class) 38 | ->setFactory([new Reference('redis.lock.factory'), 'get']); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function setUp() { 45 | parent::setUp(); 46 | $this->lock = $this->container->get('lock'); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function testBackendLockRelease() { 53 | $redis_interface = self::getRedisInterfaceEnv(); 54 | // Verify that the correct lock backend is being instantiated by the 55 | // factory. 56 | $this->assertInstanceOf('\Drupal\redis\Lock\\' . $redis_interface, $this->lock); 57 | 58 | // Verify that a lock that has never been acquired is marked as available. 59 | // @todo Remove this line when #3002640 lands. 60 | // @see https://www.drupal.org/project/drupal/issues/3002640 61 | $this->assertTrue($this->lock->lockMayBeAvailable('lock_a')); 62 | 63 | parent::testBackendLockRelease(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/Cache/CacheBackendFactory.php: -------------------------------------------------------------------------------- 1 | clientFactory = $client_factory; 55 | $this->checksumProvider = $checksum_provider; 56 | $this->serializer = $serializer; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function get($bin) { 63 | if (!isset($this->bins[$bin])) { 64 | $class_name = $this->clientFactory->getClass(ClientFactory::REDIS_IMPL_CACHE); 65 | $this->bins[$bin] = new $class_name($bin, $this->clientFactory->getClient(), $this->checksumProvider, $this->serializer); 66 | } 67 | return $this->bins[$bin]; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/RedisPrefixTrait.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 75 | } 76 | 77 | /** 78 | * Get prefix 79 | * 80 | * @return string 81 | */ 82 | protected function getPrefix() { 83 | if (!isset($this->prefix)) { 84 | $this->prefix = $this->getDefaultPrefix(); 85 | } 86 | return $this->prefix; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Queue/QueueBase.php: -------------------------------------------------------------------------------- 1 | name = $name; 81 | $this->reserveTimeout = $settings['reserve_timeout']; 82 | $this->availableListKey = static::KEY_PREFIX . $name . ':avail'; 83 | $this->availableItems = static::KEY_PREFIX . $name . ':items'; 84 | $this->claimedListKey = static::KEY_PREFIX . $name . ':claimed'; 85 | $this->leasedKeyPrefix = static::KEY_PREFIX . $name . ':lease:'; 86 | $this->incrementCounterKey = static::KEY_PREFIX . $name . ':counter'; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function createQueue() { 93 | // Nothing to do here. 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Flood/PhpRedis.php: -------------------------------------------------------------------------------- 1 | client = $client_factory->getClient(); 40 | $this->requestStack = $request_stack; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function register($name, $window = 3600, $identifier = NULL) { 47 | if (!isset($identifier)) { 48 | $identifier = $this->requestStack->getCurrentRequest()->getClientIp(); 49 | } 50 | 51 | $key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier; 52 | 53 | // Add a key for the event to the sorted set, the score is timestamp, so we 54 | // can count them easily. 55 | $this->client->zAdd($key, $_SERVER['REQUEST_TIME'] + $window, microtime(TRUE)); 56 | // Set or update the expiration for the sorted set, it will be removed if 57 | // the newest entry expired. 58 | $this->client->expire($key, $_SERVER['REQUEST_TIME'] + $window); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function clear($name, $identifier = NULL) { 65 | if (!isset($identifier)) { 66 | $identifier = $this->requestStack->getCurrentRequest()->getClientIp(); 67 | } 68 | 69 | $key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier; 70 | $this->client->del($key); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function isAllowed($name, $threshold, $window = 3600, $identifier = NULL) { 77 | if (!isset($identifier)) { 78 | $identifier = $this->requestStack->getCurrentRequest()->getClientIp(); 79 | } 80 | 81 | $key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier; 82 | 83 | // Count the in the last $window seconds. 84 | $number = $this->client->zCount($key, $_SERVER['REQUEST_TIME'], 'inf'); 85 | return ($number < $threshold); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function garbageCollection() { 92 | // No garbage collection necessary. 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/Flood/Predis.php: -------------------------------------------------------------------------------- 1 | client = $client_factory->getClient(); 40 | $this->requestStack = $request_stack; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function register($name, $window = 3600, $identifier = NULL) { 47 | if (!isset($identifier)) { 48 | $identifier = $this->requestStack->getCurrentRequest()->getClientIp(); 49 | } 50 | 51 | $key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier; 52 | 53 | // Add a key for the event to the sorted set, the score is timestamp, so we 54 | // can count them easily. 55 | $this->client->zAdd($key, $_SERVER['REQUEST_TIME'] + $window, microtime(TRUE)); 56 | // Set or update the expiration for the sorted set, it will be removed if 57 | // the newest entry expired. 58 | $this->client->expire($key, $_SERVER['REQUEST_TIME'] + $window); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function clear($name, $identifier = NULL) { 65 | if (!isset($identifier)) { 66 | $identifier = $this->requestStack->getCurrentRequest()->getClientIp(); 67 | } 68 | 69 | $key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier; 70 | $this->client->del($key); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function isAllowed($name, $threshold, $window = 3600, $identifier = NULL) { 77 | if (!isset($identifier)) { 78 | $identifier = $this->requestStack->getCurrentRequest()->getClientIp(); 79 | } 80 | 81 | $key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier; 82 | 83 | // Count the in the last $window seconds. 84 | $number = $this->client->zCount($key, $_SERVER['REQUEST_TIME'], 'inf'); 85 | return ($number < $threshold); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function garbageCollection() { 92 | // No garbage collection necessary. 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/Client/PhpRedis.php: -------------------------------------------------------------------------------- 1 | askForMaster($client, $host, $password); 23 | if (is_array($ip_host)) { 24 | list($host, $port) = $ip_host; 25 | } 26 | } 27 | 28 | $client->connect($host, $port); 29 | 30 | if (isset($password)) { 31 | $client->auth($password); 32 | } 33 | 34 | if (isset($base)) { 35 | $client->select($base); 36 | } 37 | 38 | // Do not allow PhpRedis serialize itself data, we are going to do it 39 | // ourself. This will ensure less memory footprint on Redis size when 40 | // we will attempt to store small values. 41 | $client->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_NONE); 42 | 43 | return $client; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName() { 50 | return 'PhpRedis'; 51 | } 52 | 53 | /** 54 | * Connect to sentinels to get Redis master instance. 55 | * 56 | * Just asking one sentinels after another until given the master location. 57 | * More info about this mode at https://redis.io/topics/sentinel. 58 | * 59 | * @param \Redis $client 60 | * The PhpRedis client. 61 | * @param array $sentinels 62 | * An array of the sentinels' ip:port. 63 | * @param string $password 64 | * An optional Sentinels' password. 65 | * 66 | * @return mixed 67 | * An array with ip & port of the Master instance or NULL. 68 | */ 69 | protected function askForMaster(\Redis $client, array $sentinels = [], $password = NULL) { 70 | 71 | $ip_port = NULL; 72 | $settings = Settings::get('redis.connection', []); 73 | $settings += ['instance' => NULL]; 74 | 75 | if ($settings['instance']) { 76 | foreach ($sentinels as $sentinel) { 77 | list($host, $port) = explode(':', $sentinel); 78 | // Prevent fatal PHP errors when one of the sentinels is down. 79 | set_error_handler(function () { 80 | return TRUE; 81 | }); 82 | // 0.5s timeout. 83 | $success = $client->connect($host, $port, 0.5); 84 | restore_error_handler(); 85 | 86 | if (!$success) { 87 | continue; 88 | } 89 | 90 | if (isset($password)) { 91 | $client->auth($password); 92 | } 93 | 94 | if ($client->isConnected()) { 95 | $ip_port = $client->rawcommand('SENTINEL', 'get-master-addr-by-name', $settings['instance']); 96 | if ($ip_port) { 97 | break; 98 | } 99 | } 100 | $client->close(); 101 | } 102 | } 103 | return $ip_port; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /tests/src/Functional/Lock/RedisLockFunctionalTest.php: -------------------------------------------------------------------------------- 1 | siteDirectory . '/settings.php'; 36 | chmod($filename, 0666); 37 | $contents = file_get_contents($filename); 38 | $redis_interface = self::getRedisInterfaceEnv(); 39 | $module_path = drupal_get_path('module', 'redis'); 40 | $contents .= "\n\n" . "\$settings['container_yamls'][] = '$module_path/example.services.yml';"; 41 | $contents .= "\n\n" . '$settings["redis.connection"]["interface"] = \'' . $redis_interface . '\';'; 42 | file_put_contents($filename, $contents); 43 | $settings = Settings::getAll(); 44 | $settings['container_yamls'][] = $module_path . '/example.services.yml'; 45 | $settings['redis.connection']['interface'] = $redis_interface; 46 | new Settings($settings); 47 | OpCodeCache::invalidate(DRUPAL_ROOT . '/' . $filename); 48 | 49 | $this->rebuildContainer(); 50 | 51 | // Get database schema. 52 | $db_schema = Database::getConnection()->schema(); 53 | // Make sure that the semaphore table isn't used. 54 | $db_schema->dropTable('semaphore'); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function testLockAcquire() { 61 | $redis_interface = self::getRedisInterfaceEnv(); 62 | $lock = $this->container->get('lock'); 63 | $this->assertInstanceOf('\Drupal\redis\Lock\\' . $redis_interface, $lock); 64 | 65 | // Verify that a lock that has never been acquired is marked as available. 66 | // @todo Remove this line when #3002640 lands. 67 | // @see https://www.drupal.org/project/drupal/issues/3002640 68 | $this->assertTrue($lock->lockMayBeAvailable('system_test_lock_acquire')); 69 | 70 | parent::testLockAcquire(); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function testPersistentLock() { 77 | $redis_interface = self::getRedisInterfaceEnv(); 78 | $persistent_lock = $this->container->get('lock.persistent'); 79 | $this->assertInstanceOf('\Drupal\redis\PersistentLock\\' . $redis_interface, $persistent_lock); 80 | 81 | // Verify that a lock that has never been acquired is marked as available. 82 | // @todo Remove this line when #3002640 lands. 83 | // @see https://www.drupal.org/project/drupal/issues/3002640 84 | $this->assertTrue($persistent_lock->lockMayBeAvailable('lock1')); 85 | 86 | parent::testPersistentLock(); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Cache/RedisCacheTagsChecksum.php: -------------------------------------------------------------------------------- 1 | client = $factory->getClient(); 50 | $this->clientType = $factory->getClientName(); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function doInvalidateTags(array $tags) { 57 | $keys = array_map([$this, 'getTagKey'], $tags); 58 | 59 | // We want to differentiate between PhpRedis and Redis clients. 60 | if ($this->clientType === 'PhpRedis') { 61 | $multi = $this->client->multi(); 62 | foreach ($keys as $key) { 63 | $multi->incr($key); 64 | } 65 | $multi->exec(); 66 | } 67 | elseif ($this->clientType === 'Predis') { 68 | 69 | $pipe = $this->client->pipeline(); 70 | foreach ($keys as $key) { 71 | $pipe->incr($key); 72 | } 73 | $pipe->execute(); 74 | } 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | protected function getTagInvalidationCounts(array $tags) { 81 | $keys = array_map([$this, 'getTagKey'], $tags); 82 | // The mget command returns the values as an array with numeric keys, 83 | // combine it with the tags array to get the expected return value and run 84 | // it through intval() to convert to integers and FALSE to 0. 85 | return array_map('intval', array_combine($tags, $this->client->mget($keys))); 86 | } 87 | 88 | /** 89 | * Return the key for the given cache tag. 90 | * 91 | * @param string $tag 92 | * The cache tag. 93 | * 94 | * @return string 95 | * The prefixed cache tag. 96 | */ 97 | protected function getTagKey($tag) { 98 | return $this->getPrefix() . ':cachetags:' . $tag; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | protected function getDatabaseConnection() { 105 | // This is not injected to avoid a dependency on the database in the 106 | // critical path. It is only needed during cache tag invalidations. 107 | return \Drupal::database(); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/Cache/PhpRedis.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | $this->checksumProvider = $checksum_provider; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getMultiple(&$cids, $allow_invalid = FALSE) { 39 | // Avoid an error when there are no cache ids. 40 | if (empty($cids)) { 41 | return []; 42 | } 43 | 44 | $return = []; 45 | 46 | // Build the list of keys to fetch. 47 | $keys = array_map([$this, 'getKey'], $cids); 48 | 49 | // Optimize for the common case when only a single cache entry needs to 50 | // be fetched, no pipeline is needed then. 51 | if (count($keys) > 1) { 52 | $pipe = $this->client->multi(); 53 | foreach ($keys as $key) { 54 | $pipe->hgetall($key); 55 | } 56 | $result = $pipe->exec(); 57 | } 58 | else { 59 | $result = [$this->client->hGetAll(reset($keys))]; 60 | } 61 | 62 | // Loop over the cid values to ensure numeric indexes. 63 | foreach (array_values($cids) as $index => $key) { 64 | // Check if a valid result was returned from Redis. 65 | if (isset($result[$index]) && is_array($result[$index])) { 66 | // Check expiration and invalidation and convert into an object. 67 | $item = $this->expandEntry($result[$index], $allow_invalid); 68 | if ($item) { 69 | $return[$item->cid] = $item; 70 | } 71 | } 72 | } 73 | 74 | // Remove fetched cids from the list. 75 | $cids = array_diff($cids, array_keys($return)); 76 | 77 | return $return; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { 84 | 85 | $ttl = $this->getExpiration($expire); 86 | 87 | $key = $this->getKey($cid); 88 | 89 | // If the item is already expired, delete it. 90 | if ($ttl <= 0) { 91 | $this->delete($key); 92 | } 93 | 94 | // Build the cache item and save it as a hash array. 95 | $entry = $this->createEntryHash($cid, $data, $expire, $tags); 96 | $pipe = $this->client->multi(); 97 | $pipe->hMset($key, $entry); 98 | $pipe->expire($key, $ttl); 99 | $pipe->exec(); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function doDeleteMultiple(array $cids) { 106 | $keys = array_map([$this, 'getKey'], $cids); 107 | $this->client->del($keys); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /tests/src/Kernel/RedisQueueTest.php: -------------------------------------------------------------------------------- 1 | NULL]; 32 | $class_name = $client_factory->getClass(ClientFactory::REDIS_IMPL_QUEUE); 33 | 34 | /** @var \Drupal\Core\Queue\QueueInterface $queue1 */ 35 | $queue1 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient()); 36 | $queue1->createQueue(); 37 | 38 | /** @var \Drupal\Core\Queue\QueueInterface $queue2 */ 39 | $queue2 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient()); 40 | $queue2->createQueue(); 41 | 42 | $this->runQueueTest($queue1, $queue2); 43 | $queue1->deleteQueue(); 44 | $queue2->deleteQueue(); 45 | 46 | $class_name = $client_factory->getClass(ClientFactory::REDIS_IMPL_RELIABLE_QUEUE); 47 | 48 | /** @var \Drupal\Core\Queue\QueueInterface $queue1 */ 49 | $queue1 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient()); 50 | $queue1->createQueue(); 51 | 52 | /** @var \Drupal\Core\Queue\QueueInterface $queue2 */ 53 | $queue2 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient()); 54 | $queue2->createQueue(); 55 | 56 | $this->runQueueTest($queue1, $queue2); 57 | } 58 | 59 | /** 60 | * Tests Redis blocking queue. 61 | */ 62 | public function testRedisBlockingQueue() { 63 | self::setUpSettings(); 64 | // Create two queues. 65 | $client_factory = \Drupal::service('redis.factory'); 66 | $settings = ['reserve_timeout' => 30]; 67 | $class_name = $client_factory->getClass(ClientFactory::REDIS_IMPL_QUEUE); 68 | 69 | /** @var \Drupal\Core\Queue\QueueInterface $queue1 */ 70 | $queue1 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient()); 71 | $queue1->createQueue(); 72 | 73 | /** @var \Drupal\Core\Queue\QueueInterface $queue2 */ 74 | $queue2 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient()); 75 | $queue2->createQueue(); 76 | 77 | $this->runQueueTest($queue1, $queue2); 78 | } 79 | 80 | /** 81 | * Overrides \Drupal\system\Tests\Queue\QueueTestQueueTest::testSystemQueue(). 82 | * 83 | * We override tests from core class we extend to prevent them from running. 84 | */ 85 | public function testSystemQueue() { 86 | $this->markTestSkipped(); 87 | } 88 | 89 | /** 90 | * Overrides \Drupal\system\Tests\Queue\QueueTestQueueTest::testMemoryQueue(). 91 | * 92 | * We override tests from core class we extend to prevent them from running. 93 | */ 94 | public function testMemoryQueue() { 95 | $this->markTestSkipped(); 96 | } 97 | 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/Cache/Predis.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | $this->checksumProvider = $checksum_provider; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getMultiple(&$cids, $allow_invalid = FALSE) { 39 | // Avoid an error when there are no cache ids. 40 | if (empty($cids)) { 41 | return []; 42 | } 43 | 44 | $return = []; 45 | 46 | // Build the list of keys to fetch. 47 | $keys = array_map([$this, 'getKey'], $cids); 48 | 49 | // Optimize for the common case when only a single cache entry needs to 50 | // be fetched, no pipeline is needed then. 51 | if (count($keys) > 1) { 52 | $pipe = $this->client->pipeline(); 53 | foreach ($keys as $key) { 54 | $pipe->hgetall($key); 55 | } 56 | $result = $pipe->execute(); 57 | } 58 | else { 59 | $result = [$this->client->hGetAll(reset($keys))]; 60 | } 61 | 62 | // Loop over the cid values to ensure numeric indexes. 63 | foreach (array_values($cids) as $index => $key) { 64 | // Check if a valid result was returned from Redis. 65 | if (isset($result[$index]) && is_array($result[$index])) { 66 | // Check expiration and invalidation and convert into an object. 67 | $item = $this->expandEntry($result[$index], $allow_invalid); 68 | if ($item) { 69 | $return[$item->cid] = $item; 70 | } 71 | } 72 | } 73 | 74 | // Remove fetched cids from the list. 75 | $cids = array_diff($cids, array_keys($return)); 76 | 77 | return $return; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { 84 | 85 | $ttl = $this->getExpiration($expire); 86 | 87 | $key = $this->getKey($cid); 88 | 89 | // If the item is already expired, delete it. 90 | if ($ttl <= 0) { 91 | $this->delete($key); 92 | } 93 | 94 | // Build the cache item and save it as a hash array. 95 | $entry = $this->createEntryHash($cid, $data, $expire, $tags); 96 | $pipe = $this->client->pipeline(); 97 | $pipe->hmset($key, $entry); 98 | $pipe->expire($key, $ttl); 99 | $pipe->execute(); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function doDeleteMultiple(array $cids) { 106 | if (!empty($cids)) { 107 | $keys = array_map([$this, 'getKey'], $cids); 108 | $this->client->del($keys); 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/Lock/Predis.php: -------------------------------------------------------------------------------- 1 | client = $factory->getClient(); 26 | // __destruct() is causing problems with garbage collections, register a 27 | // shutdown function instead. 28 | drupal_register_shutdown_function([$this, 'releaseAll']); 29 | } 30 | 31 | /** 32 | * Generate a redis key name for the current lock name. 33 | * 34 | * @param string $name 35 | * Lock name. 36 | * 37 | * @return string 38 | * The redis key for the given lock. 39 | */ 40 | protected function getKey($name) { 41 | return $this->getPrefix() . ':lock:' . $name; 42 | } 43 | 44 | public function acquire($name, $timeout = 30.0) { 45 | $key = $this->getKey($name); 46 | $id = $this->getLockId(); 47 | 48 | // Insure that the timeout is at least 1 ms. 49 | $timeout = max($timeout, 0.001); 50 | 51 | // If we already have the lock, check for his owner and attempt a new EXPIRE 52 | // command on it. 53 | if (isset($this->locks[$name])) { 54 | 55 | // Create a new transaction, for atomicity. 56 | $this->client->watch($key); 57 | 58 | // Global tells us we are the owner, but in real life it could have expired 59 | // and another process could have taken it, check that. 60 | if ($this->client->get($key) != $id) { 61 | // Explicit UNWATCH we are not going to run the MULTI/EXEC block. 62 | $this->client->unwatch(); 63 | unset($this->locks[$name]); 64 | return FALSE; 65 | } 66 | 67 | $result = $this->client->psetex($key, (int) ($timeout * 1000), $id); 68 | 69 | // If the set failed, someone else wrote the key, we failed to acquire 70 | // the lock. 71 | if (FALSE === $result) { 72 | unset($this->locks[$name]); 73 | // Explicit transaction release which also frees the WATCH'ed key. 74 | $this->client->discard(); 75 | return FALSE; 76 | } 77 | 78 | return ($this->locks[$name] = TRUE); 79 | } 80 | else { 81 | // Use a SET with microsecond expiration and the NX flag, which will only 82 | // succeed if the key does not exist yet. 83 | $result = $this->client->set($key, $id, 'nx', 'px', (int) ($timeout * 1000)); 84 | 85 | // If the result is FALSE or NULL, we failed to acquire the lock. 86 | if (FALSE === $result || NULL === $result) { 87 | return FALSE; 88 | } 89 | 90 | // Register the lock. 91 | return ($this->locks[$name] = TRUE); 92 | } 93 | } 94 | 95 | public function lockMayBeAvailable($name) { 96 | $key = $this->getKey($name); 97 | $value = $this->client->get($key); 98 | 99 | return $value === FALSE || $value === NULL; 100 | } 101 | 102 | public function release($name) { 103 | $key = $this->getKey($name); 104 | $id = $this->getLockId(); 105 | 106 | unset($this->locks[$name]); 107 | 108 | // Ensure the lock deletion is an atomic transaction. If another thread 109 | // manages to removes all lock, we can not alter it anymore else we will 110 | // release the lock for the other thread and cause race conditions. 111 | $this->client->watch($key); 112 | 113 | if ($this->client->get($key) == $id) { 114 | $pipe = $this->client->pipeline(); 115 | $pipe->del([$key]); 116 | $pipe->execute(); 117 | } 118 | else { 119 | $this->client->unwatch(); 120 | } 121 | } 122 | 123 | public function releaseAll($lock_id = NULL) { 124 | // We can afford to deal with a slow algorithm here, this should not happen 125 | // on normal run because we should have removed manually all our locks. 126 | foreach ($this->locks as $name => $foo) { 127 | $this->release($name); 128 | } 129 | } 130 | } 131 | 132 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # @file 2 | # .travis.yml - Drupal for Travis CI Integration 3 | # 4 | # Template provided by https://github.com/LionsAd/drupal_ti. 5 | # 6 | # Based for simpletest upon: 7 | # https://github.com/sonnym/travis-ci-drupal-module-example 8 | 9 | language: php 10 | 11 | php: 12 | - 7.1 13 | - 7.2 14 | - 7.3 15 | 16 | matrix: 17 | fast_finish: true 18 | 19 | env: 20 | global: 21 | # add composer's global bin directory to the path 22 | # see: https://github.com/drush-ops/drush#install---composer 23 | - PATH="$PATH:$HOME/.composer/vendor/bin" 24 | # force composer 1.8+ to use a specific folder as home 25 | - COMPOSER_HOME="$HOME/.composer/" 26 | 27 | - DRUPAL_TI_DRUSH_VERSION="drush/drush:^9" 28 | 29 | # Configuration variables. 30 | - DRUPAL_TI_MODULE_NAME="redis" 31 | - DRUPAL_TI_SIMPLETEST_GROUP="redis" 32 | 33 | # Define runners and environment vars to include before and after the 34 | # main runners / environment vars. 35 | #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./.drupal_ti/before" 36 | #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after" 37 | 38 | # The environment to use, supported are: drupal-7, drupal-8 39 | - DRUPAL_TI_ENVIRONMENT="drupal-8" 40 | 41 | # Drupal specific variables. 42 | - DRUPAL_TI_DB="drupal_travis_db" 43 | - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db" 44 | # Note: Do not add a trailing slash here. 45 | - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1" 46 | - DRUPAL_TI_WEBSERVER_PORT="8080" 47 | 48 | # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end. 49 | - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT" 50 | 51 | # === Behat specific variables. 52 | # This is relative to $TRAVIS_BUILD_DIR 53 | - DRUPAL_TI_BEHAT_DIR="./tests/behat" 54 | # These arguments are passed to the bin/behat command. 55 | - DRUPAL_TI_BEHAT_ARGS="" 56 | # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables. 57 | - DRUPAL_TI_BEHAT_YML="behat.yml.dist" 58 | # This is used to setup Xvfb. 59 | - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16" 60 | # The version of seleniumthat should be used. 61 | - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44" 62 | # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here. 63 | - DRUPAL_TI_BEHAT_DRIVER="phantomjs" 64 | - DRUPAL_TI_BEHAT_BROWSER="firefox" 65 | 66 | # Set Drupal version in which to run tests. 67 | - DRUPAL_TI_CORE_BRANCH="8.8.x" 68 | 69 | # PHPUnit specific commandline arguments. 70 | - DRUPAL_TI_PHPUNIT_ARGS="--verbose --debug" 71 | # Specifying the phpunit-core src/ directory is useful when e.g. a vendor/ 72 | # directory is present in the module directory, which phpunit would then 73 | # try to find tests in. This option is relative to $TRAVIS_BUILD_DIR. 74 | #- DRUPAL_TI_PHPUNIT_CORE_SRC_DIRECTORY="./tests/src" 75 | 76 | # Code coverage via coveralls.io 77 | - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*" 78 | # This needs to match your .coveralls.yml file. 79 | - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml" 80 | 81 | # Debug options 82 | #- DRUPAL_TI_DEBUG="-x -v" 83 | # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium", 84 | # etc. to only output those channels. 85 | #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver" 86 | 87 | # [[[ SELECT ANY OR MORE OPTIONS ]]] 88 | #- DRUPAL_TI_RUNNERS="phpunit" 89 | #- DRUPAL_TI_RUNNERS="simpletest" 90 | #- DRUPAL_TI_RUNNERS="behat" 91 | - DRUPAL_TI_RUNNERS="phpunit-core" 92 | matrix: 93 | - REDIS_INTERFACE=PhpRedis 94 | - REDIS_INTERFACE=Predis 95 | 96 | # This will create the database 97 | mysql: 98 | database: drupal_travis_db 99 | username: root 100 | encoding: utf8 101 | 102 | services: 103 | - redis-server 104 | - mysql 105 | 106 | before_install: 107 | - composer global require "lionsad/drupal_ti:dev-master" 108 | - drupal-ti before_install 109 | 110 | install: 111 | - drupal-ti install 112 | 113 | before_script: 114 | - drupal-ti --include .travis-before-script.sh 115 | - drupal-ti before_script 116 | 117 | script: 118 | - drupal-ti script 119 | 120 | after_script: 121 | - drupal-ti after_script 122 | -------------------------------------------------------------------------------- /src/Lock/PhpRedis.php: -------------------------------------------------------------------------------- 1 | client = $factory->getClient(); 26 | // __destruct() is causing problems with garbage collections, register a 27 | // shutdown function instead. 28 | drupal_register_shutdown_function([$this, 'releaseAll']); 29 | } 30 | 31 | /** 32 | * Generate a redis key name for the current lock name. 33 | * 34 | * @param string $name 35 | * Lock name. 36 | * 37 | * @return string 38 | * The redis key for the given lock. 39 | */ 40 | protected function getKey($name) { 41 | return $this->getPrefix() . ':lock:' . $name; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function acquire($name, $timeout = 30.0) { 48 | $key = $this->getKey($name); 49 | $id = $this->getLockId(); 50 | 51 | // Insure that the timeout is at least 1 ms. 52 | $timeout = max($timeout, 0.001); 53 | 54 | // If we already have the lock, check for his owner and attempt a new EXPIRE 55 | // command on it. 56 | if (isset($this->locks[$name])) { 57 | 58 | // Create a new transaction, for atomicity. 59 | $this->client->watch($key); 60 | 61 | // Global tells us we are the owner, but in real life it could have expired 62 | // and another process could have taken it, check that. 63 | if ($this->client->get($key) != $id) { 64 | // Explicit UNWATCH we are not going to run the MULTI/EXEC block. 65 | $this->client->unwatch(); 66 | unset($this->locks[$name]); 67 | return FALSE; 68 | } 69 | 70 | $result = $this->client->multi() 71 | ->psetex($key, (int) ($timeout * 1000), $id) 72 | ->exec(); 73 | 74 | // If the set failed, someone else wrote the key, we failed to acquire 75 | // the lock. 76 | if (FALSE === $result) { 77 | unset($this->locks[$name]); 78 | // Explicit transaction release which also frees the WATCH'ed key. 79 | $this->client->discard(); 80 | return FALSE; 81 | } 82 | 83 | return ($this->locks[$name] = TRUE); 84 | } 85 | else { 86 | // Use a SET with microsecond expiration and the NX flag, which will only 87 | // succeed if the key does not exist yet. 88 | $result = $this->client->set($key, $id, ['nx', 'px' => (int) ($timeout * 1000)]); 89 | 90 | // If the result is FALSE, we failed to acquire the lock. 91 | if (FALSE === $result) { 92 | return FALSE; 93 | } 94 | 95 | // Register the lock. 96 | return ($this->locks[$name] = TRUE); 97 | } 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function lockMayBeAvailable($name) { 104 | $key = $this->getKey($name); 105 | $value = $this->client->get($key); 106 | 107 | return $value === FALSE || $value === NULL; 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function release($name) { 114 | $key = $this->getKey($name); 115 | $id = $this->getLockId(); 116 | 117 | unset($this->locks[$name]); 118 | 119 | // Ensure the lock deletion is an atomic transaction. If another thread 120 | // manages to removes all lock, we can not alter it anymore else we will 121 | // release the lock for the other thread and cause race conditions. 122 | $this->client->watch($key); 123 | 124 | if ($this->client->get($key) == $id) { 125 | $this->client->multi(); 126 | $this->client->del($key); 127 | $this->client->exec(); 128 | } 129 | else { 130 | $this->client->unwatch(); 131 | } 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function releaseAll($lock_id = NULL) { 138 | // We can afford to deal with a slow algorithm here, this should not happen 139 | // on normal run because we should have removed manually all our locks. 140 | foreach ($this->locks as $name => $foo) { 141 | $this->release($name); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Queue/PhpRedis.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function createItem($data) { 38 | $record = new \stdClass(); 39 | $record->data = $data; 40 | $record->qid = $this->incrementId(); 41 | // We cannot rely on REQUEST_TIME because many items might be created 42 | // by a single request which takes longer than 1 second. 43 | $record->timestamp = time(); 44 | 45 | if (!$this->client->hsetnx($this->availableItems, $record->qid, serialize($record))) { 46 | return FALSE; 47 | } 48 | 49 | $start_len = $this->client->lLen($this->availableListKey); 50 | if ($start_len < $this->client->lpush($this->availableListKey, $record->qid)) { 51 | return $record->qid; 52 | } 53 | 54 | return FALSE; 55 | } 56 | 57 | /** 58 | * Gets next serial ID for Redis queue items. 59 | * 60 | * @return int 61 | * Next serial ID for Redis queue item. 62 | */ 63 | protected function incrementId() { 64 | return $this->client->incr($this->incrementCounterKey); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function numberOfItems() { 71 | return $this->client->lLen($this->availableListKey) + $this->client->lLen($this->claimedListKey); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function claimItem($lease_time = 30) { 78 | // Is it OK to do garbage collection here (we need to loop list of claimed 79 | // items)? 80 | $this->garbageCollection(); 81 | $item = FALSE; 82 | 83 | if ($this->reserveTimeout !== NULL) { 84 | // A blocking version of claimItem to be used with long-running queue workers. 85 | $qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout); 86 | } 87 | else { 88 | $qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey); 89 | } 90 | 91 | if ($qid) { 92 | $job = $this->client->hget($this->availableItems, $qid); 93 | if ($job) { 94 | $item = unserialize($job); 95 | $this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1'); 96 | } 97 | } 98 | 99 | return $item; 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function releaseItem($item) { 106 | $this->client->lrem($this->claimedListKey, $item->qid, -1); 107 | $this->client->lpush($this->availableListKey, $item->qid); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function deleteItem($item) { 114 | $this->client->lrem($this->claimedListKey, $item->qid, -1); 115 | $this->client->hdel($this->availableItems, $item->qid); 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function deleteQueue() { 122 | $keys_to_remove = [ 123 | $this->claimedListKey, 124 | $this->availableListKey, 125 | $this->availableItems, 126 | $this->incrementCounterKey 127 | ]; 128 | 129 | foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) { 130 | $keys_to_remove[] = $key; 131 | } 132 | 133 | $this->client->del($keys_to_remove); 134 | } 135 | 136 | /** 137 | * Automatically release items, that have been claimed and exceeded lease time. 138 | */ 139 | protected function garbageCollection() { 140 | foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) { 141 | if (!$this->client->exists($this->leasedKeyPrefix . $qid)) { 142 | // The lease expired for this ID. 143 | $this->client->lrem($this->claimedListKey, $qid, -1); 144 | $this->client->lpush($this->availableListKey, $qid); 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Queue/Predis.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function createItem($data) { 38 | // TODO: Fixme 39 | $record = new \stdClass(); 40 | $record->data = $data; 41 | $record->qid = $this->incrementId(); 42 | // We cannot rely on REQUEST_TIME because many items might be created 43 | // by a single request which takes longer than 1 second. 44 | $record->timestamp = time(); 45 | 46 | if (!$this->client->hsetnx($this->availableItems, $record->qid, serialize($record))) { 47 | return FALSE; 48 | } 49 | 50 | $start_len = $this->client->lLen($this->availableListKey); 51 | if ($start_len < $this->client->lpush($this->availableListKey, $record->qid)) { 52 | return $record->qid; 53 | } 54 | } 55 | 56 | /** 57 | * Gets next serial ID for Redis queue items. 58 | * 59 | * @return int 60 | * Next serial ID for Redis queue item. 61 | */ 62 | protected function incrementId() { 63 | return $this->client->incr($this->incrementCounterKey); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function numberOfItems() { 70 | return $this->client->lLen($this->availableListKey) + $this->client->lLen($this->claimedListKey); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function claimItem($lease_time = 30) { 77 | // Is it OK to do garbage collection here (we need to loop list of claimed 78 | // items)? 79 | $this->garbageCollection(); 80 | $item = FALSE; 81 | 82 | if ($this->reserveTimeout !== NULL) { 83 | // A blocking version of claimItem to be used with long-running queue workers. 84 | $qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout); 85 | } 86 | else { 87 | $qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey); 88 | } 89 | 90 | if ($qid) { 91 | $job = $this->client->hget($this->availableItems, $qid); 92 | if ($job) { 93 | $item = unserialize($job); 94 | $this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1'); 95 | } 96 | } 97 | 98 | return $item; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function releaseItem($item) { 105 | $this->client->lrem($this->claimedListKey, -1, $item->qid); 106 | $this->client->lpush($this->availableListKey, $item->qid); 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function deleteItem($item) { 113 | $this->client->lrem($this->claimedListKey, -1, $item->qid); 114 | $this->client->lrem($this->availableListKey, -1, $item->qid); 115 | $this->client->hdel($this->availableItems, $item->qid); 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function deleteQueue() { 122 | // TODO: Fixme 123 | $keys_to_remove = [ 124 | $this->claimedListKey, 125 | $this->availableListKey, 126 | $this->availableItems, 127 | $this->incrementCounterKey 128 | ]; 129 | 130 | foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) { 131 | $keys_to_remove[] = $key; 132 | } 133 | 134 | $this->client->del($keys_to_remove); 135 | } 136 | 137 | /** 138 | * Automatically release items, that have been claimed and exceeded lease time. 139 | */ 140 | protected function garbageCollection() { 141 | foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) { 142 | if (!$this->client->exists($this->leasedKeyPrefix . $qid)) { 143 | // The lease expired for this ID. 144 | $this->client->lrem($this->claimedListKey, $qid, -1); 145 | $this->client->lpush($this->availableListKey, $qid); 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Queue/ReliablePhpRedis.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function createItem($data) { 38 | $record = new \stdClass(); 39 | $record->data = $data; 40 | $record->qid = $this->incrementId(); 41 | // We cannot rely on REQUEST_TIME because many items might be created 42 | // by a single request which takes longer than 1 second. 43 | $record->timestamp = time(); 44 | 45 | $result = $this->client->multi() 46 | ->hsetnx($this->availableItems, $record->qid, serialize($record)) 47 | ->lLen($this->availableListKey) 48 | ->lpush($this->availableListKey, $record->qid) 49 | ->exec(); 50 | 51 | $success = $result[0] && $result[2] > $result[1]; 52 | 53 | return $success ? $record->qid : FALSE; 54 | } 55 | 56 | /** 57 | * Gets next serial ID for Redis queue items. 58 | * 59 | * @return int 60 | * Next serial ID for Redis queue item. 61 | */ 62 | protected function incrementId() { 63 | return $this->client->incr($this->incrementCounterKey); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function numberOfItems() { 70 | return $this->client->lLen($this->availableListKey) + $this->client->lLen($this->claimedListKey); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function claimItem($lease_time = 30) { 77 | // Is it OK to do garbage collection here (we need to loop list of claimed 78 | // items)? 79 | $this->garbageCollection(); 80 | $item = FALSE; 81 | 82 | if ($this->reserveTimeout !== NULL) { 83 | // A blocking version of claimItem to be used with long-running queue workers. 84 | $qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout); 85 | } 86 | else { 87 | $qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey); 88 | } 89 | 90 | if ($qid) { 91 | $job = $this->client->hget($this->availableItems, $qid); 92 | if ($job) { 93 | $item = unserialize($job); 94 | $this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1'); 95 | } 96 | } 97 | 98 | return $item; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function releaseItem($item) { 105 | $this->client->multi() 106 | ->lrem($this->claimedListKey, $item->qid, -1) 107 | ->lpush($this->availableListKey, $item->qid) 108 | ->exec(); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function deleteItem($item) { 115 | $this->client->multi() 116 | ->lrem($this->claimedListKey, $item->qid, -1) 117 | ->hdel($this->availableItems, $item->qid) 118 | ->exec(); 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function deleteQueue() { 125 | $keys_to_remove = [ 126 | $this->claimedListKey, 127 | $this->availableListKey, 128 | $this->availableItems, 129 | $this->incrementCounterKey 130 | ]; 131 | 132 | foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) { 133 | $keys_to_remove[] = $key; 134 | } 135 | 136 | $this->client->del($keys_to_remove); 137 | } 138 | 139 | /** 140 | * Automatically release items, that have been claimed and exceeded lease time. 141 | */ 142 | protected function garbageCollection() { 143 | foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) { 144 | if (!$this->client->exists($this->leasedKeyPrefix . $qid)) { 145 | // The lease expired for this ID. 146 | $this->client->multi() 147 | ->lrem($this->claimedListKey, $qid, -1) 148 | ->lpush($this->availableListKey, $qid) 149 | ->exec(); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Queue/ReliablePredis.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function createItem($data) { 38 | $record = new \stdClass(); 39 | $record->data = $data; 40 | $record->qid = $this->incrementId(); 41 | // We cannot rely on REQUEST_TIME because many items might be created 42 | // by a single request which takes longer than 1 second. 43 | $record->timestamp = time(); 44 | 45 | $pipe = $this->client->pipeline(); 46 | $pipe->hsetnx($this->availableItems, $record->qid, serialize($record)); 47 | $pipe->lLen($this->availableListKey); 48 | $pipe->lpush($this->availableListKey, $record->qid); 49 | $result = $pipe->execute(); 50 | 51 | $success = $result[0] && $result[2] > $result[1]; 52 | 53 | return $success ? $record->qid : FALSE; 54 | } 55 | 56 | /** 57 | * Gets next serial ID for Redis queue items. 58 | * 59 | * @return int 60 | * Next serial ID for Redis queue item. 61 | */ 62 | protected function incrementId() { 63 | return $this->client->incr($this->incrementCounterKey); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function numberOfItems() { 70 | return $this->client->lLen($this->availableListKey) + $this->client->lLen($this->claimedListKey); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function claimItem($lease_time = 30) { 77 | // Is it OK to do garbage collection here (we need to loop list of claimed 78 | // items)? 79 | $this->garbageCollection(); 80 | $item = FALSE; 81 | 82 | if ($this->reserveTimeout !== NULL) { 83 | // A blocking version of claimItem to be used with long-running queue workers. 84 | $qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout); 85 | } 86 | else { 87 | $qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey); 88 | } 89 | 90 | if ($qid) { 91 | $job = $this->client->hget($this->availableItems, $qid); 92 | if ($job) { 93 | $item = unserialize($job); 94 | $this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1'); 95 | } 96 | } 97 | 98 | return $item; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function releaseItem($item) { 105 | $this->client->pipeline() 106 | ->lrem($this->claimedListKey, -1, $item->qid) 107 | ->lpush($this->availableListKey, $item->qid) 108 | ->execute(); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function deleteItem($item) { 115 | $this->client->pipeline() 116 | ->lrem($this->claimedListKey, -1, $item->qid) 117 | ->lrem($this->availableListKey, -1, $item->qid) 118 | ->hdel($this->availableItems, $item->qid) 119 | ->execute(); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function deleteQueue() { 126 | // TODO: Fixme 127 | $keys_to_remove = [ 128 | $this->claimedListKey, 129 | $this->availableListKey, 130 | $this->availableItems, 131 | $this->incrementCounterKey 132 | ]; 133 | 134 | foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) { 135 | $keys_to_remove[] = $key; 136 | } 137 | 138 | $this->client->del($keys_to_remove); 139 | } 140 | 141 | /** 142 | * Automatically release items, that have been claimed and exceeded lease time. 143 | */ 144 | protected function garbageCollection() { 145 | foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) { 146 | if (!$this->client->exists($this->leasedKeyPrefix . $qid)) { 147 | // The lease expired for this ID. 148 | $this->client->lrem($this->claimedListKey, -1, $qid); 149 | $this->client->lpush($this->availableListKey, $qid); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/ClientFactory.php: -------------------------------------------------------------------------------- 1 | getName(); 140 | } 141 | 142 | /** 143 | * Get client singleton. 144 | */ 145 | public static function getClient() { 146 | if (!isset(self::$_client)) { 147 | $settings = Settings::get('redis.connection', []); 148 | $settings += [ 149 | 'host' => self::REDIS_DEFAULT_HOST, 150 | 'port' => self::REDIS_DEFAULT_PORT, 151 | 'base' => self::REDIS_DEFAULT_BASE, 152 | 'password' => self::REDIS_DEFAULT_PASSWORD, 153 | ]; 154 | 155 | // If using replication, lets create the client appropriately. 156 | if (isset($settings['replication']) && $settings['replication'] === TRUE) { 157 | foreach ($settings['replication.host'] as $key => $replicationHost) { 158 | if (!isset($replicationHost['port'])) { 159 | $settings['replication.host'][$key]['port'] = self::REDIS_DEFAULT_PORT; 160 | } 161 | } 162 | 163 | self::$_client = self::getClientInterface()->getClient( 164 | $settings['host'], 165 | $settings['port'], 166 | $settings['base'], 167 | $settings['password'], 168 | $settings['replication.host']); 169 | } 170 | else { 171 | self::$_client = self::getClientInterface()->getClient( 172 | $settings['host'], 173 | $settings['port'], 174 | $settings['base'], 175 | $settings['password']); 176 | } 177 | } 178 | 179 | return self::$_client; 180 | } 181 | 182 | /** 183 | * Get specific class implementing the current client usage for the specific 184 | * asked core subsystem. 185 | * 186 | * @param string $system 187 | * One of the ClientFactory::IMPL_* constant. 188 | * @param string $clientName 189 | * Client name, if fixed. 190 | * 191 | * @return string 192 | * Class name, if found. 193 | * 194 | * @throws \Exception 195 | * If not found. 196 | */ 197 | public static function getClass($system, $clientName = NULL) { 198 | $className = $system . ($clientName ?: self::getClientName()); 199 | 200 | if (!class_exists($className)) { 201 | throw new \Exception($className . " does not exists"); 202 | } 203 | 204 | return $className; 205 | } 206 | 207 | /** 208 | * For unit testing only reset internals. 209 | */ 210 | static public function reset() { 211 | self::$_clientInterface = null; 212 | self::$_client = null; 213 | } 214 | } 215 | 216 | -------------------------------------------------------------------------------- /tests/src/Functional/WebTest.php: -------------------------------------------------------------------------------- 1 | drupalPlaceBlock('system_breadcrumb_block'); 41 | $this->drupalPlaceBlock('local_tasks_block'); 42 | 43 | // Set in-memory settings. 44 | $settings = Settings::getAll(); 45 | 46 | // Get REDIS_INTERFACE env variable. 47 | $redis_interface = self::getRedisInterfaceEnv(); 48 | $settings['redis.connection']['interface'] = $redis_interface; 49 | $settings['redis_compress_length'] = 100; 50 | 51 | $settings['cache'] = [ 52 | 'default' => 'cache.backend.redis', 53 | ]; 54 | 55 | $settings['container_yamls'][] = drupal_get_path('module', 'redis') . '/example.services.yml'; 56 | 57 | $settings['bootstrap_container_definition'] = [ 58 | 'parameters' => [], 59 | 'services' => [ 60 | 'redis.factory' => [ 61 | 'class' => 'Drupal\redis\ClientFactory', 62 | ], 63 | 'cache.backend.redis' => [ 64 | 'class' => 'Drupal\redis\Cache\CacheBackendFactory', 65 | 'arguments' => ['@redis.factory', '@cache_tags_provider.container', '@serialization.phpserialize'], 66 | ], 67 | 'cache.container' => [ 68 | 'class' => '\Drupal\redis\Cache\PhpRedis', 69 | 'factory' => ['@cache.backend.redis', 'get'], 70 | 'arguments' => ['container'], 71 | ], 72 | 'cache_tags_provider.container' => [ 73 | 'class' => 'Drupal\redis\Cache\RedisCacheTagsChecksum', 74 | 'arguments' => ['@redis.factory'], 75 | ], 76 | 'serialization.phpserialize' => [ 77 | 'class' => 'Drupal\Component\Serialization\PhpSerialize', 78 | ], 79 | ], 80 | ]; 81 | new Settings($settings); 82 | 83 | // Write the containers_yaml update by hand, since writeSettings() doesn't 84 | // support some of the definitions. 85 | $filename = $this->siteDirectory . '/settings.php'; 86 | chmod($filename, 0666); 87 | $contents = file_get_contents($filename); 88 | 89 | // Add the container_yaml and cache definition. 90 | $contents .= "\n\n" . '$settings["container_yamls"][] = "' . drupal_get_path('module', 'redis') . '/example.services.yml";'; 91 | $contents .= "\n\n" . '$settings["cache"] = ' . var_export($settings['cache'], TRUE) . ';'; 92 | $contents .= "\n\n" . '$settings["redis_compress_length"] = 100;'; 93 | 94 | // Add the classloader. 95 | $contents .= "\n\n" . '$class_loader->addPsr4(\'Drupal\\\\redis\\\\\', \'' . drupal_get_path('module', 'redis') . '/src\');'; 96 | 97 | // Add the bootstrap container definition. 98 | $contents .= "\n\n" . '$settings["bootstrap_container_definition"] = ' . var_export($settings['bootstrap_container_definition'], TRUE) . ';'; 99 | 100 | file_put_contents($filename, $contents); 101 | OpCodeCache::invalidate(DRUPAL_ROOT . '/' . $filename); 102 | 103 | // Reset the cache factory. 104 | $this->container->set('cache.factory', NULL); 105 | $this->rebuildContainer(); 106 | 107 | // Get database schema. 108 | $db_schema = Database::getConnection()->schema(); 109 | 110 | // Make sure that the cache and lock tables aren't used. 111 | $db_schema->dropTable('cache_default'); 112 | $db_schema->dropTable('cache_render'); 113 | $db_schema->dropTable('cache_config'); 114 | $db_schema->dropTable('cache_container'); 115 | $db_schema->dropTable('cachetags'); 116 | $db_schema->dropTable('semaphore'); 117 | $db_schema->dropTable('flood'); 118 | } 119 | 120 | /** 121 | * Tests enabling modules and creating configuration. 122 | */ 123 | public function testModuleInstallation() { 124 | $admin_user = $this->createUser([], NULL, TRUE); 125 | $this->drupalLogin($admin_user); 126 | 127 | // Enable a few modules. 128 | $edit["modules[node][enable]"] = TRUE; 129 | $edit["modules[views][enable]"] = TRUE; 130 | $edit["modules[field_ui][enable]"] = TRUE; 131 | $edit["modules[text][enable]"] = TRUE; 132 | $this->drupalPostForm('admin/modules', $edit, t('Install')); 133 | $this->drupalPostForm(NULL, [], t('Continue')); 134 | 135 | $assert = $this->assertSession(); 136 | 137 | // The order of the modules is not guaranteed, so just assert that they are 138 | // all listed. 139 | $assert->elementTextContains('css', '.messages--status', '6 modules have been enabled'); 140 | $assert->elementTextContains('css', '.messages--status', 'Field UI'); 141 | $assert->elementTextContains('css', '.messages--status', 'Node'); 142 | $assert->elementTextContains('css', '.messages--status', 'Text'); 143 | $assert->elementTextContains('css', '.messages--status', 'Views'); 144 | $assert->elementTextContains('css', '.messages--status', 'Field'); 145 | $assert->elementTextContains('css', '.messages--status', 'Filter'); 146 | $assert->checkboxChecked('edit-modules-field-ui-enable'); 147 | 148 | // Create a node type with a field. 149 | $edit = [ 150 | 'name' => $this->randomString(), 151 | 'type' => $node_type = mb_strtolower($this->randomMachineName()), 152 | ]; 153 | $this->drupalPostForm('admin/structure/types/add', $edit, t('Save and manage fields')); 154 | $field_name = mb_strtolower($this->randomMachineName()); 155 | $this->fieldUIAddNewField('admin/structure/types/manage/' . $node_type, $field_name, NULL, 'text'); 156 | 157 | // Create a node, check display, edit, verify that it has been updated. 158 | $edit = [ 159 | 'title[0][value]' => $this->randomMachineName(), 160 | 'body[0][value]' => $this->randomMachineName(), 161 | 'field_' . $field_name . '[0][value]' => $this->randomMachineName(), 162 | ]; 163 | $this->drupalPostForm('node/add/' . $node_type, $edit, t('Save')); 164 | 165 | // Test the output as anonymous user. 166 | $this->drupalLogout(); 167 | $this->drupalGet('node'); 168 | $this->assertSession()->responseContains($edit['title[0][value]']); 169 | 170 | $this->drupalLogin($admin_user); 171 | $this->drupalGet('node'); 172 | $this->clickLink($edit['title[0][value]']); 173 | $this->assertSession()->responseContains($edit['body[0][value]']); 174 | $this->clickLink(t('Edit')); 175 | $update = [ 176 | 'title[0][value]' => $this->randomMachineName(), 177 | ]; 178 | $this->drupalPostForm(NULL, $update, t('Save')); 179 | $this->assertSession()->responseContains($update['title[0][value]']); 180 | $this->drupalGet('node'); 181 | $this->assertSession()->responseContains($update['title[0][value]']); 182 | 183 | $this->drupalLogout(); 184 | $this->drupalGet('node'); 185 | $this->clickLink($update['title[0][value]']); 186 | $this->assertSession()->responseContains($edit['body[0][value]']); 187 | 188 | // Get database schema. 189 | $db_schema = Database::getConnection()->schema(); 190 | $this->assertFalse($db_schema->tableExists('cache_default')); 191 | $this->assertFalse($db_schema->tableExists('cache_render')); 192 | $this->assertFalse($db_schema->tableExists('cache_config')); 193 | $this->assertFalse($db_schema->tableExists('cache_container')); 194 | $this->assertFalse($db_schema->tableExists('cachetags')); 195 | $this->assertFalse($db_schema->tableExists('semaphore')); 196 | $this->assertFalse($db_schema->tableExists('flood')); 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/Controller/ReportController.php: -------------------------------------------------------------------------------- 1 | redis = $client_factory->getClient(); 47 | } 48 | else { 49 | $this->redis = FALSE; 50 | } 51 | 52 | $this->dateFormatter = $date_formatter; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public static function create(ContainerInterface $container) { 59 | return new static($container->get('redis.factory'), $container->get('date.formatter')); 60 | } 61 | 62 | /** 63 | * Redis report overview. 64 | */ 65 | public function overview() { 66 | 67 | $build['report'] = [ 68 | '#theme' => 'status_report', 69 | '#requirements' => [], 70 | ]; 71 | 72 | if ($this->redis === FALSE) { 73 | 74 | $build['report']['#requirements'] = [ 75 | 'client' => [ 76 | 'title' => 'Redis', 77 | 'value' => t('Not connected.'), 78 | 'severity_status' => 'error', 79 | 'description' => t('No Redis client connected. Verify cache settings.'), 80 | ], 81 | ]; 82 | 83 | return $build; 84 | } 85 | 86 | include_once DRUPAL_ROOT . '/core/includes/install.inc'; 87 | 88 | $start = microtime(TRUE); 89 | 90 | $info = $this->redis->info(); 91 | 92 | $prefix_length = strlen($this->getPrefix()) + 1; 93 | 94 | $entries_per_bin = array_fill_keys(\Drupal::getContainer()->getParameter('cache_bins'), 0); 95 | 96 | $required_cached_contexts = \Drupal::getContainer()->getParameter('renderer.config')['required_cache_contexts']; 97 | 98 | $render_cache_totals = []; 99 | $render_cache_contexts = []; 100 | $cache_tags = []; 101 | $i = 0; 102 | $cache_tags_max = FALSE; 103 | foreach ($this->scan($this->getPrefix() . '*') as $key) { 104 | $i++; 105 | $second_colon_pos = mb_strpos($key, ':', $prefix_length); 106 | if ($second_colon_pos !== FALSE) { 107 | $bin = mb_substr($key, $prefix_length, $second_colon_pos - $prefix_length); 108 | if (isset($entries_per_bin[$bin])) { 109 | $entries_per_bin[$bin]++; 110 | } 111 | 112 | if ($bin == 'render') { 113 | $cache_key = mb_substr($key, $second_colon_pos + 1); 114 | 115 | $first_context = mb_strpos($cache_key, '['); 116 | if ($first_context) { 117 | $cache_key_only = mb_substr($cache_key, 0, $first_context - 1); 118 | if (!isset($render_cache_totals[$cache_key_only])) { 119 | $render_cache_totals[$cache_key_only] = 1; 120 | } 121 | else { 122 | $render_cache_totals[$cache_key_only]++; 123 | } 124 | 125 | if (preg_match_all('/\[([a-z0-9:_.]+)\]=([^:]*)/', $cache_key, $matches)) { 126 | foreach ($matches[1] as $index => $context) { 127 | $render_cache_contexts[$cache_key_only][$context][$matches[2][$index]] = $matches[2][$index]; 128 | } 129 | } 130 | } 131 | } 132 | elseif ($bin == 'cachetags') { 133 | $cache_tag = mb_substr($key, $second_colon_pos + 1); 134 | // @todo: Make the max configurable or allow ot override it through 135 | // a query parameter. 136 | if (count($cache_tags) < 50000) { 137 | $cache_tags[$cache_tag] = $this->redis->get($key); 138 | } 139 | else { 140 | $cache_tags_max = TRUE; 141 | } 142 | } 143 | } 144 | 145 | // Do not process more than 100k cache keys. 146 | // @todo Adjust this after more testing or move to a separate page. 147 | } 148 | 149 | arsort($entries_per_bin); 150 | arsort($render_cache_totals); 151 | arsort($cache_tags); 152 | 153 | $per_bin_string = ''; 154 | foreach ($entries_per_bin as $bin => $entries) { 155 | $per_bin_string .= "$bin: $entries