├── .gitignore ├── .travis-before-script.sh ├── .travis.yml ├── README.PhpRedis.txt ├── README.Predis.txt ├── README.md ├── composer.json ├── example.services.yml ├── redis.info.yml ├── redis.install ├── redis.links.menu.yml ├── redis.module ├── redis.routing.yml ├── redis.services.yml ├── src ├── Cache │ ├── CacheBackendFactory.php │ ├── CacheBase.php │ ├── PhpRedis.php │ ├── Predis.php │ └── RedisCacheTagsChecksum.php ├── Client │ ├── PhpRedis.php │ └── Predis.php ├── ClientFactory.php ├── ClientInterface.php ├── Controller │ └── ReportController.php ├── Flood │ ├── FloodFactory.php │ ├── PhpRedis.php │ └── Predis.php ├── Lock │ ├── LockFactory.php │ ├── PhpRedis.php │ └── Predis.php ├── PersistentLock │ ├── PhpRedis.php │ └── Predis.php ├── Queue │ ├── PhpRedis.php │ ├── Predis.php │ ├── QueueBase.php │ ├── QueueRedisFactory.php │ ├── ReliablePhpRedis.php │ ├── ReliablePredis.php │ ├── ReliableQueueBase.php │ └── ReliableQueueRedisFactory.php └── RedisPrefixTrait.php └── tests └── src ├── Functional ├── Lock │ └── RedisLockFunctionalTest.php └── WebTest.php ├── Kernel ├── RedisCacheTest.php ├── RedisFloodTest.php ├── RedisLockTest.php └── RedisQueueTest.php └── Traits └── RedisTestInterfaceTrait.php /.gitignore: -------------------------------------------------------------------------------- 1 | predis 2 | -------------------------------------------------------------------------------- /.travis-before-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e $DRUPAL_TI_DEBUG 4 | 5 | # Ensure the right Drupal version is installed. 6 | # Note: This function is re-entrant. 7 | drupal_ti_ensure_drupal 8 | 9 | # Add needed dependencies. 10 | cd "$DRUPAL_TI_DRUPAL_DIR" 11 | 12 | # Download predis 13 | composer require predis/predis 14 | 15 | 16 | # These variables come from environments/drupal-*.sh 17 | mkdir -p "$DRUPAL_TI_MODULES_PATH" 18 | cd "$DRUPAL_TI_MODULES_PATH" 19 | 20 | #Enable php-redis 21 | echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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'; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redis backends 2 | ==================== 3 | 4 | This package provides two different Redis backends. If you want to use 5 | Redis as cache backend, you have to choose one of the two, but you cannot use 6 | both at the same time. 7 | 8 | PhpRedis 9 | -------- 10 | 11 | This implementation uses the PhpRedis PHP extension. In order to use it, you 12 | will need to compile the extension yourself. 13 | 14 | Predis 15 | ------ 16 | 17 | Support for the Predis PHP library is experimental, but feel free to try it out. 18 | You can install the required library using composer. Check out the README.Predis.txt file 19 | for more information. 20 | 21 | Important notice 22 | ---------------- 23 | 24 | This module requires at least Redis 2.4, additionally, the lock backend 25 | requires Redis 2.6 to support millisecond timeouts and atomic lock operations. 26 | 27 | Getting started 28 | =============== 29 | 30 | Quick setup 31 | ----------- 32 | 33 | Here is a simple yet working easy way to setup the module. 34 | 35 | This method will allow Drupal to use Redis for all caches. 36 | 37 | $settings['redis.connection']['interface'] = 'PhpRedis'; // Can be "Predis". 38 | $settings['redis.connection']['host'] = '1.2.3.4'; // Your Redis instance hostname. 39 | $settings['cache']['default'] = 'cache.backend.redis'; 40 | 41 | To use some Predis goodness, including a redis primary/replica setup you can use a config like this. 42 | 43 | $settings['redis.connection']['interface'] = 'Predis'; // Use predis library. 44 | $settings['redis.connection']['replication'] = TRUE; // Turns on replication. 45 | $settings['redis.connection']['replication.host'][1]['host'] = '1.2.3.4'; // Your Redis instance hostname. 46 | $settings['redis.connection']['replication.host'][1]['port'] = '6379'; // Only required if using non-standard port. 47 | $settings['redis.connection']['replication.host'][1]['role'] = 'primary'; // The redis instance role. 48 | $settings['redis.connection']['replication.host'][2]['host'] = '1.2.3.5'; 49 | $settings['redis.connection']['replication.host'][2]['port'] = '6379'; 50 | $settings['redis.connection']['replication.host'][2]['role'] = 'replica'; 51 | $settings['redis.connection']['replication.host'][3]['host'] = '1.2.3.6'; 52 | $settings['redis.connection']['replication.host'][3]['port'] = '6379'; 53 | $settings['redis.connection']['replication.host'][3]['role'] = 'replica'; 54 | $settings['cache']['default'] = 'cache.backend.redis'; 55 | 56 | Either include the default example.services.yml from the module, which will 57 | replace all supported backend services (that currently includes the cache tags 58 | checksum service and the lock backends, check the file for the current list) 59 | or copy the service definitions into a site specific services.yml. 60 | 61 | $settings['container_yamls'][] = 'modules/redis/example.services.yml'; 62 | 63 | Note that for any of this, the redis module must be enabled. See next chapters 64 | for more information. 65 | 66 | Is there any cache bins that should *never* go into Redis? 67 | ---------------------------------------------------------- 68 | 69 | TL;DR: No. 70 | 71 | Redis has been maturing a lot over time, and will apply different sensible 72 | settings for different bins; It's today very stable. 73 | 74 | Advanced configuration 75 | ====================== 76 | 77 | Choose the Redis client library to use 78 | -------------------------------------- 79 | 80 | Note: This is not yet supported, only the PhpRedis interface is available. 81 | 82 | Add into your settings.php file: 83 | 84 | $settings['redis.connection']['interface'] = 'PhpRedis'; 85 | 86 | You can replace 'PhpRedis' with 'Predis', depending on the library you chose. 87 | 88 | 89 | Tell Drupal to use the cache backend 90 | ------------------------------------ 91 | 92 | Usual cache backend configuration, as follows, to add into your settings.php 93 | file like any other backend: 94 | 95 | # Use for all bins otherwise specified. 96 | $settings['cache']['default'] = 'cache.backend.redis'; 97 | 98 | # Use this to only use it for specific cache bins. 99 | $settings['cache']['bins']['render'] = 'cache.backend.redis'; 100 | 101 | Tell Drupal to use the lock backend 102 | ----------------------------------- 103 | 104 | See the provided example.services.yml file on how to override the lock services. 105 | 106 | Tell Drupal to use the queue backend 107 | ------------------------------------ 108 | 109 | This module provides reliable and non-reliable queue implementations. Depending 110 | on which is to be use you need to choose "queue.redis" or "queue.redis_reliable" 111 | as a service name. 112 | 113 | When you have configured basic information (host, library, ... - see Quick setup) 114 | add this to your settings.php file: 115 | 116 | # Use for all queues unless otherwise specified for a specific queue. 117 | $settings['queue_default'] = 'queue.redis'; 118 | 119 | # Or if you want to use reliable queue implementation. 120 | $settings['queue_default'] = 'queue.redis_reliable'; 121 | 122 | 123 | # Use this to only use Redis for a specific queue (aggregator_feeds in this case). 124 | $settings['queue_service_aggregator_feeds'] = 'queue.redis'; 125 | 126 | # Or if you want to use reliable queue implementation. 127 | $settings['queue_service_aggregator_feeds'] = 'queue.redis_reliable'; 128 | 129 | 130 | Common settings 131 | =============== 132 | 133 | Connect to a remote host 134 | ------------------------ 135 | 136 | If your Redis instance is remote, you can use this syntax: 137 | 138 | $settings['redis.connection']['interface'] = 'PhpRedis'; // Can be "Predis". 139 | $settings['redis.connection']['host'] = '1.2.3.4'; // Your Redis instance hostname. 140 | $settings['redis.connection']['port'] = '6379'; // Redis port 141 | 142 | Port is optional, default is 6379 (default Redis port). 143 | 144 | Compression 145 | ------------------------- 146 | Compressing the data stored in redis can massively reduce the nedeed storage. 147 | 148 | To enable, set the minimal length after which the cached data should be 149 | compressed: 150 | 151 | $settings['redis_compress_length'] = 100; 152 | 153 | By default, compression level 1 is used, which provides considerable storage 154 | optimization with minimal CPU overhead, to change: 155 | 156 | $settings['redis_compress_level'] = 6; 157 | 158 | Using a specific database 159 | ------------------------- 160 | 161 | Per default, Redis ships the database "0". All default connections will be use 162 | this one if nothing is specified. 163 | 164 | Depending on you OS or OS distribution, you might have numerous database. To 165 | use one in particular, just add to your settings.php file: 166 | 167 | $settings['redis.connection']['base'] = 12; 168 | 169 | Connection to a password protected instance 170 | ------------------------------------------- 171 | 172 | If you are using a password protected instance, specify the password this way: 173 | 174 | $settings['redis.connection']['password'] = "mypassword"; 175 | 176 | Depending on the backend, using a wrong auth will behave differently: 177 | 178 | - Predis will throw an exception and make Drupal fail during early boostrap. 179 | 180 | - PhpRedis will make Redis calls silent and creates some PHP warnings, thus 181 | Drupal will behave as if it was running with a null cache backend (no cache 182 | at all). 183 | 184 | Prefixing site cache entries (avoiding sites name collision) 185 | ------------------------------------------------------------ 186 | 187 | If you need to differentiate multiple sites using the same Redis instance and 188 | database, you will need to specify a prefix for your site cache entries. 189 | 190 | Cache prefix configuration attempts to use a unified variable across contrib 191 | backends that support this feature. This variable name is 'cache_prefix'. 192 | 193 | This variable is polymorphic, the simplest version is to provide a raw string 194 | that will be the default prefix for all cache bins: 195 | 196 | $settings['cache_prefix'] = 'mysite_'; 197 | 198 | Alternatively, to provide the same functionality, you can provide the variable 199 | as an array: 200 | 201 | $settings['cache_prefix']['default'] = 'mysite_'; 202 | 203 | This allows you to provide different prefix depending on the bin name. Common 204 | usage is that each key inside the 'cache_prefix' array is a bin name, the value 205 | the associated prefix. If the value is FALSE, then no prefix is 206 | used for this bin. 207 | 208 | The 'default' meta bin name is provided to define the default prefix for non 209 | specified bins. It behaves like the other names, which means that an explicit 210 | FALSE will order the backend not to provide any prefix for any non specified 211 | bin. 212 | 213 | Here is a complex sample: 214 | 215 | // Default behavior for all bins, prefix is 'mysite_'. 216 | $settings['cache_prefix']['default'] = 'mysite_'; 217 | 218 | // Set no prefix explicitely for 'cache' and 'cache_bootstrap' bins. 219 | $settings['cache_prefix']['cache'] = FALSE; 220 | $settings['cache_prefix']['cache_bootstrap'] = FALSE; 221 | 222 | // Set another prefix for 'cache_menu' bin. 223 | $settings['cache_prefix']['cache_menu'] = 'menumysite_'; 224 | 225 | Note that if you don't specify the default behavior, the Redis module will 226 | attempt to use the HTTP_HOST variable in order to provide a multisite safe 227 | default behavior. Notice that this is not failsafe, in such environment you 228 | are strongly advised to set at least an explicit default prefix. 229 | 230 | Note that this last notice is Redis only specific, because per default Redis 231 | server will not namespace data, thus sharing an instance for multiple sites 232 | will create conflicts. This is not true for every contributed backends. 233 | 234 | Flush mode 235 | ---------- 236 | 237 | @todo: Update for Drupal 8 238 | 239 | Redis allows to set a time-to-live at the key level, which frees us from 240 | handling the garbage collection at clear() calls; Unfortunately Drupal never 241 | explicitely clears single cached pages or blocks. If you didn't configure the 242 | "cache_lifetime" core variable, its value is "0" which means that temporary 243 | items never expire: in this specific case, we need to adopt a different 244 | behavior than leting Redis handling the TTL by itself; This is why we have 245 | three different implementations of the flush algorithm you can use: 246 | 247 | * 0: Never flush temporary: leave Redis handling the TTL; This mode is 248 | not compatible for the "page" and "block" bins but is the default for 249 | all others. 250 | 251 | * 1: Keep a copy of temporary items identifiers in a SET and flush them 252 | accordingly to spec (DatabaseCache default backend mimic behavior): 253 | this is the default for "page" and "block" bin if you don't change the 254 | configuration. 255 | 256 | * 2: Flush everything including permanent or valid items on clear() calls: 257 | this behavior mimics the pre-1.0 releases of this module. Use it only 258 | if you experience backward compatibility problems on a production 259 | environement - at the cost of potential performance issues; All other 260 | users should ignore this parameter. 261 | 262 | You can configure a default flush mode which will override the sensible 263 | provided defaults by setting the 'redis_flush_mode' variable. 264 | 265 | // For example this is the safer mode. 266 | $conf['redis_flush_mode'] = 1; 267 | 268 | But you may also want to change the behavior for only a few bins. 269 | 270 | // This will put mode 0 on "bootstrap" bin. 271 | $conf['redis_flush_mode_cache_bootstrap'] = 0; 272 | 273 | // And mode 2 to "page" bin. 274 | $conf['redis_flush_mode_cache_page'] = 2; 275 | 276 | Note that you must prefix your bins with "cache" as the Drupal 7 bin naming 277 | convention requires it. 278 | 279 | Keep in mind that defaults will provide the best balance between performance 280 | and safety for most sites; Non advanced users should ever change them. 281 | 282 | Default lifetime for permanent items 283 | ------------------------------------ 284 | 285 | @todo: Update for Drupal 8 286 | 287 | Redis when reaching its maximum memory limit will stop writing data in its 288 | storage engine: this is a feature that avoid the Redis server crashing when 289 | there is no memory left on the machine. 290 | 291 | As a workaround, Redis can be configured as a LRU cache for both volatile or 292 | permanent items, which means it can behave like Memcache; Problem is that if 293 | you use Redis as a permanent storage for other business matters than this 294 | module you cannot possibly configure it to drop permanent items or you'll 295 | loose data. 296 | 297 | This workaround allows you to explicity set a very long or configured default 298 | lifetime for CACHE_PERMANENT items (that would normally be permanent) which 299 | will mark them as being volatile in Redis storage engine: this then allows you 300 | to configure a LRU behavior for volatile keys without engaging the permenent 301 | business stuff in a dangerous LRU mechanism; Cache items even if permament will 302 | be dropped when unused using this. 303 | 304 | Per default the TTL for permanent items will set to safe-enough value which is 305 | one year; No matter how Redis will be configured default configuration or lazy 306 | admin will inherit from a safe module behavior with zero-conf. 307 | 308 | For advanturous people, you can manage the TTL on a per bin basis and change 309 | the default one: 310 | 311 | // Make CACHE_PERMANENT items being permanent once again 312 | // 0 is a special value usable for all bins to explicitely tell the 313 | // cache items will not be volatile in Redis. 314 | $conf['redis_perm_ttl'] = 0; 315 | 316 | // Make them being volatile with a default lifetime of 1 year. 317 | $conf['redis_perm_ttl'] = "1 year"; 318 | 319 | // You can override on a per-bin basis; 320 | // For example make cached field values live only 3 monthes: 321 | $conf['redis_perm_ttl_cache_field'] = "3 months"; 322 | 323 | // But you can also put a timestamp in there; In this case the 324 | // value must be a STRICTLY TYPED integer: 325 | $conf['redis_perm_ttl_cache_field'] = 2592000; // 30 days. 326 | 327 | Time interval string will be parsed using DateInterval::createFromDateString 328 | please refer to its documentation: 329 | 330 | http://www.php.net/manual/en/dateinterval.createfromdatestring.php 331 | 332 | Last but not least please be aware that this setting affects the 333 | CACHE_PERMANENT ONLY; All other use cases (CACHE_TEMPORARY or user set TTL 334 | on single cache entries) will continue to behave as documented in Drupal core 335 | cache backend documentation. 336 | 337 | Lock backends 338 | ------------- 339 | 340 | @todo: Update for Drupal 8 341 | 342 | Both implementations provides a Redis lock backend. Redis lock backend proved to 343 | be faster than the default SQL based one when using both servers on the same box. 344 | 345 | Both backends, thanks to the Redis WATCH, MULTI and EXEC commands provides a 346 | real race condition free mutexes if you use Redis >= 2.1.0. 347 | 348 | Testing 349 | ======= 350 | 351 | I did not find any hint about making tests being configurable, so per default 352 | the tested Redis server must always be on localhost with default configuration. 353 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.install: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /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.module: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /redis.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cache.backend.redis: 3 | class: Drupal\redis\Cache\CacheBackendFactory 4 | arguments: ['@redis.factory', '@cache_tags.invalidator.checksum', '@serialization.phpserialize'] 5 | redis.factory: 6 | class: Drupal\redis\ClientFactory 7 | redis.lock.factory: 8 | class: Drupal\redis\Lock\LockFactory 9 | arguments: ['@redis.factory'] 10 | redis.flood.factory: 11 | class: Drupal\redis\Flood\FloodFactory 12 | arguments: ['@redis.factory', '@request_stack'] 13 | queue.redis_reliable: 14 | class: Drupal\redis\Queue\ReliableQueueRedisFactory 15 | arguments: ['@redis.factory', '@settings'] 16 | queue.redis: 17 | class: Drupal\redis\Queue\QueueRedisFactory 18 | arguments: ['@redis.factory', '@settings'] 19 | -------------------------------------------------------------------------------- /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/Cache/CacheBase.php: -------------------------------------------------------------------------------- 1 | permTtl; 114 | } 115 | 116 | /** 117 | * CacheBase constructor. 118 | * @param $bin 119 | * The cache bin for which the object is created. 120 | * @param \Drupal\Component\Serialization\SerializationInterface $serializer 121 | * The serialization class to use. 122 | */ 123 | public function __construct($bin, SerializationInterface $serializer) { 124 | $this->bin = $bin; 125 | $this->serializer = $serializer; 126 | $this->setPermTtl(); 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public function get($cid, $allow_invalid = FALSE) { 133 | $cids = [$cid]; 134 | $cache = $this->getMultiple($cids, $allow_invalid); 135 | return reset($cache); 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function setMultiple(array $items) { 142 | foreach ($items as $cid => $item) { 143 | $this->set($cid, $item['data'], isset($item['expire']) ? $item['expire'] : CacheBackendInterface::CACHE_PERMANENT, isset($item['tags']) ? $item['tags'] : []); 144 | } 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function delete($cid) { 151 | $this->deleteMultiple([$cid]); 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function deleteMultiple(array $cids) { 158 | $in_transaction = \Drupal::database()->inTransaction(); 159 | if ($in_transaction) { 160 | if (empty($this->delayedDeletions)) { 161 | \Drupal::database()->addRootTransactionEndCallback([$this, 'postRootTransactionCommit']); 162 | } 163 | $this->delayedDeletions = array_unique(array_merge($this->delayedDeletions, $cids)); 164 | } 165 | else { 166 | $this->doDeleteMultiple($cids); 167 | } 168 | } 169 | 170 | /** 171 | * Execute the deletion. 172 | * 173 | * This can be delayed to avoid race conditions. 174 | * 175 | * @param array $cids 176 | * An array of cache IDs to delete. 177 | * 178 | * @see static::deleteMultiple() 179 | */ 180 | protected abstract function doDeleteMultiple(array $cids); 181 | 182 | /** 183 | * Callback to be invoked after a database transaction gets committed. 184 | * 185 | * Invalidates all delayed cache deletions. 186 | * 187 | * @param bool $success 188 | * Whether or not the transaction was successful. 189 | */ 190 | public function postRootTransactionCommit($success) { 191 | if ($success) { 192 | $this->doDeleteMultiple($this->delayedDeletions); 193 | } 194 | $this->delayedDeletions = []; 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function removeBin() { 201 | $this->deleteAll(); 202 | } 203 | 204 | /** 205 | * {@inheritdoc} 206 | */ 207 | public function invalidate($cid) { 208 | $this->invalidateMultiple([$cid]); 209 | } 210 | 211 | /** 212 | * Return the key for the given cache key. 213 | */ 214 | public function getKey($cid = NULL) { 215 | if (NULL === $cid) { 216 | return $this->getPrefix() . ':' . $this->bin; 217 | } 218 | else { 219 | return $this->getPrefix() . ':' . $this->bin . ':' . $cid; 220 | } 221 | } 222 | 223 | /** 224 | * Calculate the correct expiration time. 225 | * 226 | * @param int $expire 227 | * The expiration time provided for the cache set. 228 | * 229 | * @return int 230 | * The default expiration if expire is PERMANENT or higher than the default. 231 | * May return negative values if the item is already expired. 232 | */ 233 | protected function getExpiration($expire) { 234 | if ($expire == Cache::PERMANENT || $expire > $this->permTtl) { 235 | return $this->permTtl; 236 | } 237 | return $expire - \Drupal::time()->getRequestTime(); 238 | } 239 | /** 240 | * Return the key for the tag used to specify the bin of cache-entries. 241 | */ 242 | protected function getTagForBin() { 243 | return 'x-redis-bin:' . $this->bin; 244 | } 245 | 246 | /** 247 | * Set the minimum TTL (unit testing only). 248 | */ 249 | public function setMinTtl($ttl) { 250 | $this->minTtl = $ttl; 251 | } 252 | 253 | /** 254 | * Set the permanent TTL. 255 | */ 256 | public function setPermTtl($ttl = NULL) { 257 | if (isset($ttl)) { 258 | $this->permTtl = $ttl; 259 | } 260 | else { 261 | // Attempt to set from settings. 262 | if (($settings = Settings::get('redis.settings', [])) && isset($settings['perm_ttl_' . $this->bin])) { 263 | $ttl = $settings['perm_ttl_' . $this->bin]; 264 | if ($ttl === (int) $ttl) { 265 | $this->permTtl = $ttl; 266 | } 267 | else { 268 | if ($iv = DateInterval::createFromDateString($ttl)) { 269 | // http://stackoverflow.com/questions/14277611/convert-dateinterval-object-to-seconds-in-php 270 | $this->permTtl = ($iv->y * 31536000 + $iv->m * 2592000 + $iv->days * 86400 + $iv->h * 3600 + $iv->i * 60 + $iv->s); 271 | } 272 | else { 273 | // Log error about invalid ttl. 274 | trigger_error(sprintf("Parsed TTL '%s' has an invalid value: switching to default", $ttl)); 275 | $this->permTtl = self::LIFETIME_PERM_DEFAULT; 276 | } 277 | 278 | } 279 | } 280 | } 281 | } 282 | 283 | /** 284 | * Prepares a cached item. 285 | * 286 | * Checks that items are either permanent or did not expire, and unserializes 287 | * data as appropriate. 288 | * 289 | * @param array $values 290 | * The hash returned from redis or false. 291 | * @param bool $allow_invalid 292 | * If FALSE, the method returns FALSE if the cache item is not valid. 293 | * 294 | * @return mixed|false 295 | * The item with data unserialized as appropriate and a property indicating 296 | * whether the item is valid, or FALSE if there is no valid item to load. 297 | */ 298 | protected function expandEntry(array $values, $allow_invalid) { 299 | // Check for entry being valid. 300 | if (empty($values['cid'])) { 301 | return FALSE; 302 | } 303 | 304 | // Ignore items that are scheduled for deletion. 305 | if (in_array($values['cid'], $this->delayedDeletions)) { 306 | return FALSE; 307 | } 308 | 309 | $cache = (object) $values; 310 | 311 | $cache->tags = explode(' ', $cache->tags); 312 | 313 | // Check expire time, allow to have a cache invalidated explicitly, don't 314 | // check if already invalid. 315 | if ($cache->valid) { 316 | $cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME; 317 | 318 | // Check if invalidateTags() has been called with any of the items's tags. 319 | if ($cache->valid && !$this->checksumProvider->isValid($cache->checksum, $cache->tags)) { 320 | $cache->valid = FALSE; 321 | } 322 | } 323 | 324 | // Ensure the entry does not predate the last delete all time. 325 | $last_delete_timestamp = $this->getLastDeleteAll(); 326 | if ($last_delete_timestamp && ((float)$values['created']) < $last_delete_timestamp) { 327 | return FALSE; 328 | } 329 | 330 | if (!$allow_invalid && !$cache->valid) { 331 | return FALSE; 332 | } 333 | 334 | if (!empty($cache->gz)) { 335 | // Uncompress, suppress warnings e.g. for broken CRC32. 336 | $cache->data = @gzuncompress($cache->data); 337 | // In such cases, void the cache entry. 338 | if ($cache->data === FALSE) { 339 | return FALSE; 340 | } 341 | } 342 | 343 | if ($cache->serialized) { 344 | $cache->data = $this->serializer->decode($cache->data); 345 | } 346 | 347 | return $cache; 348 | } 349 | 350 | /** 351 | * Create cache entry. 352 | * 353 | * @param string $cid 354 | * @param mixed $data 355 | * @param int $expire 356 | * @param string[] $tags 357 | * 358 | * @return array 359 | */ 360 | protected function createEntryHash($cid, $data, $expire = Cache::PERMANENT, array $tags) { 361 | // Always add a cache tag for the current bin, so that we can use that for 362 | // invalidateAll(). 363 | $tags[] = $this->getTagForBin(); 364 | assert(Inspector::assertAllStrings($tags), 'Cache Tags must be strings.'); 365 | $hash = [ 366 | 'cid' => $cid, 367 | 'created' => round(microtime(TRUE), 3), 368 | 'expire' => $expire, 369 | 'tags' => implode(' ', $tags), 370 | 'valid' => 1, 371 | 'checksum' => $this->checksumProvider->getCurrentChecksum($tags), 372 | ]; 373 | 374 | // Let Redis handle the data types itself. 375 | if (!is_string($data)) { 376 | $hash['data'] = $this->serializer->encode($data); 377 | $hash['serialized'] = 1; 378 | } 379 | else { 380 | $hash['data'] = $data; 381 | $hash['serialized'] = 0; 382 | } 383 | 384 | if (Settings::get('redis_compress_length', 0) && strlen($hash['data']) > Settings::get('redis_compress_length', 0)) { 385 | $hash['data'] = @gzcompress($hash['data'], Settings::get('redis_compress_level', 1)); 386 | $hash['gz'] = TRUE; 387 | } 388 | 389 | return $hash; 390 | } 391 | /** 392 | * {@inheritdoc} 393 | */ 394 | public function invalidateMultiple(array $cids) { 395 | // Loop over all cache items, they are stored as a hash, so we can access 396 | // the valid flag directly, only write if it exists and is not 0. 397 | foreach ($cids as $cid) { 398 | $key = $this->getKey($cid); 399 | if ($this->client->hGet($key, 'valid')) { 400 | $this->client->hSet($key, 'valid', 0); 401 | } 402 | } 403 | } 404 | 405 | /** 406 | * {@inheritdoc} 407 | */ 408 | public function invalidateAll() { 409 | // To invalidate the whole bin, we invalidate a special tag for this bin. 410 | $this->checksumProvider->invalidateTags([$this->getTagForBin()]); 411 | } 412 | 413 | /** 414 | * {@inheritdoc} 415 | */ 416 | public function garbageCollection() { 417 | // @todo Do we need to do anything here? 418 | } 419 | 420 | /** 421 | * Returns the last delete all timestamp. 422 | * 423 | * @return float 424 | * The last delete timestamp as a timestamp with a millisecond precision. 425 | */ 426 | protected function getLastDeleteAll() { 427 | // Cache the last delete all timestamp. 428 | if ($this->lastDeleteAll === NULL) { 429 | $this->lastDeleteAll = (float) $this->client->get($this->getKey(static::LAST_DELETE_ALL_KEY)); 430 | } 431 | return $this->lastDeleteAll; 432 | } 433 | 434 | /** 435 | * {@inheritdoc} 436 | */ 437 | public function deleteAll() { 438 | // The last delete timestamp is in milliseconds, ensure that no cache 439 | // was written in the same millisecond. 440 | // @todo This is needed to make the tests pass, is this safe enough for real 441 | // usage? 442 | usleep(1000); 443 | $this->lastDeleteAll = round(microtime(TRUE), 3); 444 | $this->client->set($this->getKey(static::LAST_DELETE_ALL_KEY), $this->lastDeleteAll); 445 | } 446 | 447 | } 448 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/ClientInterface.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
"; 156 | } 157 | 158 | $render_cache_string = ''; 159 | foreach (array_slice($render_cache_totals, 0, 50) as $cache_key => $total) { 160 | $contexts = implode(', ', array_diff(array_keys($render_cache_contexts[$cache_key]), $required_cached_contexts)); 161 | $render_cache_string .= $contexts ? "$cache_key: $total ($contexts)
" : "$cache_key: $total
"; 162 | } 163 | 164 | $cache_tags_string = ''; 165 | foreach (array_slice($cache_tags, 0, 50) as $cache_tag => $invalidations) { 166 | $cache_tags_string .= "$cache_tag: $invalidations
"; 167 | } 168 | 169 | $end = microtime(TRUE); 170 | $memory_config = $this->redis->config('get', 'maxmemory*'); 171 | 172 | if ($memory_config['maxmemory']) { 173 | $memory_value = $this->t('@used_memory / @max_memory (@used_percentage%), maxmemory policy: @policy', [ 174 | '@used_memory' => $info['used_memory_human'], 175 | '@max_memory' => format_size($memory_config['maxmemory']), 176 | '@used_percentage' => (int) ($info['used_memory'] / $memory_config['maxmemory'] * 100), 177 | '@policy' => $memory_config['maxmemory-policy'], 178 | ]); 179 | } 180 | else { 181 | $memory_value = $this->t('@used_memory / unlimited, maxmemory policy: @policy', [ 182 | '@used_memory' => $info['used_memory_human'] ?? $info['Memory']['used_memory_human'], 183 | '@policy' => $memory_config['maxmemory-policy'], 184 | ]); 185 | } 186 | 187 | $requirements = [ 188 | 'client' => [ 189 | 'title' => $this->t('Client'), 190 | 'value' => t("Connected, using the @name client.", ['@name' => ClientFactory::getClientName()]), 191 | ], 192 | 'version' => [ 193 | 'title' => $this->t('Version'), 194 | 'value' => $info['redis_version'] ?? $info['Server']['redis_version'], 195 | ], 196 | 'clients' => [ 197 | 'title' => $this->t('Connected clients'), 198 | 'value' => $info['connected_clients'] ?? $info['Clients']['connected_clients'], 199 | ], 200 | 'dbsize' => [ 201 | 'title' => $this->t('Keys'), 202 | 'value' => $this->redis->dbSize(), 203 | ], 204 | 'memory' => [ 205 | 'title' => $this->t('Memory'), 206 | 'value' => $memory_value, 207 | ], 208 | 'uptime' => [ 209 | 'title' => $this->t('Uptime'), 210 | 'value' => $this->dateFormatter->formatInterval($info['uptime_in_seconds'] ?? $info['Server']['uptime_in_seconds']), 211 | ], 212 | 'read_write' => [ 213 | 'title' => $this->t('Read/Write'), 214 | 'value' => $this->t('@read read (@percent_read%), @write written (@percent_write%), @commands commands in @connections connections.', [ 215 | '@read' => format_size($info['total_net_output_bytes'] ?? $info['Stats']['total_net_output_bytes']), 216 | '@percent_read' => round(100 / (($info['total_net_output_bytes'] ?? $info['Stats']['total_net_output_bytes']) + ($info['total_net_input_bytes'] ?? $info['Stats']['total_net_input_bytes'])) * ($info['total_net_output_bytes'] ?? $info['Stats']['total_net_output_bytes'])), 217 | '@write' => format_size($info['total_net_input_bytes'] ?? $info['Stats']['total_net_input_bytes']), 218 | '@percent_write' => round(100 / (($info['total_net_output_bytes'] ?? $info['Stats']['total_net_output_bytes']) + ($info['total_net_input_bytes'] ?? $info['Stats']['total_net_input_bytes'])) * ($info['total_net_input_bytes'] ?? $info['Stats']['total_net_input_bytes'])), 219 | '@commands' => $info['total_commands_processed'] ?? $info['Stats']['total_commands_processed'], 220 | '@connections' => $info['total_connections_received'] ?? $info['Stats']['total_connections_received'], 221 | ]), 222 | ], 223 | 'per_bin' => [ 224 | 'title' => $this->t('Keys per cache bin'), 225 | 'value' => ['#markup' => $per_bin_string], 226 | ], 227 | 'render_cache' => [ 228 | 'title' => $this->t('Render cache entries with most variations'), 229 | 'value' => ['#markup' => $render_cache_string], 230 | ], 231 | 'cache_tags' => [ 232 | 'title' => $this->t('Most invalidated cache tags'), 233 | 'value' => ['#markup' => $cache_tags_string], 234 | ], 235 | 'cache_tag_totals' => [ 236 | 'title' => $this->t('Total cache tag invalidations'), 237 | 'value' => [ 238 | '#markup' => $this->t('@count tags with @invalidations invalidations.', [ 239 | '@count' => count($cache_tags), 240 | '@invalidations' => array_sum($cache_tags), 241 | ]), 242 | ], 243 | ], 244 | 'time_spent' => [ 245 | 'title' => $this->t('Time spent'), 246 | 'value' => ['#markup' => $this->t('@count keys in @time seconds.', ['@count' => $i, '@time' => round(($end - $start), 4)])], 247 | ], 248 | ]; 249 | 250 | // Warnings/hints. 251 | if ($memory_config['maxmemory-policy'] == 'noeviction') { 252 | $redis_url = Url::fromUri('https://redis.io/topics/lru-cache', [ 253 | 'fragment' => 'eviction-policies', 254 | 'attributes' => [ 255 | 'target' => '_blank', 256 | ], 257 | ]); 258 | $requirements['memory']['severity_status'] = 'warning'; 259 | $requirements['memory']['description'] = $this->t('It is recommended to configure the maxmemory policy to e.g. volatile-lru, see Redis documentation.', [ 260 | ':documentation_url' => $redis_url->toString(), 261 | ]); 262 | } 263 | if (count($cache_tags) == 0) { 264 | $requirements['cache_tag_totals']['severity_status'] = 'warning'; 265 | $requirements['cache_tag_totals']['description'] = $this->t('No cache tags found, make sure that the redis cache tag checksum service is used. See example.services.yml on root of this module.'); 266 | unset($requirements['cache_tags']); 267 | } 268 | 269 | if ($cache_tags_max) { 270 | $requirements['max_cache_tags'] = [ 271 | 'severity_status' => 'warning', 272 | 'title' => $this->t('Cache tags limit reached'), 273 | 'value' => ['#markup' => $this->t('Cache tag count incomplete, only counted @count cache tags.', ['@count' => count($cache_tags)])], 274 | ]; 275 | } 276 | 277 | $build['report']['#requirements'] = $requirements; 278 | 279 | return $build; 280 | } 281 | 282 | /** 283 | * Wrapper to SCAN through matching redis keys. 284 | * 285 | * @param string $match 286 | * The MATCH pattern. 287 | * @param int $count 288 | * Count of keys per iteration (only a suggestion to Redis). 289 | * 290 | * @return \Generator 291 | */ 292 | protected function scan($match, $count = 10000) { 293 | $it = NULL; 294 | if ($this->redis instanceof \Redis) { 295 | while ($keys = $this->redis->scan($it, $this->getPrefix() . '*', $count)) { 296 | yield from $keys; 297 | } 298 | } 299 | elseif ($this->redis instanceof \Predis\Client) { 300 | yield from new Keyspace($this->redis, $match, $count); 301 | } 302 | } 303 | 304 | } 305 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/Queue/ReliableQueueBase.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/src/Traits/RedisTestInterfaceTrait.php: -------------------------------------------------------------------------------- 1 |