added redis module
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user