├── .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