added redis module

This commit is contained in:
Bachir Soussi Chiadmi
2018-01-23 21:13:49 +01:00
parent fdb2089ae1
commit 10c7c59e05
44 changed files with 4225 additions and 0 deletions
@@ -0,0 +1 @@
predis
@@ -0,0 +1,21 @@
#!/bin/bash
set -e $DRUPAL_TI_DEBUG
# Ensure the right Drupal version is installed.
# Note: This function is re-entrant.
drupal_ti_ensure_drupal
# Add needed dependencies.
cd "$DRUPAL_TI_DRUPAL_DIR"
# Download predis
composer require predis/predis
# These variables come from environments/drupal-*.sh
mkdir -p "$DRUPAL_TI_MODULES_PATH"
cd "$DRUPAL_TI_MODULES_PATH"
#Enable php-redis
echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
@@ -0,0 +1,119 @@
# @file
# .travis.yml - Drupal for Travis CI Integration
#
# Template provided by https://github.com/LionsAd/drupal_ti.
#
# Based for simpletest upon:
# https://github.com/sonnym/travis-ci-drupal-module-example
language: php
php:
- 5.5
- 5.6
- 7
- hhvm
matrix:
fast_finish: true
allow_failures:
- php: hhvm
env:
global:
# add composer's global bin directory to the path
# see: https://github.com/drush-ops/drush#install---composer
- PATH="$PATH:$HOME/.composer/vendor/bin"
# Configuration variables.
- DRUPAL_TI_MODULE_NAME="redis"
- DRUPAL_TI_SIMPLETEST_GROUP="redis"
# Define runners and environment vars to include before and after the
# main runners / environment vars.
#- DRUPAL_TI_SCRIPT_DIR_BEFORE="./.drupal_ti/before"
#- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after"
# The environment to use, supported are: drupal-7, drupal-8
- DRUPAL_TI_ENVIRONMENT="drupal-8"
# Drupal specific variables.
- DRUPAL_TI_DB="drupal_travis_db"
- DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db"
# Note: Do not add a trailing slash here.
- DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1"
- DRUPAL_TI_WEBSERVER_PORT="8080"
# Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end.
- DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT"
# === Behat specific variables.
# This is relative to $TRAVIS_BUILD_DIR
- DRUPAL_TI_BEHAT_DIR="./tests/behat"
# These arguments are passed to the bin/behat command.
- DRUPAL_TI_BEHAT_ARGS=""
# Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables.
- DRUPAL_TI_BEHAT_YML="behat.yml.dist"
# This is used to setup Xvfb.
- DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16"
# The version of seleniumthat should be used.
- DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44"
# Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here.
- DRUPAL_TI_BEHAT_DRIVER="phantomjs"
- DRUPAL_TI_BEHAT_BROWSER="firefox"
# Use Drupal 8.3.x to run tests.
- DRUPAL_TI_CORE_BRANCH="8.3.x"
# PHPUnit specific commandline arguments.
- DRUPAL_TI_PHPUNIT_ARGS="--verbose --debug"
# Specifying the phpunit-core src/ directory is useful when e.g. a vendor/
# directory is present in the module directory, which phpunit would then
# try to find tests in. This option is relative to $TRAVIS_BUILD_DIR.
#- DRUPAL_TI_PHPUNIT_CORE_SRC_DIRECTORY="./tests/src"
# Code coverage via coveralls.io
- DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*"
# This needs to match your .coveralls.yml file.
- DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml"
# Debug options
#- DRUPAL_TI_DEBUG="-x -v"
# Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium",
# etc. to only output those channels.
#- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver"
# [[[ SELECT ANY OR MORE OPTIONS ]]]
#- DRUPAL_TI_RUNNERS="phpunit"
#- DRUPAL_TI_RUNNERS="simpletest"
#- DRUPAL_TI_RUNNERS="behat"
- DRUPAL_TI_RUNNERS="phpunit-core"
matrix:
- REDIS_INTERFACE=PhpRedis
- REDIS_INTERFACE=Predis
# This will create the database
mysql:
database: drupal_travis_db
username: root
encoding: utf8
services:
- redis-server
before_install:
- composer global require "lionsad/drupal_ti:dev-master#396d11d200005eb68491d24170da0a98ae7f51b3"
- drupal-ti before_install
install:
- drupal-ti install
before_script:
- drupal-ti --include .travis-before-script.sh
- drupal-ti before_script
script:
- drupal-ti script
after_script:
- drupal-ti after_script
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,52 @@
PhpRedis cache backend
======================
This client, for now, is only able to use the PhpRedis extension.
Get PhpRedis
------------
You can download this library at:
https://github.com/nicolasff/phpredis
This is PHP extension, too recent for being packaged in most distribution, you
will probably need to compile it yourself.
Default behavior is to connect via tcp://localhost:6379 but you might want to
connect differently.
Use the Sentinel high availability mode
---------------------------------------
Redis can provide a master/slave mode with sentinels server monitoring them.
More information about setting it : https://redis.io/topics/sentinel.
This mode needs the following settings:
Modify the host as follow:
// Sentinels instances list with hostname:port format.
$settings['redis.connection']['host'] = ['1.2.3.4:5000','1.2.3.5:5000','1.2.3.6:5000'];
Add the new instance setting:
// Redis instance name.
$settings['redis.connection']['instance'] = 'instance_name';
Connect via UNIX socket
-----------------------
Just add this line to your settings.php file:
$conf['redis_cache_socket'] = '/tmp/redis.sock';
Don't forget to change the path depending on your operating system and Redis
server configuration.
Connect to a remote host and database
-------------------------------------
See README.md file.
For this particular implementation, host settings are overridden by the
UNIX socket parameter.
@@ -0,0 +1,37 @@
Predis cache backend
====================
Using Predis for the Drupal 8 version of this module is still experimental.
Get Predis
----------
Predis can be installed to the vendor directory using composer like so:
composer require nrk/predis
Configuration of module for use with Predis
----------------------------
There is not much different to configure about Predis.
Adding this to settings.php should suffice for basic usage:
$settings['redis.connection']['interface'] = 'Predis';
$settings['redis.connection']['host'] = '1.2.3.4'; // Your Redis instance hostname.
$settings['cache']['default'] = 'cache.backend.redis';
To add more magic with a primary/replica setup you can use a config like this:
$settings['redis.connection']['interface'] = 'Predis'; // Use predis library.
$settings['redis.connection']['replication'] = TRUE; // Turns on replication.
$settings['redis.connection']['replication.host'][1]['host'] = '1.2.3.4'; // Your Redis instance hostname.
$settings['redis.connection']['replication.host'][1]['port'] = '6379'; // Only required if using non-standard port.
$settings['redis.connection']['replication.host'][1]['role'] = 'primary'; // The redis instance role.
$settings['redis.connection']['replication.host'][2]['host'] = '1.2.3.5';
$settings['redis.connection']['replication.host'][2]['port'] = '6379';
$settings['redis.connection']['replication.host'][2]['role'] = 'replica';
$settings['redis.connection']['replication.host'][3]['host'] = '1.2.3.6';
$settings['redis.connection']['replication.host'][3]['port'] = '6379';
$settings['redis.connection']['replication.host'][3]['role'] = 'replica';
$settings['cache']['default'] = 'cache.backend.redis';
@@ -0,0 +1,338 @@
Redis backends
====================
This package provides two different Redis backends. If you want to use
Redis as cache backend, you have to choose one of the two, but you cannot use
both at the same time.
PhpRedis
--------
This implementation uses the PhpRedis PHP extension. In order to use it, you
will need to compile the extension yourself.
Predis
------
Support for the Predis PHP library is experimental, but feel free to try it out.
You can install the required library using composer. Check out the README.Predis.txt file
for more information.
Important notice
----------------
This module requires at least Redis 2.4, additionally, the lock backend
requires Redis 2.6 to support millisecond timeouts and atomic lock operations.
Getting started
===============
Quick setup
-----------
Here is a simple yet working easy way to setup the module.
This method will allow Drupal to use Redis for all caches.
$settings['redis.connection']['interface'] = 'PhpRedis'; // Can be "Predis".
$settings['redis.connection']['host'] = '1.2.3.4'; // Your Redis instance hostname.
$settings['cache']['default'] = 'cache.backend.redis';
To use some Predis goodness, including a redis primary/replica setup you can use a config like this.
$settings['redis.connection']['interface'] = 'Predis'; // Use predis library.
$settings['redis.connection']['replication'] = TRUE; // Turns on replication.
$settings['redis.connection']['replication.host'][1]['host'] = '1.2.3.4'; // Your Redis instance hostname.
$settings['redis.connection']['replication.host'][1]['port'] = '6379'; // Only required if using non-standard port.
$settings['redis.connection']['replication.host'][1]['role'] = 'primary'; // The redis instance role.
$settings['redis.connection']['replication.host'][2]['host'] = '1.2.3.5';
$settings['redis.connection']['replication.host'][2]['port'] = '6379';
$settings['redis.connection']['replication.host'][2]['role'] = 'replica';
$settings['redis.connection']['replication.host'][3]['host'] = '1.2.3.6';
$settings['redis.connection']['replication.host'][3]['port'] = '6379';
$settings['redis.connection']['replication.host'][3]['role'] = 'replica';
$settings['cache']['default'] = 'cache.backend.redis';
Either include the default example.services.yml from the module, which will
replace all supported backend services (that currently includes the cache tags
checksum service and the lock backends, check the file for the current list)
or copy the service definitions into a site specific services.yml.
$settings['container_yamls'][] = 'modules/redis/example.services.yml';
Note that for any of this, the redis module must be enabled. See next chapters
for more information.
Is there any cache bins that should *never* go into Redis?
----------------------------------------------------------
TL;DR: No.
Redis has been maturing a lot over time, and will apply different sensible
settings for different bins; It's today very stable.
Advanced configuration
======================
Choose the Redis client library to use
--------------------------------------
Note: This is not yet supported, only the PhpRedis interface is available.
Add into your settings.php file:
$settings['redis.connection']['interface'] = 'PhpRedis';
You can replace 'PhpRedis' with 'Predis', depending on the library you chose.
Tell Drupal to use the cache backend
------------------------------------
Usual cache backend configuration, as follows, to add into your settings.php
file like any other backend:
# Use for all bins otherwise specified.
$settings['cache']['default'] = 'cache.backend.redis';
# Use this to only use it for specific cache bins.
$settings['cache']['bins']['render'] = 'cache.backend.redis';
Tell Drupal to use the lock backend
-----------------------------------
See the provided example.services.yml file on how to override the lock services.
Tell Drupal to use the queue backend
------------------------------------
This module provides reliable and non-reliable queue implementations. Depending
on which is to be use you need to choose "queue.redis" or "queue.redis_reliable"
as a service name.
When you have configured basic information (host, library, ... - see Quick setup)
add this to your settings.php file:
# Use for all queues unless otherwise specified for a specific queue.
$settings['queue_default'] = 'queue.redis';
# Or if you want to use reliable queue implementation.
$settings['queue_default'] = 'queue.redis_reliable';
# Use this to only use Redis for a specific queue (aggregator_feeds in this case).
$settings['queue_service_aggregator_feeds'] = 'queue.redis';
# Or if you want to use reliable queue implementation.
$settings['queue_service_aggregator_feeds'] = 'queue.redis_reliable';
Common settings
===============
Connect to a remote host
------------------------
If your Redis instance is remote, you can use this syntax:
$settings['redis.connection']['interface'] = 'PhpRedis'; // Can be "Predis".
$settings['redis.connection']['host'] = '1.2.3.4'; // Your Redis instance hostname.
$settings['redis.connection']['port'] = '6379'; // Redis port
Port is optional, default is 6379 (default Redis port).
Using a specific database
-------------------------
Per default, Redis ships the database "0". All default connections will be use
this one if nothing is specified.
Depending on you OS or OS distribution, you might have numerous database. To
use one in particular, just add to your settings.php file:
$settings['redis.connection']['base'] = 12;
Connection to a password protected instance
-------------------------------------------
If you are using a password protected instance, specify the password this way:
$settings['redis.connection']['password'] = "mypassword";
Depending on the backend, using a wrong auth will behave differently:
- Predis will throw an exception and make Drupal fail during early boostrap.
- PhpRedis will make Redis calls silent and creates some PHP warnings, thus
Drupal will behave as if it was running with a null cache backend (no cache
at all).
Prefixing site cache entries (avoiding sites name collision)
------------------------------------------------------------
If you need to differentiate multiple sites using the same Redis instance and
database, you will need to specify a prefix for your site cache entries.
Cache prefix configuration attempts to use a unified variable across contrib
backends that support this feature. This variable name is 'cache_prefix'.
This variable is polymorphic, the simplest version is to provide a raw string
that will be the default prefix for all cache bins:
$settings['cache_prefix'] = 'mysite_';
Alternatively, to provide the same functionality, you can provide the variable
as an array:
$settings['cache_prefix']['default'] = 'mysite_';
This allows you to provide different prefix depending on the bin name. Common
usage is that each key inside the 'cache_prefix' array is a bin name, the value
the associated prefix. If the value is FALSE, then no prefix is
used for this bin.
The 'default' meta bin name is provided to define the default prefix for non
specified bins. It behaves like the other names, which means that an explicit
FALSE will order the backend not to provide any prefix for any non specified
bin.
Here is a complex sample:
// Default behavior for all bins, prefix is 'mysite_'.
$settings['cache_prefix']['default'] = 'mysite_';
// Set no prefix explicitely for 'cache' and 'cache_bootstrap' bins.
$settings['cache_prefix']['cache'] = FALSE;
$settings['cache_prefix']['cache_bootstrap'] = FALSE;
// Set another prefix for 'cache_menu' bin.
$settings['cache_prefix']['cache_menu'] = 'menumysite_';
Note that if you don't specify the default behavior, the Redis module will
attempt to use the HTTP_HOST variable in order to provide a multisite safe
default behavior. Notice that this is not failsafe, in such environment you
are strongly advised to set at least an explicit default prefix.
Note that this last notice is Redis only specific, because per default Redis
server will not namespace data, thus sharing an instance for multiple sites
will create conflicts. This is not true for every contributed backends.
Flush mode
----------
@todo: Update for Drupal 8
Redis allows to set a time-to-live at the key level, which frees us from
handling the garbage collection at clear() calls; Unfortunately Drupal never
explicitely clears single cached pages or blocks. If you didn't configure the
"cache_lifetime" core variable, its value is "0" which means that temporary
items never expire: in this specific case, we need to adopt a different
behavior than leting Redis handling the TTL by itself; This is why we have
three different implementations of the flush algorithm you can use:
* 0: Never flush temporary: leave Redis handling the TTL; This mode is
not compatible for the "page" and "block" bins but is the default for
all others.
* 1: Keep a copy of temporary items identifiers in a SET and flush them
accordingly to spec (DatabaseCache default backend mimic behavior):
this is the default for "page" and "block" bin if you don't change the
configuration.
* 2: Flush everything including permanent or valid items on clear() calls:
this behavior mimics the pre-1.0 releases of this module. Use it only
if you experience backward compatibility problems on a production
environement - at the cost of potential performance issues; All other
users should ignore this parameter.
You can configure a default flush mode which will override the sensible
provided defaults by setting the 'redis_flush_mode' variable.
// For example this is the safer mode.
$conf['redis_flush_mode'] = 1;
But you may also want to change the behavior for only a few bins.
// This will put mode 0 on "bootstrap" bin.
$conf['redis_flush_mode_cache_bootstrap'] = 0;
// And mode 2 to "page" bin.
$conf['redis_flush_mode_cache_page'] = 2;
Note that you must prefix your bins with "cache" as the Drupal 7 bin naming
convention requires it.
Keep in mind that defaults will provide the best balance between performance
and safety for most sites; Non advanced users should ever change them.
Default lifetime for permanent items
------------------------------------
@todo: Update for Drupal 8
Redis when reaching its maximum memory limit will stop writing data in its
storage engine: this is a feature that avoid the Redis server crashing when
there is no memory left on the machine.
As a workaround, Redis can be configured as a LRU cache for both volatile or
permanent items, which means it can behave like Memcache; Problem is that if
you use Redis as a permanent storage for other business matters than this
module you cannot possibly configure it to drop permanent items or you'll
loose data.
This workaround allows you to explicity set a very long or configured default
lifetime for CACHE_PERMANENT items (that would normally be permanent) which
will mark them as being volatile in Redis storage engine: this then allows you
to configure a LRU behavior for volatile keys without engaging the permenent
business stuff in a dangerous LRU mechanism; Cache items even if permament will
be dropped when unused using this.
Per default the TTL for permanent items will set to safe-enough value which is
one year; No matter how Redis will be configured default configuration or lazy
admin will inherit from a safe module behavior with zero-conf.
For advanturous people, you can manage the TTL on a per bin basis and change
the default one:
// Make CACHE_PERMANENT items being permanent once again
// 0 is a special value usable for all bins to explicitely tell the
// cache items will not be volatile in Redis.
$conf['redis_perm_ttl'] = 0;
// Make them being volatile with a default lifetime of 1 year.
$conf['redis_perm_ttl'] = "1 year";
// You can override on a per-bin basis;
// For example make cached field values live only 3 monthes:
$conf['redis_perm_ttl_cache_field'] = "3 months";
// But you can also put a timestamp in there; In this case the
// value must be a STRICTLY TYPED integer:
$conf['redis_perm_ttl_cache_field'] = 2592000; // 30 days.
Time interval string will be parsed using DateInterval::createFromDateString
please refer to its documentation:
http://www.php.net/manual/en/dateinterval.createfromdatestring.php
Last but not least please be aware that this setting affects the
CACHE_PERMANENT ONLY; All other use cases (CACHE_TEMPORARY or user set TTL
on single cache entries) will continue to behave as documented in Drupal core
cache backend documentation.
Lock backends
-------------
@todo: Update for Drupal 8
Both implementations provides a Redis lock backend. Redis lock backend proved to
be faster than the default SQL based one when using both servers on the same box.
Both backends, thanks to the Redis WATCH, MULTI and EXEC commands provides a
real race condition free mutexes if you use Redis >= 2.1.0.
Testing
=======
I did not find any hint about making tests being configurable, so per default
the tested Redis server must always be on localhost with default configuration.
@@ -0,0 +1,13 @@
{
"name": "drupal/redis",
"type": "drupal-module",
"suggest": {
"predis/predis": "^1.1.1"
},
"license": "GPL-2.0",
"autoload": {
"psr-4": {
"Drupal\\redis\\": "src"
}
}
}
@@ -0,0 +1,33 @@
# This file contains example services overrides.
#
# Enable with this line in settings.php
# $settings['container_yamls'][] = 'modules/redis/example.services.yml';
#
# Or copy & paste the desired services into sites/default/services.yml.
#
# Note that the redis module must be enabled for this to work.
services:
# Cache tag checksum backend. Used by redis and most other cache backend
# to deal with cache tag invalidations.
cache_tags.invalidator.checksum:
class: Drupal\redis\Cache\RedisCacheTagsChecksum
arguments: ['@redis.factory']
tags:
- { name: cache_tags_invalidator }
# Replaces the default lock backend with a redis implementation.
lock:
class: Drupal\Core\Lock\LockBackendInterface
factory: ['@redis.lock.factory', get]
# Replaces the default persistent lock backend with a redis implementation.
lock.persistent:
class: Drupal\Core\Lock\LockBackendInterface
factory: ['@redis.lock.factory', get]
arguments: [true]
# Replaces the default flood backend with a redis implementation.
flood:
class: Drupal\Core\Flood\FloodInterface
factory: ['@redis.flood.factory', get]
@@ -0,0 +1,12 @@
name: Redis
description: Provide a module placeholder, for using as dependency for module that needs Redis.
package: Performance
type: module
# core: 8.x
configure: redis.admin_display
# Information added by Drupal.org packaging script on 2017-09-14
version: '8.x-1.0-rc2'
core: '8.x'
project: 'redis'
datestamp: 1505390051
@@ -0,0 +1,40 @@
<?php
/**
* @file
* Redis install related functions.
*/
use \Drupal\redis\ClientFactory;
/**
* Implements hook_requirements().
*/
function redis_requirements($phase) {
// This module is configured via settings.php file. Using any other phase
// than runtime to proceed to some consistency checks is useless.
if ('runtime' !== $phase) {
return [];
}
$requirements = [];
if (ClientFactory::hasClient()) {
$requirements['redis'] = [
'title' => "Redis",
'value' => t("Connected, using the <em>@name</em> client.", ['@name' => ClientFactory::getClientName()]),
'severity' => REQUIREMENT_OK,
];
}
else {
$requirements['redis'] = [
'title' => "Redis",
'value' => t("Not connected."),
'severity' => REQUIREMENT_WARNING,
'description' => t("No Redis client connected, this module is useless thereof. Ensure that you enabled module using it or disable it."),
];
}
return $requirements;
}
@@ -0,0 +1,28 @@
<?php
/**
* @file
* Redis module.
*
* This file is a placeholder for other modules that need the Redis client for
* something else than lock and cache.
*/
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\redis\ClientFactory;
/**
* Implements hook_help().
*/
function redis_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.redis':
if (ClientFactory::hasClient()) {
$messages = '<p><strong>' . t("Current connected client uses the <em>@name</em> library.", ['@name' => ClientFactory::getClientName()]) . '</strong></p>';
}
else {
$messages = '<p><strong>' . t('No redis connection configured.') . '</strong></p>';
}
return $messages;
}
}
@@ -0,0 +1,18 @@
services:
cache.backend.redis:
class: Drupal\redis\Cache\CacheBackendFactory
arguments: ['@redis.factory', '@cache_tags.invalidator.checksum', '@serialization.phpserialize']
redis.factory:
class: Drupal\redis\ClientFactory
redis.lock.factory:
class: Drupal\redis\Lock\LockFactory
arguments: ['@redis.factory']
redis.flood.factory:
class: Drupal\redis\Flood\FloodFactory
arguments: ['@redis.factory', '@request_stack']
queue.redis_reliable:
class: Drupal\redis\Queue\ReliableQueueRedisFactory
arguments: ['@redis.factory', '@settings']
queue.redis:
class: Drupal\redis\Queue\QueueRedisFactory
arguments: ['@redis.factory', '@settings']
@@ -0,0 +1,70 @@
<?php
namespace Drupal\redis\Cache;
use Drupal\Component\Serialization\SerializationInterface;
use Drupal\Core\Cache\CacheFactoryInterface;
use Drupal\Core\Cache\CacheTagsChecksumInterface;
use Drupal\redis\ClientFactory;
/**
* A cache backend factory responsible for the construction of redis cache bins.
*/
class CacheBackendFactory implements CacheFactoryInterface {
/**
* @var \Drupal\redis\ClientInterface
*/
protected $clientFactory;
/**
* The cache tags checksum provider.
*
* @var \Drupal\Core\Cache\CacheTagsChecksumInterface
*/
protected $checksumProvider;
/**
* The serialization class to use.
*
* @var \Drupal\Component\Serialization\SerializationInterface
*/
protected $serializer;
/**
* List of cache bins.
*
* Renderer and possibly other places fetch backends directly from the
* factory. Avoid that the backend objects have to fetch meta information like
* the last delete all timestamp multiple times.
*
* @var array
*/
protected $bins = [];
/**
* Creates a redis CacheBackendFactory.
*
* @param \Drupal\redis\ClientFactory $client_factory
* @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
* @param \Drupal\redis\Cache\SerializationInterface $serializer
* The serialization class to use.
*/
public function __construct(ClientFactory $client_factory, CacheTagsChecksumInterface $checksum_provider, SerializationInterface $serializer) {
$this->clientFactory = $client_factory;
$this->checksumProvider = $checksum_provider;
$this->serializer = $serializer;
}
/**
* {@inheritdoc}
*/
public function get($bin) {
if (!isset($this->bins[$bin])) {
$class_name = $this->clientFactory->getClass(ClientFactory::REDIS_IMPL_CACHE);
$this->bins[$bin] = new $class_name($bin, $this->clientFactory->getClient(), $this->checksumProvider, $this->serializer);
}
return $this->bins[$bin];
}
}
@@ -0,0 +1,378 @@
<?php
namespace Drupal\redis\Cache;
use \DateInterval;
use Drupal\Component\Serialization\SerializationInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Site\Settings;
use Drupal\redis\RedisPrefixTrait;
/**
* Base class for redis cache backends.
*
* *
*
*/
abstract class CacheBase implements CacheBackendInterface {
use RedisPrefixTrait;
/**
* Temporary cache items lifetime is infinite.
*/
const LIFETIME_INFINITE = 0;
/**
* Default lifetime for permanent items.
* Approximatively 1 year.
*/
const LIFETIME_PERM_DEFAULT = 31536000;
/**
* Computed keys are let's say arround 60 characters length due to
* key prefixing, which makes 1,000 keys DEL command to be something
* arround 50,000 bytes length: this is huge and may not pass into
* Redis, let's split this off.
* Some recommend to never get higher than 1,500 bytes within the same
* command which makes us forced to split this at a very low threshold:
* 20 seems a safe value here (1,280 average length).
*/
const KEY_THRESHOLD = 20;
/**
* Latest delete all flush KEY name.
*/
const LAST_DELETE_ALL_KEY = '_redis_last_delete_all';
/**
* @var string
*/
protected $bin;
/**
* The serialization class to use.
*
* @var \Drupal\Component\Serialization\SerializationInterface
*/
protected $serializer;
/**
* Default TTL for CACHE_PERMANENT items.
*
* See "Default lifetime for permanent items" section of README.md
* file for a comprehensive explaination of why this exists.
*
* @var int
*/
protected $permTtl = self::LIFETIME_PERM_DEFAULT;
/**
* Minimal TTL to use.
*
* Note that this is for testing purposes. Do not specify the minimal TTL
* outside of unit-tests.
*/
protected $minTtl = 0;
/**
* @var \Drupal\redis\ClientInterface
*/
protected $client;
/**
* The cache tags checksum provider.
*
* @var \Drupal\Core\Cache\CacheTagsChecksumInterface|\Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $checksumProvider;
/**
* The last delete timestamp.
*
* @var float
*/
protected $lastDeleteAll = NULL;
/**
* Get TTL for CACHE_PERMANENT items.
*
* @return int
* Lifetime in seconds.
*/
public function getPermTtl() {
return $this->permTtl;
}
/**
* CacheBase constructor.
* @param $bin
* The cache bin for which the object is created.
* @param \Drupal\Component\Serialization\SerializationInterface $serializer
* The serialization class to use.
*/
public function __construct($bin, SerializationInterface $serializer) {
$this->bin = $bin;
$this->serializer = $serializer;
$this->setPermTtl();
}
/**
* {@inheritdoc}
*/
public function get($cid, $allow_invalid = FALSE) {
$cids = [$cid];
$cache = $this->getMultiple($cids, $allow_invalid);
return reset($cache);
}
/**
* {@inheritdoc}
*/
public function setMultiple(array $items) {
foreach ($items as $cid => $item) {
$this->set($cid, $item['data'], isset($item['expire']) ? $item['expire'] : CacheBackendInterface::CACHE_PERMANENT, isset($item['tags']) ? $item['tags'] : []);
}
}
/**
* {@inheritdoc}
*/
public function delete($cid) {
$this->deleteMultiple([$cid]);
}
/**
* {@inheritdoc}
*/
public function removeBin() {
$this->deleteAll();
}
/**
* {@inheritdoc}
*/
public function invalidate($cid) {
$this->invalidateMultiple([$cid]);
}
/**
* Return the key for the given cache key.
*/
public function getKey($cid = NULL) {
if (NULL === $cid) {
return $this->getPrefix() . ':' . $this->bin;
}
else {
return $this->getPrefix() . ':' . $this->bin . ':' . $cid;
}
}
/**
* Calculate the correct expiration time.
*
* @param int $expire
* The expiration time provided for the cache set.
*
* @return int
* The default expiration if expire is PERMANENT or higher than the default.
* May return negative values if the item is already expired.
*/
protected function getExpiration($expire) {
if ($expire == Cache::PERMANENT || $expire > $this->permTtl) {
return $this->permTtl;
}
return $expire - REQUEST_TIME;
}
/**
* Return the key for the tag used to specify the bin of cache-entries.
*/
protected function getTagForBin() {
return 'x-redis-bin:' . $this->bin;
}
/**
* Set the minimum TTL (unit testing only).
*/
public function setMinTtl($ttl) {
$this->minTtl = $ttl;
}
/**
* Set the permanent TTL.
*/
public function setPermTtl($ttl = NULL) {
if (isset($ttl)) {
$this->permTtl = $ttl;
}
else {
// Attempt to set from settings.
if (($settings = Settings::get('redis.settings', [])) && isset($settings['perm_ttl_' . $this->bin])) {
$ttl = $settings['perm_ttl_' . $this->bin];
if ($ttl === (int) $ttl) {
$this->permTtl = $ttl;
}
else {
if ($iv = DateInterval::createFromDateString($ttl)) {
// http://stackoverflow.com/questions/14277611/convert-dateinterval-object-to-seconds-in-php
$this->permTtl = ($iv->y * 31536000 + $iv->m * 2592000 + $iv->days * 86400 + $iv->h * 3600 + $iv->i * 60 + $iv->s);
}
else {
// Log error about invalid ttl.
trigger_error(sprintf("Parsed TTL '%s' has an invalid value: switching to default", $ttl));
$this->permTtl = self::LIFETIME_PERM_DEFAULT;
}
}
}
}
}
/**
* Prepares a cached item.
*
* Checks that items are either permanent or did not expire, and unserializes
* data as appropriate.
*
* @param array $values
* The hash returned from redis or false.
* @param bool $allow_invalid
* If FALSE, the method returns FALSE if the cache item is not valid.
*
* @return mixed|false
* The item with data unserialized as appropriate and a property indicating
* whether the item is valid, or FALSE if there is no valid item to load.
*/
protected function expandEntry(array $values, $allow_invalid) {
// Check for entry being valid.
if (empty($values['cid'])) {
return FALSE;
}
$cache = (object) $values;
$cache->tags = explode(' ', $cache->tags);
// Check expire time, allow to have a cache invalidated explicitly, don't
// check if already invalid.
if ($cache->valid) {
$cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME;
// Check if invalidateTags() has been called with any of the items's tags.
if ($cache->valid && !$this->checksumProvider->isValid($cache->checksum, $cache->tags)) {
$cache->valid = FALSE;
}
}
// Ensure the entry does not predate the last delete all time.
$last_delete_timestamp = $this->getLastDeleteAll();
if ($last_delete_timestamp && ((float)$values['created']) < $last_delete_timestamp) {
return FALSE;
}
if (!$allow_invalid && !$cache->valid) {
return FALSE;
}
if ($cache->serialized) {
$cache->data = $this->serializer->decode($cache->data);
}
return $cache;
}
/**
* Create cache entry.
*
* @param string $cid
* @param mixed $data
* @param int $expire
* @param string[] $tags
*
* @return array
*/
protected function createEntryHash($cid, $data, $expire = Cache::PERMANENT, array $tags) {
// Always add a cache tag for the current bin, so that we can use that for
// invalidateAll().
$tags[] = $this->getTagForBin();
assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache Tags must be strings.');
$hash = [
'cid' => $cid,
'created' => round(microtime(TRUE), 3),
'expire' => $expire,
'tags' => implode(' ', $tags),
'valid' => 1,
'checksum' => $this->checksumProvider->getCurrentChecksum($tags),
];
// Let Redis handle the data types itself.
if (!is_string($data)) {
$hash['data'] = $this->serializer->encode($data);
$hash['serialized'] = 1;
}
else {
$hash['data'] = $data;
$hash['serialized'] = 0;
}
return $hash;
}
/**
* {@inheritdoc}
*/
public function invalidateMultiple(array $cids) {
// Loop over all cache items, they are stored as a hash, so we can access
// the valid flag directly, only write if it exists and is not 0.
foreach ($cids as $cid) {
$key = $this->getKey($cid);
if ($this->client->hGet($key, 'valid')) {
$this->client->hSet($key, 'valid', 0);
}
}
}
/**
* {@inheritdoc}
*/
public function invalidateAll() {
// To invalidate the whole bin, we invalidate a special tag for this bin.
$this->checksumProvider->invalidateTags([$this->getTagForBin()]);
}
/**
* {@inheritdoc}
*/
public function garbageCollection() {
// @todo Do we need to do anything here?
}
/**
* Returns the last delete all timestamp.
*
* @return float
* The last delete timestamp as a timestamp with a millisecond precision.
*/
protected function getLastDeleteAll() {
// Cache the last delete all timestamp.
if ($this->lastDeleteAll === NULL) {
$this->lastDeleteAll = (float) $this->client->get($this->getKey(static::LAST_DELETE_ALL_KEY));
}
return $this->lastDeleteAll;
}
/**
* {@inheritdoc}
*/
public function deleteAll() {
// The last delete timestamp is in milliseconds, ensure that no cache
// was written in the same millisecond.
// @todo This is needed to make the tests pass, is this safe enough for real
// usage?
usleep(1000);
$this->lastDeleteAll = round(microtime(TRUE), 3);
$this->client->set($this->getKey(static::LAST_DELETE_ALL_KEY), $this->lastDeleteAll);
}
}
@@ -0,0 +1,111 @@
<?php
namespace Drupal\redis\Cache;
use Drupal\Component\Serialization\SerializationInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheTagsChecksumInterface;
/**
* PhpRedis cache backend.
*/
class PhpRedis extends CacheBase {
/**
* @var \Redis
*/
protected $client;
/**
* Creates a PHpRedis cache backend.
*
* @param $bin
* The cache bin for which the object is created.
* @param \Redis $client
* @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
* @param \Drupal\redis\Cache\SerializationInterface $serializer
* The serialization class to use.
*/
public function __construct($bin, \Redis $client, CacheTagsChecksumInterface $checksum_provider, SerializationInterface $serializer) {
parent::__construct($bin, $serializer);
$this->client = $client;
$this->checksumProvider = $checksum_provider;
}
/**
* {@inheritdoc}
*/
public function getMultiple(&$cids, $allow_invalid = FALSE) {
// Avoid an error when there are no cache ids.
if (empty($cids)) {
return [];
}
$return = [];
// Build the list of keys to fetch.
$keys = array_map([$this, 'getKey'], $cids);
// Optimize for the common case when only a single cache entry needs to
// be fetched, no pipeline is needed then.
if (count($keys) > 1) {
$pipe = $this->client->multi(\Redis::PIPELINE);
foreach ($keys as $key) {
$pipe->hgetall($key);
}
$result = $pipe->exec();
}
else {
$result = [$this->client->hGetAll(reset($keys))];
}
// Loop over the cid values to ensure numeric indexes.
foreach (array_values($cids) as $index => $key) {
// Check if a valid result was returned from Redis.
if (isset($result[$index]) && is_array($result[$index])) {
// Check expiration and invalidation and convert into an object.
$item = $this->expandEntry($result[$index], $allow_invalid);
if ($item) {
$return[$item->cid] = $item;
}
}
}
// Remove fetched cids from the list.
$cids = array_diff($cids, array_keys($return));
return $return;
}
/**
* {@inheritdoc}
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
$ttl = $this->getExpiration($expire);
$key = $this->getKey($cid);
// If the item is already expired, delete it.
if ($ttl <= 0) {
$this->delete($key);
}
// Build the cache item and save it as a hash array.
$entry = $this->createEntryHash($cid, $data, $expire, $tags);
$pipe = $this->client->multi(\REdis::PIPELINE);
$pipe->hMset($key, $entry);
$pipe->expire($key, $ttl);
$pipe->exec();
}
/**
* {@inheritdoc}
*/
public function deleteMultiple(array $cids) {
$keys = array_map([$this, 'getKey'], $cids);
$this->client->del($keys);
}
}
@@ -0,0 +1,113 @@
<?php
namespace Drupal\redis\Cache;
use Drupal\Component\Serialization\SerializationInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheTagsChecksumInterface;
/**
* Predis cache backend.
*/
class Predis extends CacheBase {
/**
* @var \Predis\Client
*/
protected $client;
/**
* Creates a Predis cache backend.
*
* @param $bin
* The cache bin for which the object is created.
* @param \Redis $client
* @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider
* @param \Drupal\redis\Cache\SerializationInterface $serializer
* The serialization class to use.
*/
public function __construct($bin, \Predis\Client $client, CacheTagsChecksumInterface $checksum_provider, SerializationInterface $serializer) {
parent::__construct($bin, $serializer);
$this->client = $client;
$this->checksumProvider = $checksum_provider;
}
/**
* {@inheritdoc}
*/
public function getMultiple(&$cids, $allow_invalid = FALSE) {
// Avoid an error when there are no cache ids.
if (empty($cids)) {
return [];
}
$return = [];
// Build the list of keys to fetch.
$keys = array_map([$this, 'getKey'], $cids);
// Optimize for the common case when only a single cache entry needs to
// be fetched, no pipeline is needed then.
if (count($keys) > 1) {
$pipe = $this->client->pipeline();
foreach ($keys as $key) {
$pipe->hgetall($key);
}
$result = $pipe->execute();
}
else {
$result = [$this->client->hGetAll(reset($keys))];
}
// Loop over the cid values to ensure numeric indexes.
foreach (array_values($cids) as $index => $key) {
// Check if a valid result was returned from Redis.
if (isset($result[$index]) && is_array($result[$index])) {
// Check expiration and invalidation and convert into an object.
$item = $this->expandEntry($result[$index], $allow_invalid);
if ($item) {
$return[$item->cid] = $item;
}
}
}
// Remove fetched cids from the list.
$cids = array_diff($cids, array_keys($return));
return $return;
}
/**
* {@inheritdoc}
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
$ttl = $this->getExpiration($expire);
$key = $this->getKey($cid);
// If the item is already expired, delete it.
if ($ttl <= 0) {
$this->delete($key);
}
// Build the cache item and save it as a hash array.
$entry = $this->createEntryHash($cid, $data, $expire, $tags);
$pipe = $this->client->pipeline();
$pipe->hmset($key, $entry);
$pipe->expire($key, $ttl);
$pipe->execute();
}
/**
* {@inheritdoc}
*/
public function deleteMultiple(array $cids) {
if (!empty($cids)) {
$keys = array_map([$this, 'getKey'], $cids);
$this->client->del($keys);
}
}
}
@@ -0,0 +1,150 @@
<?php
namespace Drupal\redis\Cache;
use Drupal\Core\Cache\CacheTagsChecksumInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\redis\ClientFactory;
use Drupal\redis\RedisPrefixTrait;
/**
* Cache tags invalidations checksum implementation that uses redis.
*/
class RedisCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface {
use RedisPrefixTrait;
/**
* Contains already loaded cache invalidations from the database.
*
* @var array
*/
protected $tagCache = [];
/**
* A list of tags that have already been invalidated in this request.
*
* Used to prevent the invalidation of the same cache tag multiple times.
*
* @var array
*/
protected $invalidatedTags = [];
/**
* {@inheritdoc}
*/
protected $client;
/**
* @var string
*/
protected $clientType;
/**
* Creates a PHpRedis cache backend.
*/
public function __construct(ClientFactory $factory) {
$this->client = $factory->getClient();
$this->clientType = $factory->getClientName();
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags) {
$keys_to_increment = [];
foreach ($tags as $tag) {
// Only invalidate tags once per request unless they are written again.
if (isset($this->invalidatedTags[$tag])) {
continue;
}
$this->invalidatedTags[$tag] = TRUE;
unset($this->tagCache[$tag]);
$keys_to_increment[] = $this->getTagKey($tag);
}
if ($keys_to_increment) {
// We want to differentiate between PhpRedis and Redis clients.
if ($this->clientType === 'PhpRedis') {
$multi = $this->client->multi(\Redis::PIPELINE);
foreach ($keys_to_increment as $key) {
$multi->incr($key);
}
$multi->exec();
}
elseif ($this->clientType === 'Predis') {
$pipe = $this->client->pipeline();
foreach ($keys_to_increment as $key) {
$pipe->incr($key);
}
$pipe->execute();
}
}
}
/**
* {@inheritdoc}
*/
public function getCurrentChecksum(array $tags) {
// Remove tags that were already invalidated during this request from the
// static caches so that another invalidation can occur later in the same
// request. Without that, written cache items would not be invalidated
// correctly.
foreach ($tags as $tag) {
unset($this->invalidatedTags[$tag]);
}
return $this->calculateChecksum($tags);
}
/**
* {@inheritdoc}
*/
public function isValid($checksum, array $tags) {
return $checksum == $this->calculateChecksum($tags);
}
/**
* {@inheritdoc}
*/
public function calculateChecksum(array $tags) {
$checksum = 0;
$fetch = array_values(array_diff($tags, array_keys($this->tagCache)));
if ($fetch) {
$keys = array_map([$this, 'getTagKey'], $fetch);
foreach ($this->client->mget($keys) as $index => $invalidations) {
$this->tagCache[$fetch[$index]] = $invalidations ?: 0;
}
}
foreach ($tags as $tag) {
$checksum += $this->tagCache[$tag];
}
return $checksum;
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->tagCache = [];
$this->invalidatedTags = [];
}
/**
* Return the key for the given cache tag.
*
* @param string $tag
* The cache tag.
*
* @return string
* The prefixed cache tag.
*/
protected function getTagKey($tag) {
return $this->getPrefix() . ':cachetags:' . $tag;
}
}
@@ -0,0 +1,97 @@
<?php
namespace Drupal\redis\Client;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Site\Settings;
use Drupal\redis\ClientInterface;
/**
* PhpRedis client specific implementation.
*/
class PhpRedis implements ClientInterface {
/**
* {@inheritdoc}
*/
public function getClient($host = NULL, $port = NULL, $base = NULL, $password = NULL) {
$client = new \Redis();
// Sentinel mode, get the real master.
if (is_array($host)) {
$ip_host = $this->askForMaster($client, $host, $password);
if (is_array($ip_host)) {
list($host, $port) = $ip_host;
}
}
$client->connect($host, $port);
if (isset($password)) {
$client->auth($password);
}
if (isset($base)) {
$client->select($base);
}
// Do not allow PhpRedis serialize itself data, we are going to do it
// ourself. This will ensure less memory footprint on Redis size when
// we will attempt to store small values.
$client->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_NONE);
return $client;
}
/**
* {@inheritdoc}
*/
public function getName() {
return 'PhpRedis';
}
/**
* Connect to sentinels to get Redis master instance.
*
* Just asking one sentinels after another until given the master location.
* More info about this mode at https://redis.io/topics/sentinel.
*
* @param \Redis $client
* The PhpRedis client.
* @param array $sentinels
* An array of the sentinels' ip:port.
* @param string $password
* An optional Sentinels' password.
*
* @return mixed
* An array with ip & port of the Master instance or NULL.
*/
protected function askForMaster(\Redis $client, array $sentinels = [], $password = NULL) {
$ip_port = NULL;
$settings = Settings::get('redis.connection', []);
$settings += ['instance' => NULL];
if ($settings['instance']) {
foreach ($sentinels as $sentinel) {
list($host, $port) = explode(':', $sentinel);
// 0.5s timeout.
$client->connect($host, $port, 0.5);
if (isset($password)) {
$client->auth($password);
}
if ($client->isConnected()) {
$ip_port = $client->rawcommand('SENTINEL', 'get-master-addr-by-name', $settings['instance']);
if ($ip_port) {
break;
}
}
$client->close();
}
}
return $ip_port;
}
}
@@ -0,0 +1,61 @@
<?php
namespace Drupal\redis\Client;
use Drupal\redis\ClientInterface;
use Predis\Client;
/**
* Predis client specific implementation.
*/
class Predis implements ClientInterface {
public function getClient($host = NULL, $port = NULL, $base = NULL, $password = NULL, $replicationHosts = NULL) {
$connectionInfo = [
'password' => $password,
'host' => $host,
'port' => $port,
'database' => $base
];
foreach ($connectionInfo as $key => $value) {
if (!isset($value)) {
unset($connectionInfo[$key]);
}
}
// I'm not sure why but the error handler is driven crazy if timezone
// is not set at this point.
// Hopefully Drupal will restore the right one this once the current
// account has logged in.
date_default_timezone_set(@date_default_timezone_get());
// If we are passed in an array of $replicationHosts, we should attempt a clustered client connection.
if ($replicationHosts !== NULL) {
$parameters = [];
foreach ($replicationHosts as $replicationHost) {
// Configure master.
if ($replicationHost['role'] === 'primary') {
$parameters[] = 'tcp://' . $replicationHost['host'] . ':' . $replicationHost['port'] . '?alias=master';
}
else {
$parameters[] = 'tcp://' . $replicationHost['host'] . ':' . $replicationHost['port'];
}
}
$options = ['replication' => true];
$client = new Client($parameters, $options);
}
else {
$client = new Client($connectionInfo);
}
return $client;
}
public function getName() {
return 'Predis';
}
}
@@ -0,0 +1,215 @@
<?php
namespace Drupal\redis;
use Drupal\Core\Site\Settings;
/**
* Common code and client singleton, for all Redis clients.
*/
class ClientFactory {
/**
* Redis default host.
*/
const REDIS_DEFAULT_HOST = "127.0.0.1";
/**
* Redis default port.
*/
const REDIS_DEFAULT_PORT = 6379;
/**
* Redis default database: will select none (Database 0).
*/
const REDIS_DEFAULT_BASE = NULL;
/**
* Redis default password: will not authenticate.
*/
const REDIS_DEFAULT_PASSWORD = NULL;
/**
* Cache implementation namespace.
*/
const REDIS_IMPL_CACHE = '\\Drupal\\redis\\Cache\\';
/**
* Lock implementation namespace.
*/
const REDIS_IMPL_LOCK = '\\Drupal\\redis\\Lock\\';
/**
* Lock implementation namespace.
*/
const REDIS_IMPL_FLOOD = '\\Drupal\\redis\\Flood\\';
/**
* Persistent Lock implementation namespace.
*/
const REDIS_IMPL_PERSISTENT_LOCK = '\\Drupal\\redis\\PersistentLock\\';
/**
* Client implementation namespace.
*/
const REDIS_IMPL_CLIENT = '\\Drupal\\redis\\Client\\';
/**
* Queue implementation namespace.
*/
const REDIS_IMPL_QUEUE = '\\Drupal\\redis\\Queue\\';
/**
* Reliable queue implementation namespace.
*/
const REDIS_IMPL_RELIABLE_QUEUE = '\\Drupal\\redis\\Queue\\Reliable';
/**
* @var \Drupal\redis\ClientInterface
*/
protected static $_clientInterface;
/**
* @var mixed
*/
protected static $_client;
public static function hasClient() {
return isset(self::$_client);
}
/**
* Set client proxy.
*/
public static function setClient(ClientInterface $interface) {
if (isset(self::$_client)) {
throw new \Exception("Once Redis client is connected, you cannot change client proxy instance.");
}
self::$_clientInterface = $interface;
}
/**
* Lazy instanciate client proxy depending on the actual configuration.
*
* If you are using a lock or cache backend using one of the Redis client
* implementations, this will be overridden at early bootstrap phase and
* configuration will be ignored.
*
* @return ClientInterface
*/
public static function getClientInterface()
{
if (!isset(self::$_clientInterface))
{
$settings = Settings::get('redis.connection', []);
if (!empty($settings['interface']))
{
$className = self::getClass(self::REDIS_IMPL_CLIENT, $settings['interface']);
self::$_clientInterface = new $className();
}
elseif (class_exists('Predis\Client'))
{
// Transparent and abitrary preference for Predis library.
$className = self::getClass(self::REDIS_IMPL_CLIENT, 'Predis');
self::$_clientInterface = new $className();
}
elseif (class_exists('Redis'))
{
// Fallback on PhpRedis if available.
$className = self::getClass(self::REDIS_IMPL_CLIENT, 'PhpRedis');
self::$_clientInterface = new $className();
}
else
{
if (!isset(self::$_clientInterface))
{
throw new \Exception("No client interface set.");
}
}
}
return self::$_clientInterface;
}
/**
* Get underlaying library name.
*
* @return string
*/
public static function getClientName() {
return self::getClientInterface()->getName();
}
/**
* Get client singleton.
*/
public static function getClient() {
if (!isset(self::$_client)) {
$settings = Settings::get('redis.connection', []);
$settings += [
'host' => self::REDIS_DEFAULT_HOST,
'port' => self::REDIS_DEFAULT_PORT,
'base' => self::REDIS_DEFAULT_BASE,
'password' => self::REDIS_DEFAULT_PASSWORD,
];
// If using replication, lets create the client appropriately.
if (isset($settings['replication']) && $settings['replication'] === TRUE) {
foreach ($settings['replication.host'] as $key => $replicationHost) {
if (!isset($replicationHost['port'])) {
$settings['replication.host'][$key]['port'] = self::REDIS_DEFAULT_PORT;
}
}
self::$_client = self::getClientInterface()->getClient(
$settings['host'],
$settings['port'],
$settings['base'],
$settings['password'],
$settings['replication.host']);
}
else {
self::$_client = self::getClientInterface()->getClient(
$settings['host'],
$settings['port'],
$settings['base'],
$settings['password']);
}
}
return self::$_client;
}
/**
* Get specific class implementing the current client usage for the specific
* asked core subsystem.
*
* @param string $system
* One of the ClientFactory::IMPL_* constant.
* @param string $clientName
* Client name, if fixed.
*
* @return string
* Class name, if found.
*
* @throws \Exception
* If not found.
*/
public static function getClass($system, $clientName = NULL) {
$className = $system . ($clientName ?: self::getClientName());
if (!class_exists($className)) {
throw new \Exception($className . " does not exists");
}
return $className;
}
/**
* For unit testing only reset internals.
*/
static public function reset() {
self::$_clientInterface = null;
self::$_client = null;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\redis;
/**
* Client proxy, client handling class tied to the bare mininum.
*/
interface ClientInterface {
/**
* Get the connected client instance.
*
* @return mixed
* Real client depends from the library behind.
*/
public function getClient($host = NULL, $port = NULL, $base = NULL);
/**
* Get underlaying library name used.
*
* This can be useful for contribution code that may work with only some of
* the provided clients.
*
* @return string
*/
public function getName();
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\redis\Flood;
use Drupal\redis\ClientFactory;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Flood backend singleton handling.
*/
class FloodFactory {
/**
* @var \Drupal\redis\ClientInterface
*/
protected $clientFactory;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Construct the PhpRedis flood backend factory.
*
* @param \Drupal\redis\ClientFactory $client_factory
* The database connection which will be used to store the flood event
* information.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack used to retrieve the current request.
*/
public function __construct(ClientFactory $client_factory, RequestStack $request_stack) {
$this->clientFactory = $client_factory;
$this->requestStack = $request_stack;
}
/**
* Get actual flood backend.
*
* @return \Drupal\Core\Flood\FloodInterface
* Return flood instance.
*/
public function get() {
$class_name = $this->clientFactory->getClass(ClientFactory::REDIS_IMPL_FLOOD);
return new $class_name($this->clientFactory, $this->requestStack);
}
}
@@ -0,0 +1,95 @@
<?php
namespace Drupal\redis\Flood;
use Drupal\Core\Flood\FloodInterface;
use Drupal\redis\ClientFactory;
use Drupal\redis\RedisPrefixTrait;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Defines the database flood backend. This is the default Drupal backend.
*/
class PhpRedis implements FloodInterface {
use RedisPrefixTrait;
/**
* @var \Redis
*/
protected $client;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Construct the PhpRedis flood backend.
*
* @param \Drupal\redis\ClientFactory $client_factory
* The database connection which will be used to store the flood event
* information.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack used to retrieve the current request.
*/
public function __construct(ClientFactory $client_factory, RequestStack $request_stack) {
$this->client = $client_factory->getClient();
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public function register($name, $window = 3600, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = $this->requestStack->getCurrentRequest()->getClientIp();
}
$key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier;
// Add a key for the event to the sorted set, the score is timestamp, so we
// can count them easily.
$this->client->zAdd($key, $_SERVER['REQUEST_TIME'] + $window, microtime(TRUE));
// Set or update the expiration for the sorted set, it will be removed if
// the newest entry expired.
$this->client->expire($key, $_SERVER['REQUEST_TIME'] + $window);
}
/**
* {@inheritdoc}
*/
public function clear($name, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = $this->requestStack->getCurrentRequest()->getClientIp();
}
$key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier;
$this->client->del($key);
}
/**
* {@inheritdoc}
*/
public function isAllowed($name, $threshold, $window = 3600, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = $this->requestStack->getCurrentRequest()->getClientIp();
}
$key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier;
// Count the in the last $window seconds.
$number = $this->client->zCount($key, $_SERVER['REQUEST_TIME'], 'inf');
return ($number < $threshold);
}
/**
* {@inheritdoc}
*/
public function garbageCollection() {
// No garbage collection necessary.
}
}
@@ -0,0 +1,95 @@
<?php
namespace Drupal\redis\Flood;
use Drupal\Core\Flood\FloodInterface;
use Drupal\redis\ClientFactory;
use Drupal\redis\RedisPrefixTrait;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Defines the database flood backend. This is the default Drupal backend.
*/
class Predis implements FloodInterface {
use RedisPrefixTrait;
/**
* @var \Predis\Client
*/
protected $client;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Construct the PhpRedis flood backend.
*
* @param \Drupal\redis\ClientFactory $client_factory
* The database connection which will be used to store the flood event
* information.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack used to retrieve the current request.
*/
public function __construct(ClientFactory $client_factory, RequestStack $request_stack) {
$this->client = $client_factory->getClient();
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public function register($name, $window = 3600, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = $this->requestStack->getCurrentRequest()->getClientIp();
}
$key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier;
// Add a key for the event to the sorted set, the score is timestamp, so we
// can count them easily.
$this->client->zAdd($key, $_SERVER['REQUEST_TIME'] + $window, microtime(TRUE));
// Set or update the expiration for the sorted set, it will be removed if
// the newest entry expired.
$this->client->expire($key, $_SERVER['REQUEST_TIME'] + $window);
}
/**
* {@inheritdoc}
*/
public function clear($name, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = $this->requestStack->getCurrentRequest()->getClientIp();
}
$key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier;
$this->client->del($key);
}
/**
* {@inheritdoc}
*/
public function isAllowed($name, $threshold, $window = 3600, $identifier = NULL) {
if (!isset($identifier)) {
$identifier = $this->requestStack->getCurrentRequest()->getClientIp();
}
$key = $this->getPrefix() . ':flood:' . $name . ':' . $identifier;
// Count the in the last $window seconds.
$number = $this->client->zCount($key, $_SERVER['REQUEST_TIME'], 'inf');
return ($number < $threshold);
}
/**
* {@inheritdoc}
*/
public function garbageCollection() {
// No garbage collection necessary.
}
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\redis\Lock;
use Drupal\redis\ClientFactory;
/**
* Lock backend singleton handling.
*/
class LockFactory {
/**
* @var \Drupal\redis\ClientInterface
*/
protected $clientFactory;
/**
* Creates a redis LockFactory.
*/
public function __construct(ClientFactory $client_factory) {
$this->clientFactory = $client_factory;
}
/**
* Get actual lock backend.
*
* @param bool $persistent
* (optional) Whether to return a persistent lock implementation or not.
*
* @return \Drupal\Core\Lock\LockBackendInterface
* Return lock backend instance.
*/
public function get($persistent = FALSE) {
$class_name = $this->clientFactory->getClass($persistent ? ClientFactory::REDIS_IMPL_PERSISTENT_LOCK : ClientFactory::REDIS_IMPL_LOCK);
return new $class_name($this->clientFactory);
}
}
@@ -0,0 +1,147 @@
<?php
namespace Drupal\redis\Lock;
use Drupal\Core\Lock\LockBackendAbstract;
use Drupal\redis\ClientFactory;
use Drupal\redis\RedisPrefixTrait;
/**
* Predis lock backend implementation.
*/
class PhpRedis extends LockBackendAbstract {
use RedisPrefixTrait;
/**
* @var \Redis
*/
protected $client;
/**
* Creates a PHpRedis cache backend.
*/
public function __construct(ClientFactory $factory) {
$this->client = $factory->getClient();
// __destruct() is causing problems with garbage collections, register a
// shutdown function instead.
drupal_register_shutdown_function([$this, 'releaseAll']);
}
/**
* Generate a redis key name for the current lock name.
*
* @param string $name
* Lock name.
*
* @return string
* The redis key for the given lock.
*/
protected function getKey($name) {
return $this->getPrefix() . ':lock:' . $name;
}
/**
* {@inheritdoc}
*/
public function acquire($name, $timeout = 30.0) {
$key = $this->getKey($name);
$id = $this->getLockId();
// Insure that the timeout is at least 1 ms.
$timeout = max($timeout, 0.001);
// If we already have the lock, check for his owner and attempt a new EXPIRE
// command on it.
if (isset($this->locks[$name])) {
// Create a new transaction, for atomicity.
$this->client->watch($key);
// Global tells us we are the owner, but in real life it could have expired
// and another process could have taken it, check that.
if ($this->client->get($key) != $id) {
// Explicit UNWATCH we are not going to run the MULTI/EXEC block.
$this->client->unwatch();
unset($this->locks[$name]);
return FALSE;
}
$result = $this->client->multi()
->psetex($key, (int) ($timeout * 1000), $id)
->exec();
// If the set failed, someone else wrote the key, we failed to acquire
// the lock.
if (FALSE === $result) {
unset($this->locks[$name]);
// Explicit transaction release which also frees the WATCH'ed key.
$this->client->discard();
return FALSE;
}
return ($this->locks[$name] = TRUE);
}
else {
// Use a SET with microsecond expiration and the NX flag, which will only
// succeed if the key does not exist yet.
$result = $this->client->set($key, $id, ['nx', 'px' => (int) ($timeout * 1000)]);
// If the result is FALSE, we failed to acquire the lock.
if (FALSE === $result) {
return FALSE;
}
// Register the lock.
return ($this->locks[$name] = TRUE);
}
}
/**
* {@inheritdoc}
*/
public function lockMayBeAvailable($name) {
$key = $this->getKey($name);
$value = $this->client->get($key);
// In Drupal 7, this method treated the lock as available if the ID did
// match. The database backend and test expects it to return FALSE in that
// case, updated accordingly.
return FALSE === $value;
}
/**
* {@inheritdoc}
*/
public function release($name) {
$key = $this->getKey($name);
$id = $this->getLockId();
unset($this->locks[$name]);
// Ensure the lock deletion is an atomic transaction. If another thread
// manages to removes all lock, we can not alter it anymore else we will
// release the lock for the other thread and cause race conditions.
$this->client->watch($key);
if ($this->client->get($key) == $id) {
$this->client->multi();
$this->client->delete($key);
$this->client->exec();
}
else {
$this->client->unwatch();
}
}
/**
* {@inheritdoc}
*/
public function releaseAll($lock_id = NULL) {
// We can afford to deal with a slow algorithm here, this should not happen
// on normal run because we should have removed manually all our locks.
foreach ($this->locks as $name => $foo) {
$this->release($name);
}
}
}
@@ -0,0 +1,136 @@
<?php
namespace Drupal\redis\Lock;
use Drupal\Core\Lock\LockBackendAbstract;
use Drupal\redis\ClientFactory;
use Drupal\redis\RedisPrefixTrait;
/**
* Predis lock backend implementation.
*/
class Predis extends LockBackendAbstract {
use RedisPrefixTrait;
/**
* @var \Predis\Client
*/
protected $client;
/**
* Creates a PHpRedis cache backend.
*/
public function __construct(ClientFactory $factory) {
$this->client = $factory->getClient();
// __destruct() is causing problems with garbage collections, register a
// shutdown function instead.
drupal_register_shutdown_function([$this, 'releaseAll']);
}
/**
* Generate a redis key name for the current lock name.
*
* @param string $name
* Lock name.
*
* @return string
* The redis key for the given lock.
*/
protected function getKey($name) {
return $this->getPrefix() . ':lock:' . $name;
}
public function acquire($name, $timeout = 30.0) {
$key = $this->getKey($name);
$id = $this->getLockId();
// Insure that the timeout is at least 1 ms.
$timeout = max($timeout, 0.001);
// If we already have the lock, check for his owner and attempt a new EXPIRE
// command on it.
if (isset($this->locks[$name])) {
// Create a new transaction, for atomicity.
$this->client->watch($key);
// Global tells us we are the owner, but in real life it could have expired
// and another process could have taken it, check that.
if ($this->client->get($key) != $id) {
// Explicit UNWATCH we are not going to run the MULTI/EXEC block.
$this->client->unwatch();
unset($this->locks[$name]);
return FALSE;
}
$result = $this->client->pipeline()
->psetex($key, (int) ($timeout * 1000), $id)
->exec();
// If the set failed, someone else wrote the key, we failed to acquire
// the lock.
if (FALSE === $result) {
unset($this->locks[$name]);
// Explicit transaction release which also frees the WATCH'ed key.
$this->client->discard();
return FALSE;
}
return ($this->locks[$name] = TRUE);
}
else {
// Use a SET with microsecond expiration and the NX flag, which will only
// succeed if the key does not exist yet.
$result = $this->client->set($key, $id, 'nx', 'px', (int) ($timeout * 1000));
// If the result is FALSE, we failed to acquire the lock.
if (FALSE === $result) {
return FALSE;
}
// Register the lock.
return ($this->locks[$name] = TRUE);
}
}
public function lockMayBeAvailable($name) {
$key = $this->getKey($name);
$value = $this->client->get($key);
// In Drupal 7, this method treated the lock as available if the ID did
// match. The database backend and test expects it to return FALSE in that
// case, updated accordingly.
return FALSE === $value;
}
public function release($name) {
$key = $this->getKey($name);
$id = $this->getLockId();
unset($this->locks[$name]);
// Ensure the lock deletion is an atomic transaction. If another thread
// manages to removes all lock, we can not alter it anymore else we will
// release the lock for the other thread and cause race conditions.
$this->client->watch($key);
if ($this->client->get($key) == $id) {
$pipe = $this->client->pipeline();
$pipe->del([$key]);
$pipe->execute();
}
else {
$this->client->unwatch();
}
}
public function releaseAll($lock_id = NULL) {
// We can afford to deal with a slow algorithm here, this should not happen
// on normal run because we should have removed manually all our locks.
foreach ($this->locks as $name => $foo) {
$this->release($name);
}
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\redis\PersistentLock;
use Drupal\redis\ClientFactory;
/**
* PHpRedis persistent lock backend
*/
class PhpRedis extends \Drupal\redis\Lock\PhpRedis {
/**
* Creates a PHpRedis persistent lock backend.
*/
public function __construct(ClientFactory $factory) {
// Do not call the parent constructor to avoid registering a shutdown
// function that releases all the locks at the end of a request.
$this->client = $factory->getClient();
// Set the lockId to a fixed string to make the lock ID the same across
// multiple requests. The lock ID is used as a page token to relate all the
// locks set during a request to each other.
// @see \Drupal\Core\Lock\LockBackendInterface::getLockId()
$this->lockId = 'persistent';
}
}
@@ -0,0 +1,148 @@
<?php
namespace Drupal\redis\Queue;
/**
* Redis queue implementation using PhpRedis extension backend.
*
* @ingroup queue
*/
class PhpRedis extends QueueBase {
/**
* The Redis connection.
*
* @var \Redis $client
*/
protected $client;
/**
* Constructs a \Drupal\redis\Queue\PhpRedis object.
*
* @param string $name
* The name of the queue.
* @param array $settings
* Array of Redis-related settings for this queue.
* @param \Redis $client
* The PhpRedis client.
*/
public function __construct($name, array $settings, \Redis $client) {
parent::__construct($name, $settings);
$this->client = $client;
}
/**
* {@inheritdoc}
*/
public function createItem($data) {
$record = new \stdClass();
$record->data = $data;
$record->qid = $this->incrementId();
// We cannot rely on REQUEST_TIME because many items might be created
// by a single request which takes longer than 1 second.
$record->timestamp = time();
if (!$this->client->hsetnx($this->availableItems, $record->qid, serialize($record))) {
return FALSE;
}
$start_len = $this->client->lLen($this->availableListKey);
if ($start_len < $this->client->lpush($this->availableListKey, $record->qid)) {
return $record->qid;
}
return FALSE;
}
/**
* Gets next serial ID for Redis queue items.
*
* @return int
* Next serial ID for Redis queue item.
*/
protected function incrementId() {
return $this->client->incr($this->incrementCounterKey);
}
/**
* {@inheritdoc}
*/
public function numberOfItems() {
return $this->client->lLen($this->availableListKey);
}
/**
* {@inheritdoc}
*/
public function claimItem($lease_time = 30) {
// Is it OK to do garbage collection here (we need to loop list of claimed
// items)?
$this->garbageCollection();
$item = FALSE;
if ($this->reserveTimeout !== NULL) {
// A blocking version of claimItem to be used with long-running queue workers.
$qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout);
}
else {
$qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey);
}
if ($qid) {
$job = $this->client->hget($this->availableItems, $qid);
if ($job) {
$item = unserialize($job);
$this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1');
}
}
return $item;
}
/**
* {@inheritdoc}
*/
public function releaseItem($item) {
$this->client->lrem($this->claimedListKey, $item->qid, -1);
$this->client->lpush($this->availableListKey, $item->qid);
}
/**
* {@inheritdoc}
*/
public function deleteItem($item) {
$this->client->lrem($this->claimedListKey, $item->qid, -1);
$this->client->hdel($this->availableItems, $item->qid);
}
/**
* {@inheritdoc}
*/
public function deleteQueue() {
$keys_to_remove = [
$this->claimedListKey,
$this->availableListKey,
$this->availableItems,
$this->incrementCounterKey
];
foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) {
$keys_to_remove[] = $key;
}
$this->client->del($keys_to_remove);
}
/**
* Automatically release items, that have been claimed and exceeded lease time.
*/
protected function garbageCollection() {
foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) {
if (!$this->client->exists($this->leasedKeyPrefix . $qid)) {
// The lease expired for this ID.
$this->client->lrem($this->claimedListKey, $qid, -1);
$this->client->lpush($this->availableListKey, $qid);
}
}
}
}
@@ -0,0 +1,152 @@
<?php
namespace Drupal\redis\Queue;
/**
* Redis queue implementation using Predis library backend.
*
* @ingroup queue
*/
class Predis extends QueueBase {
/**
* The Redis connection.
*
* @var \Predis\Client $client
*/
protected $client;
/**
* Constructs a \Drupal\redis\Queue\Predis object.
*
* @param string $name
* The name of the queue.
* @param array $settings
* Array of Redis-related settings for this queue.
* @param \Predis\Client $client
* The Predis client.
*/
public function __construct($name, array $settings, \Predis\Client $client) {
parent::__construct($name, $settings);
$this->client = $client;
}
/**
* {@inheritdoc}
*/
public function createItem($data) {
// TODO: Fixme
$record = new \stdClass();
$record->data = $data;
$record->qid = $this->incrementId();
// We cannot rely on REQUEST_TIME because many items might be created
// by a single request which takes longer than 1 second.
$record->timestamp = time();
if (!$this->client->hsetnx($this->availableItems, $record->qid, serialize($record))) {
return FALSE;
}
$start_len = $this->client->lLen($this->availableListKey);
if ($start_len < $this->client->lpush($this->availableListKey, $record->qid)) {
return $record->qid;
}
}
/**
* Gets next serial ID for Redis queue items.
*
* @return int
* Next serial ID for Redis queue item.
*/
protected function incrementId() {
// TODO: Fixme
return $this->client->incr($this->incrementCounterKey);
}
/**
* {@inheritdoc}
*/
public function numberOfItems() {
// TODO: Fixme
return $this->client->lLen($this->availableListKey);
}
/**
* {@inheritdoc}
*/
public function claimItem($lease_time = 30) {
// Is it OK to do garbage collection here (we need to loop list of claimed
// items)?
$this->garbageCollection();
$item = FALSE;
if ($this->reserveTimeout !== NULL) {
// A blocking version of claimItem to be used with long-running queue workers.
$qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout);
}
else {
$qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey);
}
if ($qid) {
$job = $this->client->hget($this->availableItems, $qid);
if ($job) {
$item = unserialize($job);
$this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1');
}
}
return $item;
}
/**
* {@inheritdoc}
*/
public function releaseItem($item) {
// TODO: Fixme
$this->client->lrem($this->claimedListKey, $item->qid, -1);
$this->client->lpush($this->availableListKey, $item->qid);
}
/**
* {@inheritdoc}
*/
public function deleteItem($item) {
// TODO: Fixme
$this->client->lrem($this->claimedListKey, $item->qid, -1);
$this->client->hdel($this->availableItems, $item->qid);
}
/**
* {@inheritdoc}
*/
public function deleteQueue() {
// TODO: Fixme
$keys_to_remove = [
$this->claimedListKey,
$this->availableListKey,
$this->availableItems,
$this->incrementCounterKey
];
foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) {
$keys_to_remove[] = $key;
}
$this->client->del($keys_to_remove);
}
/**
* Automatically release items, that have been claimed and exceeded lease time.
*/
protected function garbageCollection() {
foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) {
if (!$this->client->exists($this->leasedKeyPrefix . $qid)) {
// The lease expired for this ID.
$this->client->lrem($this->claimedListKey, $qid, -1);
$this->client->lpush($this->availableListKey, $qid);
}
}
}
}
@@ -0,0 +1,96 @@
<?php
namespace Drupal\redis\Queue;
use Drupal\Core\Queue\QueueInterface;
/**
* Redis queue implementation.
*
* @ingroup queue
*/
abstract class QueueBase implements QueueInterface {
/**
* Prefix used with all keys.
*/
const KEY_PREFIX = 'drupal:queue:';
/**
* The name of the queue this instance is working with.
*
* @var string
*/
protected $name;
/**
* Key for list of available items.
*
* @var string
*/
protected $availableListKey;
/**
* Key for list of claimed items.
*
* @var string
*/
protected $claimedListKey;
/**
* Key prefix for items that are used to track expiration of leased items.
*
* @var string
*/
protected $leasedKeyPrefix;
/**
* Key of increment counter key.
*
* @var string
*/
protected $incrementCounterKey;
/**
* Key for hash table of available queue items.
*
* @var string
*/
protected $availableItems;
/**
* Reserve timeout for blocking item claim.
*
* This will be set to number of seconds to wait for an item to be claimed.
* Non-blocking approach will be used when set to NULL.
*
* @var int|null
*/
protected $reserveTimeout;
/**
* Constructs a \Drupal\Core\Queue\DatabaseQueue object.
*
* @param string $name
* The name of the queue.
* @param array $settings
* Array of Redis-related settings for this queue.
*/
public function __construct($name, array $settings) {
$this->name = $name;
$this->reserveTimeout = $settings['reserve_timeout'];
$this->availableListKey = static::KEY_PREFIX . $name . ':avail';
$this->availableItems = static::KEY_PREFIX . $name . ':items';
$this->claimedListKey = static::KEY_PREFIX . $name . ':claimed';
$this->leasedKeyPrefix = static::KEY_PREFIX . $name . ':lease:';
$this->incrementCounterKey = static::KEY_PREFIX . $name . ':counter';
}
/**
* {@inheritdoc}
*/
public function createQueue() {
// Nothing to do here.
}
}
@@ -0,0 +1,56 @@
<?php
namespace Drupal\redis\Queue;
use Drupal\Core\Site\Settings;
use Drupal\redis\ClientFactory;
/**
* Defines the queue factory for the Redis backend.
*/
class QueueRedisFactory {
/**
* Queue implementation class namespace prefix.
*/
const CLASS_NAMESPACE = ClientFactory::REDIS_IMPL_QUEUE;
/**
* @var \Drupal\redis\ClientFactory
*/
protected $clientFactory;
/**
* The settings array.
*
* @var \Drupal\Core\Site\Settings
*/
protected $settings;
/**
* Constructs this factory object.
*
* @param \Drupal\Core\Database\Connection $connection
* The Connection object containing the key-value tables.
*/
public function __construct(ClientFactory $client_factory, Settings $settings) {
$this->clientFactory = $client_factory;
$this->settings = $settings;
}
/**
* Constructs a new queue object for a given name.
*
* @param string $name
* The name of the collection holding key and value pairs.
*
* @return \Drupal\Core\Queue\DatabaseQueue
* A key/value store implementation for the given $collection.
*/
public function get($name) {
$settings = $this->settings->get('redis_queue_' . $name, ['reserve_timeout' => NULL]);
$class_name = $this->clientFactory->getClass(static::CLASS_NAMESPACE);
return new $class_name($name, $settings, $this->clientFactory->getClient());
}
}
@@ -0,0 +1,153 @@
<?php
namespace Drupal\redis\Queue;
/**
* Redis queue implementation using PhpRedis extension backend.
*
* @ingroup queue
*/
class ReliablePhpRedis extends ReliableQueueBase {
/**
* The Redis connection.
*
* @var \Redis $client
*/
protected $client;
/**
* Constructs a \Drupal\redis\Queue\PhpRedis object.
*
* @param string $name
* The name of the queue.
* @param array $settings
* Array of Redis-related settings for this queue.
* @param \Redis $client
* The PhpRedis client.
*/
public function __construct($name, array $settings, \Redis $client) {
parent::__construct($name, $settings);
$this->client = $client;
}
/**
* {@inheritdoc}
*/
public function createItem($data) {
$record = new \stdClass();
$record->data = $data;
$record->qid = $this->incrementId();
// We cannot rely on REQUEST_TIME because many items might be created
// by a single request which takes longer than 1 second.
$record->timestamp = time();
$result = $this->client->multi()
->hsetnx($this->availableItems, $record->qid, serialize($record))
->lLen($this->availableListKey)
->lpush($this->availableListKey, $record->qid)
->exec();
$success = $result[0] && $result[2] > $result[1];
return $success ? $record->qid : FALSE;
}
/**
* Gets next serial ID for Redis queue items.
*
* @return int
* Next serial ID for Redis queue item.
*/
protected function incrementId() {
return $this->client->incr($this->incrementCounterKey);
}
/**
* {@inheritdoc}
*/
public function numberOfItems() {
return $this->client->lLen($this->availableListKey);
}
/**
* {@inheritdoc}
*/
public function claimItem($lease_time = 30) {
// Is it OK to do garbage collection here (we need to loop list of claimed
// items)?
$this->garbageCollection();
$item = FALSE;
if ($this->reserveTimeout !== NULL) {
// A blocking version of claimItem to be used with long-running queue workers.
$qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout);
}
else {
$qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey);
}
if ($qid) {
$job = $this->client->hget($this->availableItems, $qid);
if ($job) {
$item = unserialize($job);
$this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1');
}
}
return $item;
}
/**
* {@inheritdoc}
*/
public function releaseItem($item) {
$this->client->multi()
->lrem($this->claimedListKey, $item->qid, -1)
->lpush($this->availableListKey, $item->qid)
->exec();
}
/**
* {@inheritdoc}
*/
public function deleteItem($item) {
$this->client->multi()
->lrem($this->claimedListKey, $item->qid, -1)
->hdel($this->availableItems, $item->qid)
->exec();
}
/**
* {@inheritdoc}
*/
public function deleteQueue() {
$keys_to_remove = [
$this->claimedListKey,
$this->availableListKey,
$this->availableItems,
$this->incrementCounterKey
];
foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) {
$keys_to_remove[] = $key;
}
$this->client->del($keys_to_remove);
}
/**
* Automatically release items, that have been claimed and exceeded lease time.
*/
protected function garbageCollection() {
foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) {
if (!$this->client->exists($this->leasedKeyPrefix . $qid)) {
// The lease expired for this ID.
$this->client->multi()
->lrem($this->claimedListKey, $qid, -1)
->lpush($this->availableListKey, $qid)
->exec();
}
}
}
}
@@ -0,0 +1,156 @@
<?php
namespace Drupal\redis\Queue;
/**
* Redis queue implementation using Predis library backend.
*
* @ingroup queue
*/
class ReliablePredis extends ReliableQueueBase {
/**
* The Redis connection.
*
* @var \Predis\Client $client
*/
protected $client;
/**
* Constructs a \Drupal\redis\Queue\Predis object.
*
* @param string $name
* The name of the queue.
* @param array $settings
* Array of Redis-related settings for this queue.
* @param \Predis\Client $client
* The Predis client.
*/
public function __construct($name, array $settings, \Predis\Client $client) {
parent::__construct($name, $settings);
$this->client = $client;
}
/**
* {@inheritdoc}
*/
public function createItem($data) {
$record = new \stdClass();
$record->data = $data;
$record->qid = $this->incrementId();
// We cannot rely on REQUEST_TIME because many items might be created
// by a single request which takes longer than 1 second.
$record->timestamp = time();
$pipe = $this->client->pipeline();
$pipe->hsetnx($this->availableItems, $record->qid, serialize($record));
$pipe->lLen($this->availableListKey);
$pipe->lpush($this->availableListKey, $record->qid);
$result = $pipe->execute();
$success = $result[0] && $result[2] > $result[1];
return $success ? $record->qid : FALSE;
}
/**
* Gets next serial ID for Redis queue items.
*
* @return int
* Next serial ID for Redis queue item.
*/
protected function incrementId() {
// TODO: Fixme
return $this->client->incr($this->incrementCounterKey);
}
/**
* {@inheritdoc}
*/
public function numberOfItems() {
// TODO: Fixme
return $this->client->lLen($this->availableListKey);
}
/**
* {@inheritdoc}
*/
public function claimItem($lease_time = 30) {
// Is it OK to do garbage collection here (we need to loop list of claimed
// items)?
$this->garbageCollection();
$item = FALSE;
if ($this->reserveTimeout !== NULL) {
// A blocking version of claimItem to be used with long-running queue workers.
$qid = $this->client->brpoplpush($this->availableListKey, $this->claimedListKey, $this->reserveTimeout);
}
else {
$qid = $this->client->rpoplpush($this->availableListKey, $this->claimedListKey);
}
if ($qid) {
$job = $this->client->hget($this->availableItems, $qid);
if ($job) {
$item = unserialize($job);
$this->client->setex($this->leasedKeyPrefix . $item->qid, $lease_time, '1');
}
}
return $item;
}
/**
* {@inheritdoc}
*/
public function releaseItem($item) {
// TODO: Fixme
$this->client->pipeline()
->lrem($this->claimedListKey, $item->qid, -1)
->lpush($this->availableListKey, $item->qid)
->exec();
}
/**
* {@inheritdoc}
*/
public function deleteItem($item) {
// TODO: Fixme
$this->client->pipeline()
->lrem($this->claimedListKey, $item->qid, -1)
->hdel($this->availableItems, $item->qid)
->exec();
}
/**
* {@inheritdoc}
*/
public function deleteQueue() {
// TODO: Fixme
$keys_to_remove = [
$this->claimedListKey,
$this->availableListKey,
$this->availableItems,
$this->incrementCounterKey
];
foreach ($this->client->keys($this->leasedKeyPrefix . '*') as $key) {
$keys_to_remove[] = $key;
}
$this->client->del($keys_to_remove);
}
/**
* Automatically release items, that have been claimed and exceeded lease time.
*/
protected function garbageCollection() {
foreach ($this->client->lrange($this->claimedListKey, 0, -1) as $qid) {
if (!$this->client->exists($this->leasedKeyPrefix . $qid)) {
// The lease expired for this ID.
$this->client->lrem($this->claimedListKey, $qid, -1);
$this->client->lpush($this->availableListKey, $qid);
}
}
}
}
@@ -0,0 +1,14 @@
<?php
namespace Drupal\redis\Queue;
use Drupal\Core\Queue\ReliableQueueInterface;
/**
* Redis queue implementation.
*
* @ingroup queue
*/
abstract class ReliableQueueBase extends QueueBase implements ReliableQueueInterface {
}
@@ -0,0 +1,17 @@
<?php
namespace Drupal\redis\Queue;
use Drupal\redis\ClientFactory;
/**
* Defines the queue factory for the Redis backend.
*/
class ReliableQueueRedisFactory extends QueueRedisFactory {
/**
* Queue implementation class namespace prefix.
*/
const CLASS_NAMESPACE = ClientFactory::REDIS_IMPL_RELIABLE_QUEUE;
}
@@ -0,0 +1,89 @@
<?php
namespace Drupal\redis;
use Drupal\Core\Site\Settings;
trait RedisPrefixTrait {
/**
* @var string
*/
protected $prefix;
/**
* Get global default prefix
*
* @param string $suffix
*
* @return string
*/
protected function getDefaultPrefix($suffix = NULL) {
$ret = NULL;
if ($test_prefix = drupal_valid_test_ua()) {
$ret = $test_prefix;
}
else {
$prefixes = Settings::get('cache_prefix', '');
if (is_string($prefixes)) {
// Variable can be a string which then considered as a default
// behavior.
$ret = $prefixes;
}
else if (NULL !== $suffix && isset($prefixes[$suffix])) {
if (FALSE !== $prefixes[$suffix]) {
// If entry is set and not false an explicit prefix is set
// for the bin.
$ret = $prefixes[$suffix];
}
else {
// If we have an explicit false it means no prefix whatever
// is the default configuration.
$ret = '';
}
}
else {
// Key is not set, we can safely rely on default behavior.
if (isset($prefixes['default']) && FALSE !== $prefixes['default']) {
$ret = $prefixes['default'];
}
else {
// When default is not set or an explicit false this means
// no prefix.
$ret = '';
}
}
}
if (empty($ret)) {
// If no prefix is given, use the same logic as core for APCu caching.
$ret = Settings::getApcuPrefix('redis', DRUPAL_ROOT);
}
return $ret;
}
/**
* Set prefix
*
* @param string $prefix
*/
public function setPrefix($prefix) {
$this->prefix = $prefix;
}
/**
* Get prefix
*
* @return string
*/
protected function getPrefix() {
if (!isset($this->prefix)) {
$this->prefix = $this->getDefaultPrefix();
}
return $this->prefix;
}
}
@@ -0,0 +1,56 @@
<?php
namespace Drupal\Tests\redis\Functional\Lock;
use Drupal\Component\Utility\OpCodeCache;
use Drupal\Core\Database\Database;
use Drupal\Core\Site\Settings;
use Drupal\Tests\system\Functional\Lock\LockFunctionalTest;
use Drupal\Tests\redis\Traits\RedisTestInterfaceTrait;
/**
* Confirm locking works between two separate requests.
*
* @group redis
*/
class RedisLockFunctionalTest extends LockFunctionalTest {
use RedisTestInterfaceTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['redis'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Write the containers_yaml update by hand, since writeSettings() doesn't
// support this syntax.
$filename = $this->siteDirectory . '/settings.php';
chmod($filename, 0666);
$contents = file_get_contents($filename);
$redis_interface = self::getRedisInterfaceEnv();
$contents .= "\n\n" . '$settings[\'container_yamls\'][] = \'modules/redis/example.services.yml\';';
$contents .= "\n\n" . '$settings["redis.connection"]["interface"] = \'' . $redis_interface . '\';';
file_put_contents($filename, $contents);
$settings = Settings::getAll();
$settings['container_yamls'][] = 'modules/redis/example.services.yml';
$settings['redis.connection']['interface'] = '\'' . $redis_interface . '\'';
new Settings($settings);
OpCodeCache::invalidate(DRUPAL_ROOT . '/' . $filename);
$this->rebuildContainer();
// Get database schema.
$db_schema = Database::getConnection()->schema();
// Make sure that the semaphore table isn't used.
$db_schema->dropTable('semaphore');
}
}
@@ -0,0 +1,193 @@
<?php
namespace Drupal\Tests\redis\Functional;
use Drupal\Component\Utility\OpCodeCache;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\Database;
use Drupal\Core\Site\Settings;
use Drupal\field_ui\Tests\FieldUiTestTrait;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\redis\Traits\RedisTestInterfaceTrait;
/**
* Tests complex processes like installing modules with redis backends.
*
* @group redis
*/
class WebTest extends BrowserTestBase {
use FieldUiTestTrait;
use RedisTestInterfaceTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['redis', 'block'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_tasks_block');
// Set in-memory settings.
$settings = Settings::getAll();
// Get REDIS_INTERFACE env variable.
$redis_interface = self::getRedisInterfaceEnv();
$settings['redis.connection']['interface'] = $redis_interface;
$settings['cache'] = [
'default' => 'cache.backend.redis',
];
$settings['container_yamls'][] = drupal_get_path('module', 'redis') . '/example.services.yml';
$settings['bootstrap_container_definition'] = [
'parameters' => [],
'services' => [
'redis.factory' => [
'class' => 'Drupal\redis\ClientFactory',
],
'cache.backend.redis' => [
'class' => 'Drupal\redis\Cache\CacheBackendFactory',
'arguments' => ['@redis.factory', '@cache_tags_provider.container', '@serialization.phpserialize'],
],
'cache.container' => [
'class' => '\Drupal\redis\Cache\PhpRedis',
'factory' => ['@cache.backend.redis', 'get'],
'arguments' => ['container'],
],
'cache_tags_provider.container' => [
'class' => 'Drupal\redis\Cache\RedisCacheTagsChecksum',
'arguments' => ['@redis.factory'],
],
'serialization.phpserialize' => [
'class' => 'Drupal\Component\Serialization\PhpSerialize',
],
],
];
new Settings($settings);
// Write the containers_yaml update by hand, since writeSettings() doesn't
// support some of the definitions.
$filename = $this->siteDirectory . '/settings.php';
chmod($filename, 0666);
$contents = file_get_contents($filename);
// Add the container_yaml and cache definition.
$contents .= "\n\n" . '$settings["container_yamls"][] = "' . drupal_get_path('module', 'redis') . '/example.services.yml";';
$contents .= "\n\n" . '$settings["cache"] = ' . var_export($settings['cache'], TRUE) . ';';
// Add the classloader.
$contents .= "\n\n" . '$class_loader->addPsr4(\'Drupal\\\\redis\\\\\', \'' . drupal_get_path('module', 'redis') . '/src\');';
// Add the bootstrap container definition.
$contents .= "\n\n" . '$settings["bootstrap_container_definition"] = ' . var_export($settings['bootstrap_container_definition'], TRUE) . ';';
file_put_contents($filename, $contents);
OpCodeCache::invalidate(DRUPAL_ROOT . '/' . $filename);
// Reset the cache factory.
$this->container->set('cache.factory', NULL);
$this->rebuildContainer();
// Get database schema.
$db_schema = Database::getConnection()->schema();
// Make sure that the cache and lock tables aren't used.
$db_schema->dropTable('cache_default');
$db_schema->dropTable('cache_render');
$db_schema->dropTable('cache_config');
$db_schema->dropTable('cache_container');
$db_schema->dropTable('cachetags');
$db_schema->dropTable('semaphore');
$db_schema->dropTable('flood');
}
/**
* Tests enabling modules and creating configuration.
*/
public function testModuleInstallation() {
$admin_user = $this->createUser([], NULL, TRUE);
$this->drupalLogin($admin_user);
// Enable a few modules.
$edit["modules[node][enable]"] = TRUE;
$edit["modules[views][enable]"] = TRUE;
$edit["modules[field_ui][enable]"] = TRUE;
$edit["modules[text][enable]"] = TRUE;
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->drupalPostForm(NULL, [], t('Continue'));
$assert = $this->assertSession();
// The order of the modules is not guaranteed, so just assert that they are
// all listed.
$assert->elementTextContains('css', '.messages--status', '6 modules have been enabled');
$assert->elementTextContains('css', '.messages--status', 'Field UI');
$assert->elementTextContains('css', '.messages--status', 'Node');
$assert->elementTextContains('css', '.messages--status', 'Text');
$assert->elementTextContains('css', '.messages--status', 'Views');
$assert->elementTextContains('css', '.messages--status', 'Field');
$assert->elementTextContains('css', '.messages--status', 'Filter');
$assert->checkboxChecked('edit-modules-field-ui-enable');
// Create a node type with a field.
$edit = [
'name' => $this->randomString(),
'type' => $node_type = Unicode::strtolower($this->randomMachineName()),
];
$this->drupalPostForm('admin/structure/types/add', $edit, t('Save and manage fields'));
$field_name = Unicode::strtolower($this->randomMachineName());
$this->fieldUIAddNewField('admin/structure/types/manage/' . $node_type, $field_name, NULL, 'text');
// Create a node, check display, edit, verify that it has been updated.
$edit = [
'title[0][value]' => $this->randomMachineName(),
'body[0][value]' => $this->randomMachineName(),
'field_' . $field_name . '[0][value]' => $this->randomMachineName(),
];
$this->drupalPostForm('node/add/' . $node_type, $edit, t('Save and publish'));
// Test the output as anonymous user.
$this->drupalLogout();
$this->drupalGet('node');
$this->assertSession()->responseContains($edit['title[0][value]']);
$this->drupalLogin($admin_user);
$this->drupalGet('node');
$this->clickLink($edit['title[0][value]']);
$this->assertSession()->responseContains($edit['body[0][value]']);
$this->clickLink(t('Edit'));
$update = [
'title[0][value]' => $this->randomMachineName(),
];
$this->drupalPostForm(NULL, $update, t('Save and keep published'));
$this->assertSession()->responseContains($update['title[0][value]']);
$this->drupalGet('node');
$this->assertSession()->responseContains($update['title[0][value]']);
$this->drupalLogout();
$this->drupalGet('node');
$this->clickLink($update['title[0][value]']);
$this->assertSession()->responseContains($edit['body[0][value]']);
// Get database schema.
$db_schema = Database::getConnection()->schema();
$this->assertFalse($db_schema->tableExists('cache_default'));
$this->assertFalse($db_schema->tableExists('cache_render'));
$this->assertFalse($db_schema->tableExists('cache_config'));
$this->assertFalse($db_schema->tableExists('cache_container'));
$this->assertFalse($db_schema->tableExists('cachetags'));
$this->assertFalse($db_schema->tableExists('semaphore'));
$this->assertFalse($db_schema->tableExists('flood'));
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\Tests\redis\Kernel;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\Core\Cache\GenericCacheBackendUnitTestBase;
use Symfony\Component\DependencyInjection\Reference;
use Drupal\Tests\redis\Traits\RedisTestInterfaceTrait;
/**
* Tests Redis cache backend using GenericCacheBackendUnitTestBase.
*
* @group redis
*/
class RedisCacheTest extends GenericCacheBackendUnitTestBase {
use RedisTestInterfaceTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['system', 'redis'];
public function register(ContainerBuilder $container) {
self::setUpSettings();
parent::register($container);
// Replace the default checksum service with the redis implementation.
if ($container->has('redis.factory')) {
$container->register('cache_tags.invalidator.checksum', 'Drupal\redis\Cache\RedisCacheTagsChecksum')
->addArgument(new Reference('redis.factory'))
->addTag('cache_tags_invalidator');
}
}
/**
* Creates a new instance of PhpRedis cache backend.
*
* @return \Drupal\redis\Cache\PhpRedis
* A new PhpRedis cache backend.
*/
protected function createCacheBackend($bin) {
$cache = \Drupal::service('cache.backend.redis')->get($bin);
$cache->setMinTtl(10);
return $cache;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\Tests\redis\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\redis\Flood\PhpRedis;
use Drupal\Tests\redis\Traits\RedisTestInterfaceTrait;
/**
* Tests Redis flood backend.
*
* @group redis
*/
class RedisFloodTest extends KernelTestBase {
use RedisTestInterfaceTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['redis'];
/**
* Test flood control.
*/
public function testFlood() {
self::setUpSettings();
$threshold = 2;
$window = 1;
$name = 'flood_test_cleanup';
$client_factory = \Drupal::service('redis.factory');
$request_stack = \Drupal::service('request_stack');
$flood = new PhpRedis($client_factory, $request_stack);
// By default the event is allowed.
$this->assertTrue($flood->isAllowed($name, $threshold));
// Register event.
$flood->register($name, $window);
// The event is still allowed.
$this->assertTrue($flood->isAllowed($name, $threshold));
$flood->register($name, $window);
// Verify event is not allowed.
$this->assertFalse($flood->isAllowed($name, $threshold));
// "Sleep" two seconds, then the event is allowed again.
$_SERVER['REQUEST_TIME'] += 2;
$this->assertTrue($flood->isAllowed($name, $threshold));
}
}
@@ -0,0 +1,95 @@
<?php
namespace Drupal\Tests\redis\Kernel;
use Drupal\redis\ClientFactory;
use \Drupal\KernelTests\Core\Queue\QueueTest as CoreQueueTest;
use Drupal\Tests\redis\Traits\RedisTestInterfaceTrait;
/**
* Tests the Redis queue functions.
*
* @group redis
*/
class RedisQueueTest extends CoreQueueTest {
use RedisTestInterfaceTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['redis'];
/**
* Tests Redis non-blocking queue.
*/
public function testRedisNonBlockingQueue() {
self::setUpSettings();
$client_factory = \Drupal::service('redis.factory');
$settings = ['reserve_timeout' => NULL];
$class_name = $client_factory->getClass(ClientFactory::REDIS_IMPL_QUEUE);
/** @var \Drupal\Core\Queue\QueueInterface $queue1 */
$queue1 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient());
$queue1->createQueue();
/** @var \Drupal\Core\Queue\QueueInterface $queue2 */
$queue2 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient());
$queue2->createQueue();
$this->queueTest($queue1, $queue2);
$queue1->deleteQueue();
$queue2->deleteQueue();
$class_name = $client_factory->getClass(ClientFactory::REDIS_IMPL_RELIABLE_QUEUE);
/** @var \Drupal\Core\Queue\QueueInterface $queue1 */
$queue1 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient());
$queue1->createQueue();
/** @var \Drupal\Core\Queue\QueueInterface $queue2 */
$queue2 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient());
$queue2->createQueue();
$this->queueTest($queue1, $queue2);
}
/**
* Tests Redis blocking queue.
*/
public function testRedisBlockingQueue() {
self::setUpSettings();
// Create two queues.
$client_factory = \Drupal::service('redis.factory');
$settings = ['reserve_timeout' => 30];
$class_name = $client_factory->getClass(ClientFactory::REDIS_IMPL_QUEUE);
/** @var \Drupal\Core\Queue\QueueInterface $queue1 */
$queue1 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient());
$queue1->createQueue();
/** @var \Drupal\Core\Queue\QueueInterface $queue2 */
$queue2 = new $class_name($this->randomMachineName(), $settings, $client_factory->getClient());
$queue2->createQueue();
$this->queueTest($queue1, $queue2);
}
/**
* Overrides \Drupal\system\Tests\Queue\QueueTestQueueTest::testSystemQueue().
*
* We override tests from core class we extend to prevent them from running.
*/
public function testSystemQueue() {}
/**
* Overrides \Drupal\system\Tests\Queue\QueueTestQueueTest::testMemoryQueue().
*
* We override tests from core class we extend to prevent them from running.
*/
public function testMemoryQueue() {}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\Tests\redis\Traits;
use Drupal\Core\Site\Settings;
trait RedisTestInterfaceTrait {
/**
* Uses an env variable to set the redis client to use for this test.
*/
public function setUpSettings() {
// Write redis_interface settings manually.
$redis_interface = self::getRedisInterfaceEnv();
$settings = Settings::getAll();
$settings['redis.connection']['interface'] = $redis_interface;
new Settings($settings);
}
/**
* Uses an env variable to set the redis client to use for this test.
*/
public function getRedisInterfaceEnv() {
// Get REDIS_INTERFACE from env variable.
$redis_interface = getenv('REDIS_INTERFACE');
// Default to PhpRedis is env variable not available.
if ($redis_interface == FALSE) {
$redis_interface = 'PhpRedis';
}
return $redis_interface;
}
}