added redis contrib module

This commit is contained in:
Bachir Soussi Chiadmi 2018-02-21 14:07:00 +01:00
parent 1eb61fe020
commit 18f4aba146
69 changed files with 6525 additions and 0 deletions

View File

@ -0,0 +1 @@
predis

View File

@ -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.

View File

@ -0,0 +1,37 @@
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/phpredis/phpredis
Most common Linux distribution should now have packages for this PHP extension
however if that's not the case for the one you use, use the above link to find
the release source download links and follow the provided instructions in order
to compile it for your system.
Default behavior is to connect via tcp://localhost:6379 but you might want to
connect differently.
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.txt file.
For this particular implementation, host settings are overridden by the
UNIX socket parameter.

View File

@ -0,0 +1,60 @@
Predis cache backend
====================
This module will work with the Predis 1.x version. Any earlier versions
are unsupported.
This client, for now, is only able to use the Predis PHP library.
The Predis library requires PHP 5.3 minimum. If your hosted environment does
not ships with at least PHP 5.3, please do not use this cache backend.
Please consider using an OPCode cache such as APC. Predis is a good and fully
featured API, the cost is that the code is a lot more than a single file in
opposition to some other backends such as the APC one.
Get Predis
----------
You can download this library at:
https://github.com/nrk/predis
This file explains how to install the Predis library and the Drupal cache
backend. If you are an advanced Drupal integrator, please consider the fact
that you can easily change all the pathes. Pathes used in this file are
likely to be default for non advanced users.
Download and install library
----------------------------
Once done, you either have to clone it into:
sites/all/libraries/predis
So that you have the following directory tree:
sites/all/libraries/predis/src/ # Where the PHP code stands
Or, any other place in order to share it:
For example, from your install profiles libraries folder:
profiles/example/libraries/predis
If you choose this solution, you have to alter a bit your $conf array into
the settings.php file as this:
define('PREDIS_BASE_PATH', DRUPAL_ROOT . '/profiles/example/libraries/predis/');
Connect to a remote host and database
-------------------------------------
See README.txt file.
Advanced configuration (PHP expert)
-----------------------------------
Best solution is, whatever is the place where you put the Predis library, that
you set up a fully working autoloader able to use it. The one being used by the
Redis module is a default fallback and will naturally being appened to the SPL
autoloader stack.

View File

@ -0,0 +1,40 @@
Redis module testing
====================
Unit tests won't cover the cache backend until the 8.x series.
This won't be fixed, by design. Drupal 8.x now ships a complete cache backend
unit tests suite which will be used when this module will be upgraded.
7.x testing
===========
7.x testing will keep it to the bare minimum and will test some minor bugs such
as admin UI or autoloading related bugs.
Cleanup environment
===================
php -f scripts/run-tests.sh -- --clean
Run common tests
================
php -f scripts/run-tests.sh -- --verbose --color \
--url "http://yoursite" \
--class "Redis_Tests_Client_UnitTestCase"
Run all PhpRedis tests
======================
php -f scripts/run-tests.sh -- --verbose --color \
--url "http://laborange.net" \
--class "Redis_Tests_Cache_PhpRedisFixesUnitTestCase,Redis_Tests_Cache_PhpRedisFlushUnitTestCase,Redis_Tests_Cache_PhpRedisShardedFixesUnitTestCase,Redis_Tests_Cache_PhpRedisShardedFlushUnitTestCase,Redis_Tests_Cache_PhpRedisShardedWithPipelineFixesUnitTestCase,Redis_Tests_Lock_PhpRedisLockingUnitTestCase,Redis_Tests_Path_PhpRedisPathUnitTestCase,Redis_Tests_Queue_PhpRedisQueueUnitTestCase"
Run all Predis tests
======================
php -f scripts/run-tests.sh -- --verbose --color \
--url "http://yoursite" \
--class "Redis_Tests_Cache_PredisFixesUnitTestCase,Redis_Tests_Cache_PredisFlushUnitTestCase,Redis_Tests_Cache_PredisShardedFixesUnitTestCase,Redis_Tests_Cache_PredisShardedFlushUnitTestCase,Redis_Tests_Cache_PredisShardedWithPipelineFixesUnitTestCase,Redis_Tests_Lock_PredisLockingUnitTestCase,Redis_Tests_Path_PredisPathUnitTestCase"

View File

@ -0,0 +1,474 @@
Redis cache backends
====================
This package provides two different Redis cache 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. Well, it will be technically possible, but it would be
quite a dumb thing to do.
Predis
------
This implementation uses the Predis PHP library. It is compatible PHP 5.3
only.
PhpRedis
--------
This implementation uses the PhpRedis PHP extention. In order to use it, you
probably will need to compile the extension yourself.
Redis version
-------------
This module requires Redis version to be 2.6.0 or later with LUA scrpting
enabled due to the EVAL command usage.
If you can't upgrade you Redis server:
- 3.x release will only officially support Redis server <= 2.8 and 3.x
nevertheless you may use it with Redis 2.4 if you configure your cache
backend to operate in sharding mode.
- For Redis 2.4 use the latest 2.x release of this module or use the
3.x release with sharding mode enabled.
- For Redis <=2.3 use any version of this module <=2.6
Notes
-----
Both backends provide the exact same functionalities. The major difference is
because PhpRedis uses a PHP extension, and not PHP code, it will performe a
lot better (Predis needs PHP userland code to be loaded).
Difference is not that visible, it's really a few millisec on my testing box,
in case you attempt to profile the code, traces will be a lot bigger.
Note that most of the settings are shared. See next sections.
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 and locks
and path alias cache replacement.
$conf['redis_client_interface'] = 'PhpRedis'; // Can be "Predis".
$conf['redis_client_host'] = '1.2.3.4'; // Your Redis instance hostname.
$conf['lock_inc'] = 'sites/all/modules/redis/redis.lock.inc';
$conf['path_inc'] = 'sites/all/modules/redis/redis.path.inc';
$conf['cache_backends'][] = 'sites/all/modules/redis/redis.autoload.inc';
$conf['cache_default_class'] = 'Redis_Cache';
See next chapters for more information.
Is there any cache bins that should *never* go into Redis?
----------------------------------------------------------
TL;DR: No. Except for 'cache_form' if you use Redis with LRU eviction.
Redis has been maturing a lot over time, and will apply different sensible
settings for different bins; It's today very stable.
Advanced configuration
======================
Use the compressed cache
------------------------
Please note this is for now an experimental feature. As a personnal note
from the module author, it should be safe to use.
Use this cache class setting to enable compression. This will save usually
about 80% RAM at the cost of some milliseconds server time.
$conf['cache_default_class'] = 'Redis_CacheCompressed';
Additionnaly, you can alter the default size compression threshold, under which
entries will not be compressed (size is in bytes, set 0 to always compress):
$conf['cache_compression_size_threshold'] = 100;
You can also change the compression level, which an positive integer between
1 and 9, 1 being the lowest but fastest compression ratio, 9 being the most
aggressive compression but is a lot slower. From testing, setting it to the
lower level (1) gives 80% memory usage decrease, which is more than enough.
$conf['cache_compression_ratio'] = 5;
Please note that those settings are global and not on a cache bin basis, you can
already control whenever the compression is to be used or not by selecting a
different cache class on per cache bin basis.
If you switch from the standard default backend (without compression) to the
compressed cache backend, it will recover transparently uncompressed data and
proceed normally without additional cache eviction, it safe to upgrade.
Donwgrading from compressed data to uncompressed data won't work, but the
cache backend will just give you cache hit miss and it will work seamlessly
too without any danger for the site.
Choose the Redis client library to use
--------------------------------------
Add into your settings.php file:
$conf['redis_client_interface'] = 'PhpRedis';
You can replace 'PhpRedis' with 'Predis', depending on the library you chose.
Note that this is optional but recommended. If you don't set this variable the
module will proceed to class lookups and attempt to choose the best client
available, having always a preference for the Predis one.
Tell Drupal to use the cache backend
------------------------------------
Usual cache backend configuration, as follows, to add into your settings.php
file like any other backend:
$conf['cache_backends'][] = 'sites/all/modules/redis/redis.autoload.inc';
$conf['cache_class_cache'] = 'Redis_Cache';
$conf['cache_class_cache_menu'] = 'Redis_Cache';
$conf['cache_class_cache_bootstrap'] = 'Redis_Cache';
// ... Any other bins.
Tell Drupal to use the lock backend
-----------------------------------
Usual lock backend override, update you settings.php file as this:
$conf['lock_inc'] = 'sites/all/modules/redis/redis.lock.inc';
Tell Drupal to use the path alias backend
-----------------------------------------
Usual path backend override, update you settings.php file as this:
$conf['path_inc'] = 'sites/all/modules/redis/redis.path.inc';
Notice that there is an additional variable for path handling that is set
per default which will ignore any path that is an admin path, gaining a few
SQL queries. If you want to be able to set aliases on admin path and restore
an almost default Drupal core behavior, you should add this line into your
settings.php file:
$conf['path_alias_admin_blacklist'] = FALSE;
Drupal 6 and lock backend
-------------------------
Considering this is a Drupal 7 module only downloading it in Drupal 6 will make
the module UI telling you this module is unsupported yet you can use the lock
backend on Drupal 6.
Read your Drupal 6 core documentation and use the redis.lock.inc file as
lock_inc replacement the same way its being done for Drupal 7 and it should
work. Note that this is untested by the module maintainer (feedback will be
greatly appreciated).
Common settings
===============
Connect throught a UNIX socket
------------------------------
All you have to do is specify this line:
$conf['redis_client_socket'] = '/some/path/redis.sock';
Both drivers support it.
Connect to a remote host
------------------------
If your Redis instance is remote, you can use this syntax:
$conf['redis_client_host'] = '1.2.3.4';
$conf['redis_client_port'] = 1234;
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:
$conf['redis_client_base'] = 12;
Please note that if you are working in shard mode, you should never set this
variable.
Connection to a password protected instance
-------------------------------------------
If you are using a password protected instance, specify the password this way:
$conf['redis_client_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 differenciate multiple sites using the same Redis instance and
database, you will need to specify a prefix for your site cache entries.
Important note: most people don't need that feature since that when no prefix
is specified, the Redis module will attempt to use the a hash of the database
credentials in order to provide a multisite safe default behavior. This means
that the module will also safely work in CLI scripts.
Cache prefix configuration attemps to use a unified variable accross 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:
$conf['cache_prefix'] = 'mysite_';
Alternatively, to provide the same functionality, you can provide the variable
as an array:
$conf['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 explicitely 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_'.
$conf['cache_prefix']['default'] = 'mysite_';
// Set no prefix explicitely for 'cache' and 'cache_bootstrap' bins.
$conf['cache_prefix']['cache'] = FALSE;
$conf['cache_prefix']['cache_bootstrap'] = FALSE;
// Set another prefix for 'cache_menu' bin.
$conf['cache_prefix']['cache_menu'] = 'menumysite_';
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.
Sharding vs normal mode
-----------------------
Per default the Redis cache backend will be in "normal" mode, meaning that
every flush call will trigger and EVAL lua script that will proceed to cache
wipeout and cleanup the Redis database from stalled entries.
Nevertheless, if you are working with a Redis server < 2.6 or in a sharded
environment, you cannot multiple keys per command nor proceed to EVAL'ed
scripts, you will then need to switch to the sharded mode.
Sharded mode will never delete entries on flush calls, but register a key
with the current flush time instead. Cache entries will then be deleted on
read if the entry checksum does not match or is older than the latest flush
call. Note that this mode is fast and safe, but must be used accordingly
with the default lifetime for permanent items, else your Redis server might
keep stalled entries into its database forever.
In order to enable the sharded mode, set into your settings.php file:
$conf['redis_flush_mode'] = 3;
Please note that the value 3 is there to keep backward compatibility with
older versions of the Redis module and will not change.
Note that previous Redis module version allowed to set a per-bin setting for
the clear mode value; nevertheless the clear mode is not a valid setting
anymore and the past issues have been resolved. Only the global value will
work as of now.
Sharding and pipelining
-----------------------
Whe using this module with sharding mode you may have a sharding proxy able to
do command pipelining. If that is the case, you should switch to "sharding with
pipelining" mode instead:
$conf['redis_flush_mode'] = 4;
Note that if you use the sharding mode because you use an older version of the
Redis server, you should always use this mode to ensure the best performances.
Default lifetime for permanent items
------------------------------------
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
Please also be careful about the fact that those settings are overriden by
the 'cache_lifetime' Drupal variable, which should always be set to 0.
Moreover, this setting will affect all cache entries without exception so
be careful and never set values too low if you don't want this setting to
override default expire value given by modules on temporary cache entries.
Lock backends
-------------
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 by using Redis transactions.
Queue backend
-------------
This module provides an experimental queue backend. It is for now implemented
only using the PhpRedis driver, any attempt to use it using Predis will result
in runtime errors.
If you want to change the queue driver system wide, set this into your
setting.php file:
$conf['queue_default_class'] = 'Redis_Queue';
$conf['queue_default_reliable_class'] = 'Redis_Queue';
Note that some queue implementations such as the batch queue are hardcoded
within Drupal and will always use a database dependent implementation.
If you need to proceed with finer tuning, you can set a per-queue class in
such way:
$conf['queue_class_NAME'] = 'Redis_Queue';
Where NAME is the arbitrary module given queue name, used as first parameter
for the method DrupalQueue::get().
THIS IS STILL VERY EXPERIMENTAL. The queue should work without any problems
except it does not implement the item lease time correctly, this means that
items that are too long to process won't be released back and forth but will
block the thread processing it instead. This is the only side effect I am
aware of at the current time.
Failover, sharding and partionning
==================================
Important notice
----------------
There are numerous support and feature request issues about client sharding,
failover ability, multi-server connection, ability to read from slave and
server clustering opened in the issue queue. Note that there is not one
universally efficient solution for this: most of the solutions require that
you cannot use the MULTI/EXEC command using more than one key, and that you
cannot use complex UNION and intersection features anymore.
This module does not implement any kind of client side key hashing or sharding
and never intended to; We recommend that you read the official Redis
documentation page about partionning.
The best solution for clustering and sharding today seems to be the proxy
assisted partionning using tools such as Twemproxy.
Current components state
------------------------
As of now, provided components are simple enough so they never use WATCH or
MULTI/EXEC transaction blocks on multiple keys : this means that you can use
them in an environment doing data sharding/partionning. This remains true
except when you use a proxy that blocks those commands such as Twemproxy.
Lock
----
Lock backend works on a single key per lock, it theorically guarantees the
atomicity of operations therefore is usable in a sharded environement. Sadly
if you use proxy assisted sharding such as Twemproxy, WATCH, MULTI and EXEC
commands won't pass making it non shardable.
Path
----
Path backend does not use on transactions, it is safe to use in a sharded
environment. Note that this backend uses a single HASH key per language
and per way (alias to source or source to alias) and therefore won't benefit
greatly if not at all from being sharded.
Cache
-----
Cache uses pipelined transactions but does not uses it to guarantee any kind
of data consistency. If you use a smart sharding proxy it is supposed to work
transparently without any hickups.
Queue
-----
Queue is still in development. There might be problems in the long term for
this component in sharded environments.
Testing
=======
Due to Drupal unit testing API being incredibly stupid, the unit tests can only
work with PHP >=5.3 while the module will work gracefully with PHP 5.2 (at least
using the PhpRedis client).
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.

View File

@ -0,0 +1,108 @@
<?php
abstract class Redis_AbstractBackend implements Redis_BackendInterface
{
/**
* Key components name separator
*/
const KEY_SEPARATOR = ':';
/**
* @var string
*/
private $prefix;
/**
* @var string
*/
private $namespace;
/**
* @var mixed
*/
private $client;
/**
* Default constructor
*
* @param mixed $client
* Redis client
* @param string $namespace
* Component namespace
* @param string $prefix
* Component prefix
*/
public function __construct($client, $namespace = null, $prefix = null)
{
$this->client = $client;
$this->prefix = $prefix;
if (null !== $namespace) {
$this->namespace = $namespace;
}
}
final public function setClient($client)
{
$this->client = $client;
}
final public function getClient()
{
return $this->client;
}
final public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
final public function getPrefix()
{
return $this->prefix;
}
final public function setNamespace($namespace)
{
$this->namespace = $namespace;
}
final public function getNamespace()
{
return $this->namespace;
}
/**
* Get prefixed key
*
* @param string|string[] $parts
* Arbitrary number of strings to compose the key
*
* @return string
*/
public function getKey($parts = array())
{
$key = array();
if (null !== $this->prefix) {
$key[] = $this->prefix;
}
if (null !== $this->namespace) {
$key[] = $this->namespace;
}
if ($parts) {
if (is_array($parts)) {
foreach ($parts as $part) {
if ($part) {
$key[] = $part;
}
}
} else {
$key[] = $parts;
}
}
return implode(self::KEY_SEPARATOR, array_filter($key));
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Client based Redis component
*/
interface Redis_BackendInterface
{
/**
* Set client
*
* @param mixed $client
*/
public function setClient($client);
/**
* Get client
*
* @return mixed
*/
public function getClient();
/**
* Set prefix
*
* @param string $prefix
*/
public function setPrefix($prefix);
/**
* Get prefix
*
* @return string
*/
public function getPrefix();
/**
* Set namespace
*
* @param string $namespace
*/
public function setNamespace($namespace);
/**
* Get namespace
*
* @return string
*/
public function getNamespace();
/**
* Get full key name using the set prefix
*
* @param string ...
* Any numer of strings to append to path using the separator
*
* @return string
*/
public function getKey();
}

View File

@ -0,0 +1,655 @@
<?php
/**
* Because those objects will be spawned during boostrap all its configuration
* must be set in the settings.php file.
*
* You will find the driver specific implementation in the Redis_Cache_*
* classes as they may differ in how the API handles transaction, pipelining
* and return values.
*/
class Redis_Cache
implements DrupalCacheInterface
{
/**
* Default lifetime for permanent items.
* Approximatively 1 year.
*/
const LIFETIME_PERM_DEFAULT = 31536000;
/**
* Uses EVAL scripts to flush data when called
*
* This remains the default behavior and is safe until you use a single
* Redis server instance and its version is >= 2.6 (older version don't
* support EVAL).
*/
const FLUSH_NORMAL = 0;
/**
* This mode is tailored for sharded Redis servers instances usage: it
* will never delete entries but only mark the latest flush timestamp
* into one of the servers in the shard. It will proceed to delete on
* read single entries when invalid entries are being loaded.
*/
const FLUSH_SHARD = 3;
/**
* Same as the one above, plus attempt to do pipelining when possible.
*
* This is supposed to work with sharding proxies that supports
* pipelining themselves, such as Twemproxy.
*/
const FLUSH_SHARD_WITH_PIPELINING = 4;
/**
* 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;
/**
* @var Redis_Cache_BackendInterface
*/
protected $backend;
/**
* @var string
*/
protected $bin;
/**
* When the global 'cache_lifetime' Drupal variable is set to a value, the
* cache backends should not expire temporary entries by themselves per
* Drupal signature. Volatile items will be dropped accordingly to their
* set lifetime.
*
* @var boolean
*/
protected $allowTemporaryFlush = true;
/**
* When in shard mode, the backend cannot proceed to multiple keys
* operations, and won't delete keys on flush calls.
*
* @var boolean
*/
protected $isSharded = false;
/**
* When in shard mode, the proxy may or may not support pipelining,
* Twemproxy is known to support it.
*
* @var boolean
*/
protected $allowPipeline = false;
/**
* Default TTL for CACHE_PERMANENT items.
*
* See "Default lifetime for permanent items" section of README.txt
* file for a comprehensive explaination of why this exists.
*
* @var int
*/
protected $permTtl = self::LIFETIME_PERM_DEFAULT;
/**
* Maximum TTL for this bin from Drupal configuration.
*
* @var int
*/
protected $maxTtl = 0;
/**
* Flush permanent and volatile cached values
*
* @var string[]
* First value is permanent latest flush time and second value
* is volatile latest flush time
*/
protected $flushCache = null;
/**
* Is this bin in shard mode
*
* @return boolean
*/
public function isSharded()
{
return $this->isSharded;
}
/**
* Does this bin allow pipelining through sharded environment
*
* @return boolean
*/
public function allowPipeline()
{
return $this->allowPipeline;
}
/**
* Does this bin allow temporary item flush
*
* @return boolean
*/
public function allowTemporaryFlush()
{
return $this->allowTemporaryFlush;
}
/**
* Get TTL for CACHE_PERMANENT items.
*
* @return int
* Lifetime in seconds.
*/
public function getPermTtl()
{
return $this->permTtl;
}
/**
* Get maximum TTL for all items.
*
* @return int
* Lifetime in seconds.
*/
public function getMaxTtl()
{
return $this->maxTtl;
}
/**
* {@inheritdoc}
*/
public function __construct($bin)
{
$this->bin = $bin;
$className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_CACHE);
$this->backend = new $className(Redis_Client::getClient(), $bin, Redis_Client::getDefaultPrefix($bin));
$this->refreshCapabilities();
$this->refreshPermTtl();
$this->refreshMaxTtl();
}
/**
* Find from Drupal variables the clear mode.
*/
public function refreshCapabilities()
{
if (0 < variable_get('cache_lifetime', 0)) {
// Per Drupal default behavior, when the 'cache_lifetime' variable
// is set we must not flush any temporary items since they have a
// life time.
$this->allowTemporaryFlush = false;
}
if (null !== ($mode = variable_get('redis_flush_mode', null))) {
$mode = (int)$mode;
} else {
$mode = self::FLUSH_NORMAL;
}
$this->isSharded = self::FLUSH_SHARD === $mode || self::FLUSH_SHARD_WITH_PIPELINING === $mode;
$this->allowPipeline = self::FLUSH_SHARD !== $mode;
}
/**
* Find from Drupal variables the right permanent items TTL.
*/
protected function refreshPermTtl()
{
$ttl = null;
if (null === ($ttl = variable_get('redis_perm_ttl_' . $this->bin, null))) {
if (null === ($ttl = variable_get('redis_perm_ttl', null))) {
$ttl = self::LIFETIME_PERM_DEFAULT;
}
}
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->d * 86400 + $iv->h * 3600 + $iv->i * 60 + $iv->s);
} else {
// Sorry but we have to log this somehow.
trigger_error(sprintf("Parsed TTL '%s' has an invalid value: switching to default", $ttl));
$this->permTtl = self::LIFETIME_PERM_DEFAULT;
}
}
}
/**
* Find from Drupal variables the maximum cache lifetime.
*/
public function refreshMaxTtl()
{
// And now cache lifetime. Be aware we exclude negative values
// considering those are Drupal misconfiguration.
$maxTtl = variable_get('cache_lifetime', 0);
if (0 < $maxTtl) {
if ($maxTtl < $this->permTtl) {
$this->maxTtl = $maxTtl;
} else {
$this->maxTtl = $this->permTtl;
}
} else if ($this->permTtl) {
$this->maxTtl = $this->permTtl;
}
}
/**
* Set last flush time
*
* @param string $permanent
* @param string $volatile
*/
public function setLastFlushTime($permanent = false, $volatile = false)
{
// Here we need to fetch absolute values from backend, to avoid
// concurrency problems and ensure data validity.
list($flushPerm, $flushVolatile) = $this->backend->getLastFlushTime();
$checksum = $this->getValidChecksum(
max(array(
$flushPerm,
$flushVolatile,
$permanent,
time(),
))
);
if ($permanent) {
$this->backend->setLastFlushTimeFor($checksum, false);
$this->backend->setLastFlushTimeFor($checksum, true);
$this->flushCache = array($checksum, $checksum);
} else if ($volatile) {
$this->backend->setLastFlushTimeFor($checksum, true);
$this->flushCache = array($flushPerm, $checksum);
}
}
/**
* Get latest flush time
*
* @return string[]
* First value is the latest flush time for permanent entries checksum,
* second value is the latest flush time for volatile entries checksum.
*/
public function getLastFlushTime()
{
if (!$this->flushCache) {
$this->flushCache = $this->backend->getLastFlushTime();
}
// At the very first hit, we might not have the timestamps set, thus
// we need to create them to avoid our entry being considered as
// invalid
if (!$this->flushCache[0]) {
$this->setLastFlushTime(true, true);
} else if (!$this->flushCache[1]) {
$this->setLastFlushTime(false, true);
}
return $this->flushCache;
}
/**
* Create cache entry
*
* @param string $cid
* @param mixed $data
*
* @return array
*/
protected function createEntryHash($cid, $data, $expire = CACHE_PERMANENT)
{
list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
if (CACHE_TEMPORARY === $expire) {
$validityThreshold = max(array($flushVolatile, $flushPerm));
} else {
$validityThreshold = $flushPerm;
}
$time = $this->getValidChecksum($validityThreshold);
$hash = array(
'cid' => $cid,
'created' => $time,
'expire' => $expire,
);
// Let Redis handle the data types itself.
if (!is_string($data)) {
$hash['data'] = serialize($data);
$hash['serialized'] = 1;
} else {
$hash['data'] = $data;
$hash['serialized'] = 0;
}
return $hash;
}
/**
* Expand cache entry from fetched data
*
* @param array $values
* Raw values fetched from Redis server data
*
* @return array
* Or FALSE if entry is invalid
*/
protected function expandEntry(array $values, $flushPerm, $flushVolatile)
{
// Check for entry being valid.
if (empty($values['cid'])) {
return;
}
// This ensures backward compatibility with older version of
// this module's data still stored in Redis.
if (isset($values['expire'])) {
$expire = (int)$values['expire'];
// Ensure the entry is valid and have not expired.
if ($expire !== CACHE_PERMANENT && $expire !== CACHE_TEMPORARY && $expire <= time()) {
return false;
}
}
// Ensure the entry does not predate the last flush time.
if ($this->allowTemporaryFlush && !empty($values['volatile'])) {
$validityThreshold = max(array($flushPerm, $flushVolatile));
} else {
$validityThreshold = $flushPerm;
}
if ($values['created'] <= $validityThreshold) {
return false;
}
$entry = (object)$values;
// Reduce the checksum to the real timestamp part
$entry->created = (int)$entry->created;
if ($entry->serialized) {
$entry->data = unserialize($entry->data);
}
return $entry;
}
/**
* {@inheritdoc}
*/
public function get($cid)
{
$values = $this->backend->get($cid);
if (empty($values)) {
return false;
}
list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
$entry = $this->expandEntry($values, $flushPerm, $flushVolatile);
if (!$entry) { // This entry exists but is invalid.
$this->backend->delete($cid);
return false;
}
return $entry;
}
/**
* {@inheritdoc}
*/
public function getMultiple(&$cids)
{
$ret = array();
$delete = array();
if (!$this->allowPipeline) {
$entries = array();
foreach ($cids as $cid) {
if ($entry = $this->backend->get($cid)) {
$entries[$cid] = $entry;
}
}
} else {
$entries = $this->backend->getMultiple($cids);
}
list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
foreach ($cids as $key => $cid) {
if (!empty($entries[$cid])) {
$entry = $this->expandEntry($entries[$cid], $flushPerm, $flushVolatile);
} else {
$entry = null;
}
if (empty($entry)) {
$delete[] = $cid;
} else {
$ret[$cid] = $entry;
unset($cids[$key]);
}
}
if (!empty($delete)) {
if ($this->allowPipeline) {
foreach ($delete as $id) {
$this->backend->delete($id);
}
} else {
$this->backend->deleteMultiple($delete);
}
}
return $ret;
}
/**
* {@inheritdoc}
*/
public function set($cid, $data, $expire = CACHE_PERMANENT)
{
$hash = $this->createEntryHash($cid, $data, $expire);
$maxTtl = $this->getMaxTtl();
switch ($expire) {
case CACHE_PERMANENT:
$this->backend->set($cid, $hash, $maxTtl, false);
break;
case CACHE_TEMPORARY:
$this->backend->set($cid, $hash, $maxTtl, true);
break;
default:
$ttl = $expire - time();
// Ensure $expire consistency
if ($ttl <= 0) {
// Entry has already expired, but we may have a stalled
// older cache entry remaining there, ensure it wont
// happen by doing a preventive delete
$this->backend->delete($cid);
} else {
if ($maxTtl && $maxTtl < $ttl) {
$ttl = $maxTtl;
}
$this->backend->set($cid, $hash, $ttl, false);
}
break;
}
}
/**
* {@inheritdoc}
*/
public function clear($cid = null, $wildcard = false)
{
if (null === $cid && !$wildcard) {
// Drupal asked for volatile entries flush, this will happen
// during cron run, mostly
$this->setLastFlushTime(false, true);
if (!$this->isSharded && $this->allowTemporaryFlush) {
$this->backend->flushVolatile();
}
} else if ($wildcard) {
if (empty($cid)) {
// This seems to be an error, just do nothing.
return;
}
if ('*' === $cid) {
// Use max() to ensure we invalidate both correctly
$this->setLastFlushTime(true);
if (!$this->isSharded) {
$this->backend->flush();
}
} else {
if (!$this->isSharded) {
$this->backend->deleteByPrefix($cid);
} else {
// @todo This needs a map algorithm the same way memcache
// module implemented it for invalidity by prefixes. This
// is a very stupid fallback
$this->setLastFlushTime(true);
}
}
} else if (is_array($cid)) {
$this->backend->deleteMultiple($cid);
} else {
$this->backend->delete($cid);
}
}
public function isEmpty()
{
return false;
}
/**
* From the given timestamp build an incremental safe time-based identifier.
*
* Due to potential accidental cache wipes, when a server goes down in the
* cluster or when a server triggers its LRU algorithm wipe-out, keys that
* matches flush or tags checksum might be dropped.
*
* Per default, each new inserted tag will trigger a checksum computation to
* be stored in the Redis server as a timestamp. In order to ensure a checksum
* validity a simple comparison between the tag checksum and the cache entry
* checksum will tell us if the entry pre-dates the current checksum or not,
* thus telling us its state. The main problem we experience is that Redis
* is being so fast it is able to create and drop entries at same second,
* sometime even the same micro second. The only safe way to avoid conflicts
* is to checksum using an arbitrary computed number (a sequence).
*
* Drupal core does exactly this thus tags checksums are additions of each tag
* individual checksum; each tag checksum is a independent arbitrary serial
* that gets incremented starting with 0 (no invalidation done yet) to n (n
* invalidations) which grows over time. This way the checksum computation
* always rises and we have a sensible default that works in all cases.
*
* This model works as long as you can ensure consistency for the serial
* storage over time. Nevertheless, as explained upper, in our case this
* serial might be dropped at some point for various valid technical reasons:
* if we start over to 0, we may accidentally compute a checksum which already
* existed in the past and make invalid entries turn back to valid again.
*
* In order to prevent this behavior, using a timestamp as part of the serial
* ensures that we won't experience this problem in a time range wider than a
* single second, which is safe enough for us. But using timestamp creates a
* new problem: Redis is so fast that we can set or delete hundreds of entries
* easily during the same second: an entry created then invalidated the same
* second will create false positives (entry is being considered as valid) -
* note that depending on the check algorithm, false negative may also happen
* the same way. Therefore we need to have an abitrary serial value to be
* incremented in order to enforce our checks to be more strict.
*
* The solution to both the first (the need for a time based checksum in case
* of checksum data being dropped) and the second (the need to have an
* arbitrary predictible serial value to avoid false positives or negatives)
* we are combining the two: every checksum will be built this way:
*
* UNIXTIMESTAMP.SERIAL
*
* For example:
*
* 1429789217.017
*
* will reprensent the 17th invalidation of the 1429789217 exact second which
* happened while writing this documentation. The next tag being invalidated
* the same second will then have this checksum:
*
* 1429789217.018
*
* And so on...
*
* In order to make it consitent with PHP string and float comparison we need
* to set fixed precision over the decimal, and store as a string to avoid
* possible float precision problems when comparing.
*
* This algorithm is not fully failsafe, but allows us to proceed to 1000
* operations on the same checksum during the same second, which is a
* sufficiently great value to reduce the conflict probability to almost
* zero for most uses cases.
*
* @param int|string $timestamp
* "TIMESTAMP[.INCREMENT]" string
*
* @return string
* The next "TIMESTAMP.INCREMENT" string.
*/
public function getNextIncrement($timestamp = null)
{
if (!$timestamp) {
return time() . '.000';
}
if (false !== ($pos = strpos($timestamp, '.'))) {
$inc = substr($timestamp, $pos + 1, 3);
return ((int)$timestamp) . '.' . str_pad($inc + 1, 3, '0', STR_PAD_LEFT);
}
return $timestamp . '.000';
}
/**
* Get valid checksum
*
* @param int|string $previous
* "TIMESTAMP[.INCREMENT]" string
*
* @return string
* The next "TIMESTAMP.INCREMENT" string.
*
* @see Redis_Cache::getNextIncrement()
*/
public function getValidChecksum($previous = null)
{
if (time() === (int)$previous) {
return $this->getNextIncrement($previous);
} else {
return $this->getNextIncrement();
}
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* Real cache backend primitives. This functions will be used by the
* Redis_Cache wrapper class that implements the high-level logic that
* allows us to be Drupal compatible.
*/
interface Redis_Cache_BackendInterface extends Redis_BackendInterface
{
/**
* Defaut constructor
*
* @param string $namespace
*/
public function __construct($client, $namespace);
/**
* Get namespace
*
* @return string
*/
public function getNamespace();
/**
* Set last flush time
*
* @param int $time
* @param boolean $volatile
*/
public function setLastFlushTimeFor($time, $volatile = false);
/**
* Get last flush time
*
* @return int[]
* First value is for non-volatile items, second value is for volatile items.
*/
public function getLastFlushTime();
/**
* Get a single entry
*
* @param string $id
*
* @return stdClass
* Cache entry or false if the entry does not exists.
*/
public function get($id);
/**
* Get multiple entries
*
* @param string[] $idList
*
* @return stdClass[]
* Existing cache entries keyed by id,
*/
public function getMultiple(array $idList);
/**
* Set a single entry
*
* @param string $id
* @param mixed $data
* @param int $ttl
* @param boolean $volatile
*/
public function set($id, $data, $ttl = null, $volatile = false);
/**
* Delete a single entry
*
* @param string $cid
*/
public function delete($id);
/**
* Delete multiple entries
*
* This method should not use a single DEL command but use a pipeline instead
*
* @param array $idList
*/
public function deleteMultiple(array $idList);
/**
* Delete entries by prefix
*
* @param string $prefix
*/
public function deleteByPrefix($prefix);
/**
* Flush all entries
*/
public function flush();
/**
* Flush all entries marked as temporary
*/
public function flushVolatile();
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @todo
* - Improve lua scripts by using SCAN family commands
* - Deambiguate why we need the namespace only for flush*() operations
* - Implement the isEmpty() method by using SCAN or KEYS
*/
abstract class Redis_Cache_Base extends Redis_AbstractBackend
{
/**
* Lastest cache flush KEY name
*/
const LAST_FLUSH_KEY = '_last_flush';
/**
* Delete by prefix lua script
*/
const EVAL_DELETE_PREFIX = <<<EOT
local keys = redis.call("KEYS", ARGV[1])
for i, k in ipairs(keys) do
redis.call("DEL", k)
end
return 1
EOT;
/**
* Delete volatile by prefix lua script
*/
const EVAL_DELETE_VOLATILE = <<<EOT
local keys = redis.call('KEYS', ARGV[1])
for i, k in ipairs(keys) do
if "1" == redis.call("HGET", k, "volatile") then
redis.call("DEL", k)
end
end
return 1
EOT;
}

View File

@ -0,0 +1,149 @@
<?php
/**
* Predis cache backend.
*/
class Redis_Cache_PhpRedis extends Redis_Cache_Base
{
public function setLastFlushTimeFor($time, $volatile = false)
{
$client = $this->getClient();
$key = $this->getKey(self::LAST_FLUSH_KEY);
if ($volatile) {
$client->hset($key, 'volatile', $time);
} else {
$client->hmset($key, array(
'permanent' => $time,
'volatile' => $time,
));
}
}
public function getLastFlushTime()
{
$client = $this->getClient();
$key = $this->getKey(self::LAST_FLUSH_KEY);
$values = $client->hmget($key, array("permanent", "volatile"));
if (empty($values) || !is_array($values)) {
$ret = array(0, 0);
} else {
if (empty($values['permanent'])) {
$values['permanent'] = 0;
}
if (empty($values['volatile'])) {
$values['volatile'] = 0;
}
$ret = array($values['permanent'], $values['volatile']);
}
return $ret;
}
public function get($id)
{
$client = $this->getClient();
$key = $this->getKey($id);
$values = $client->hgetall($key);
// Recent versions of PhpRedis will return the Redis instance
// instead of an empty array when the HGETALL target key does
// not exists. I see what you did there.
if (empty($values) || !is_array($values)) {
return false;
}
return $values;
}
public function getMultiple(array $idList)
{
$client = $this->getClient();
$ret = array();
$pipe = $client->multi(Redis::PIPELINE);
foreach ($idList as $id) {
$pipe->hgetall($this->getKey($id));
}
$replies = $pipe->exec();
foreach (array_values($idList) as $line => $id) {
if (!empty($replies[$line]) && is_array($replies[$line])) {
$ret[$id] = $replies[$line];
}
}
return $ret;
}
public function set($id, $data, $ttl = null, $volatile = false)
{
// Ensure TTL consistency: if the caller gives us an expiry timestamp
// in the past the key will expire now and will never be read.
// Behavior between Predis and PhpRedis seems to change here: when
// setting a negative expire time, PhpRedis seems to ignore the
// command and leave the key permanent.
if (null !== $ttl && $ttl <= 0) {
return;
}
$data['volatile'] = (int)$volatile;
$client = $this->getClient();
$key = $this->getKey($id);
$pipe = $client->multi(Redis::PIPELINE);
$pipe->hmset($key, $data);
if (null !== $ttl) {
$pipe->expire($key, $ttl);
}
$pipe->exec();
}
public function delete($id)
{
$this->getClient()->del($this->getKey($id));
}
public function deleteMultiple(array $idList)
{
$client = $this->getClient();
$pipe = $client->multi(Redis::PIPELINE);
foreach ($idList as $id) {
$pipe->del($this->getKey($id));
}
// Don't care if something failed.
$pipe->exec();
}
public function deleteByPrefix($prefix)
{
$client = $this->getClient();
$ret = $client->eval(self::EVAL_DELETE_PREFIX, array($this->getKey($prefix . '*')));
if (1 != $ret) {
trigger_error(sprintf("EVAL failed: %s", $client->getLastError()), E_USER_ERROR);
}
}
public function flush()
{
$client = $this->getClient();
$ret = $client->eval(self::EVAL_DELETE_PREFIX, array($this->getKey('*')));
if (1 != $ret) {
trigger_error(sprintf("EVAL failed: %s", $client->getLastError()), E_USER_ERROR);
}
}
public function flushVolatile()
{
$client = $this->getClient();
$ret = $client->eval(self::EVAL_DELETE_VOLATILE, array($this->getKey('*')));
if (1 != $ret) {
trigger_error(sprintf("EVAL failed: %s", $client->getLastError()), E_USER_ERROR);
}
}
}

View File

@ -0,0 +1,145 @@
<?php
/**
* Predis cache backend.
*/
class Redis_Cache_Predis extends Redis_Cache_Base
{
public function setLastFlushTimeFor($time, $volatile = false)
{
$client = $this->getClient();
$key = $this->getKey(self::LAST_FLUSH_KEY);
if ($volatile) {
$client->hset($key, 'volatile', $time);
} else {
$client->hmset($key, array(
'permanent' => $time,
'volatile' => $time,
));
}
}
public function getLastFlushTime()
{
$client = $this->getClient();
$key = $this->getKey(self::LAST_FLUSH_KEY);
$values = $client->hmget($key, array("permanent", "volatile"));
if (empty($values) || !is_array($values)) {
$values = array(0, 0);
} else {
if (empty($values[0])) {
$values[0] = 0;
}
if (empty($values[1])) {
$values[1] = 0;
}
}
return $values;
}
public function get($id)
{
$client = $this->getClient();
$key = $this->getKey($id);
$values = $client->hgetall($key);
// Recent versions of PhpRedis will return the Redis instance
// instead of an empty array when the HGETALL target key does
// not exists. I see what you did there.
if (empty($values) || !is_array($values)) {
return false;
}
return $values;
}
public function getMultiple(array $idList)
{
$ret = array();
$pipe = $this->getClient()->pipeline();
foreach ($idList as $id) {
$pipe->hgetall($this->getKey($id));
}
$replies = $pipe->execute();
foreach (array_values($idList) as $line => $id) {
// HGETALL signature seems to differ depending on Predis versions.
// This was found just after Predis update. Even though I'm not sure
// this comes from Predis or just because we're misusing it.
if (!empty($replies[$line]) && is_array($replies[$line])) {
$ret[$id] = $replies[$line];
}
}
return $ret;
}
public function set($id, $data, $ttl = null, $volatile = false)
{
// Ensure TTL consistency: if the caller gives us an expiry timestamp
// in the past the key will expire now and will never be read.
// Behavior between Predis and PhpRedis seems to change here: when
// setting a negative expire time, PhpRedis seems to ignore the
// command and leave the key permanent.
if (null !== $ttl && $ttl <= 0) {
return;
}
$key = $this->getKey($id);
$data['volatile'] = (int)$volatile;
$pipe = $this->getClient()->pipeline();
$pipe->hmset($key, $data);
if (null !== $ttl) {
$pipe->expire($key, $ttl);
}
$pipe->execute();
}
public function delete($id)
{
$client = $this->getClient();
$client->del($this->getKey($id));
}
public function deleteMultiple(array $idList)
{
$pipe = $this->getClient()->pipeline();
foreach ($idList as $id) {
$pipe->del($this->getKey($id));
}
$pipe->execute();
}
public function deleteByPrefix($prefix)
{
$client = $this->getClient();
$ret = $client->eval(self::EVAL_DELETE_PREFIX, 0, $this->getKey($prefix . '*'));
if (1 != $ret) {
trigger_error(sprintf("EVAL failed: %s", $client->getLastError()), E_USER_ERROR);
}
}
public function flush()
{
$client = $this->getClient();
$ret = $client->eval(self::EVAL_DELETE_PREFIX, 0, $this->getKey('*'));
if (1 != $ret) {
trigger_error(sprintf("EVAL failed: %s", $client->getLastError()), E_USER_ERROR);
}
}
public function flushVolatile()
{
$client = $this->getClient();
$ret = $client->eval(self::EVAL_DELETE_VOLATILE, 0, $this->getKey('*'));
if (1 != $ret) {
trigger_error(sprintf("EVAL failed: %s", $client->getLastError()), E_USER_ERROR);
}
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* This typically brings 80..85% compression in ~20ms/mb write, 5ms/mb read.
*/
class Redis_CacheCompressed extends Redis_Cache implements DrupalCacheInterface
{
private $compressionSizeThreshold = 100;
private $compressionRatio = 1;
/**
* {@inheritdoc}
*/
public function __construct($bin)
{
parent::__construct($bin);
$this->compressionSizeThreshold = (int)variable_get('cache_compression_size_threshold', 100);
if ($this->compressionSizeThreshold < 0) {
trigger_error('cache_compression_size_threshold must be 0 or a positive integer, negative value found, switching back to default 100', E_USER_WARNING);
$this->compressionSizeThreshold = 100;
}
// Minimum compression level (1) has good ratio in low time.
$this->compressionRatio = (int)variable_get('cache_compression_ratio', 1);
if ($this->compressionRatio < 1 || 9 < $this->compressionRatio) {
trigger_error('cache_compression_ratio must be between 1 and 9, out of bounds value found, switching back to default 1', E_USER_WARNING);
$this->compressionRatio = 1;
}
}
/**
* {@inheritdoc}
*/
protected function createEntryHash($cid, $data, $expire = CACHE_PERMANENT)
{
$hash = parent::createEntryHash($cid, $data, $expire);
// Empiric level when compression makes sense.
if (!$this->compressionSizeThreshold || strlen($hash['data']) > $this->compressionSizeThreshold) {
$hash['data'] = gzcompress($hash['data'], $this->compressionRatio);
$hash['compressed'] = true;
}
return $hash;
}
/**
* {@inheritdoc}
*/
protected function expandEntry(array $values, $flushPerm, $flushVolatile)
{
if (!empty($values['data']) && !empty($values['compressed'])) {
// Uncompress, suppress warnings e.g. for broken CRC32.
$values['data'] = @gzuncompress($values['data']);
// In such cases, void the cache entry.
if ($values['data'] === false) {
return false;
}
}
return parent::expandEntry($values, $flushPerm, $flushVolatile);
}
}

View File

@ -0,0 +1,241 @@
<?php
// It may happen we get here with no autoloader set during the Drupal core
// early bootstrap phase, at cache backend init time.
if (!interface_exists('Redis_Client_FactoryInterface')) {
require_once dirname(__FILE__) . '/Client/FactoryInterface.php';
require_once dirname(__FILE__) . '/Client/Manager.php';
}
/**
* This static class only reason to exist is to tie Drupal global
* configuration to OOP driven code of this module: it will handle
* everything that must be read from global configuration and let
* other components live without any existence of it
*/
class Redis_Client
{
/**
* Cache implementation namespace.
*/
const REDIS_IMPL_CACHE = 'Redis_Cache_';
/**
* Lock implementation namespace.
*/
const REDIS_IMPL_LOCK = 'Redis_Lock_';
/**
* Cache implementation namespace.
*/
const REDIS_IMPL_QUEUE = 'Redis_Queue_';
/**
* Path implementation namespace.
*/
const REDIS_IMPL_PATH = 'Redis_Path_';
/**
* Client factory implementation namespace.
*/
const REDIS_IMPL_CLIENT = 'Redis_Client_';
/**
* @var Redis_Client_Manager
*/
private static $manager;
/**
* @var string
*/
static protected $globalPrefix;
/**
* Get site default global prefix
*
* @return string
*/
static public function getGlobalPrefix()
{
// Provide a fallback for multisite. This is on purpose not inside the
// getPrefixForBin() function in order to decouple the unified prefix
// variable logic and custom module related security logic, that is not
// necessary for all backends. We can't just use HTTP_HOST, as multiple
// hosts might be using the same database. Or, more commonly, a site
// might not be a multisite at all, but might be using Drush leading to
// a separate HTTP_HOST of 'default'. Likewise, we can't rely on
// conf_path(), as settings.php might be modifying what database to
// connect to. To mirror what core does with database caching we use
// the DB credentials to inform our cache key.
if (null === self::$globalPrefix) {
if (isset($GLOBALS['db_url']) && is_string($GLOBALS['db_url'])) {
// Drupal 6 specifics when using the cache_backport module, we
// therefore cannot use \Database class to determine database
// settings.
self::$globalPrefix = md5($GLOBALS['db_url']);
} else {
require_once DRUPAL_ROOT . '/includes/database/database.inc';
$dbInfo = Database::getConnectionInfo();
$active = $dbInfo['default'];
self::$globalPrefix = md5($active['host'] . $active['database'] . $active['prefix']['default']);
}
}
return self::$globalPrefix;
}
/**
* Get global default prefix
*
* @param string $namespace
*
* @return string
*/
static public function getDefaultPrefix($namespace = null)
{
$ret = null;
if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) {
$ret = $GLOBALS['drupal_test_info']['test_run_id'];
} else {
$prefixes = variable_get('cache_prefix', null);
if (is_string($prefixes)) {
// Variable can be a string which then considered as a default
// behavior.
$ret = $prefixes;
} else if (null !== $namespace && isset($prefixes[$namespace])) {
if (false !== $prefixes[$namespace]) {
// If entry is set and not false an explicit prefix is set
// for the bin.
$ret = $prefixes[$namespace];
} 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)) {
$ret = Redis_Client::getGlobalPrefix();
}
return $ret;
}
/**
* Get client manager
*
* @return Redis_Client_Manager
*/
static public function getManager()
{
global $conf;
if (null === self::$manager) {
$className = self::getClass(self::REDIS_IMPL_CLIENT);
$factory = new $className();
// Build server list from conf
$serverList = array();
if (isset($conf['redis_servers'])) {
$serverList = $conf['redis_servers'];
}
if (empty($serverList) || !isset($serverList['default'])) {
// Backward configuration compatibility with older versions
$serverList[Redis_Client_Manager::REALM_DEFAULT] = array();
foreach (array('host', 'port', 'base', 'password', 'socket') as $key) {
if (isset($conf['redis_client_' . $key])) {
$serverList[Redis_Client_Manager::REALM_DEFAULT][$key] = $conf['redis_client_' . $key];
}
}
}
self::$manager = new Redis_Client_Manager($factory, $serverList);
}
return self::$manager;
}
/**
* Find client class name
*
* @return string
*/
static public function getClientInterfaceName()
{
global $conf;
if (!empty($conf['redis_client_interface'])) {
return $conf['redis_client_interface'];
} else if (class_exists('Predis\Client')) {
// Transparent and abitrary preference for Predis library.
return $conf['redis_client_interface'] = 'Predis';
} else if (class_exists('Redis')) {
// Fallback on PhpRedis if available.
return $conf['redis_client_interface'] = 'PhpRedis';
} else {
throw new Exception("No client interface set.");
}
}
/**
* For unit test use only
*/
static public function reset(Redis_Client_Manager $manager = null)
{
self::$manager = $manager;
}
/**
* Get the client for the 'default' realm
*
* @return mixed
*
* @deprecated
*/
public static function getClient()
{
return self::getManager()->getClient();
}
/**
* Get specific class implementing the current client usage for the specific
* asked core subsystem.
*
* @param string $system
* One of the Redis_Client::IMPL_* constant.
* @param string $clientName
* Client name, if fixed.
*
* @return string
* Class name, if found.
*
* @deprecated
*/
static public function getClass($system)
{
$class = $system . self::getClientInterfaceName();
if (!class_exists($class)) {
throw new Exception(sprintf("Class '%s' does not exist", $class));
}
return $class;
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* Client proxy, client handling class tied to the bare mininum.
*/
interface Redis_Client_FactoryInterface {
/**
* Get the connected client instance.
*
* @param array $options
* Options from the server pool configuration that may contain:
* - host
* - port
* - database
* - password
* - socket
*
* @return mixed
* Real client depends from the library behind.
*/
public function getClient($options = array());
/**
* 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();
}

View File

@ -0,0 +1,144 @@
<?php
/**
* Client pool manager for multi-server configurations
*/
class Redis_Client_Manager
{
/**
* Redis default host
*/
const REDIS_DEFAULT_HOST = '127.0.0.1';
/**
* Redis default port
*/
const REDIS_DEFAULT_PORT = 6379;
/**
* Redis default socket (will override host and port)
*/
const REDIS_DEFAULT_SOCKET = null;
/**
* Redis default database: will select none (Database 0)
*/
const REDIS_DEFAULT_BASE = null;
/**
* Redis default password: will not authenticate
*/
const REDIS_DEFAULT_PASSWORD = null;
/**
* Default realm
*/
const REALM_DEFAULT = 'default';
/**
* Client interface name (PhpRedis or Predis)
*
* @var string
*/
private $interfaceName;
/**
* @var array[]
*/
private $serverList = array();
/**
* @var mixed[]
*/
private $clients = array();
/**
* @var Redis_Client_FactoryInterface
*/
private $factory;
/**
* Default constructor
*
* @param Redis_Client_FactoryInterface $factory
* Client factory
* @param array $serverList
* Server connection info list
*/
public function __construct(Redis_Client_FactoryInterface $factory, $serverList = array())
{
$this->factory = $factory;
$this->serverList = $serverList;
}
/**
* Get client for the given realm
*
* @param string $realm
* @param boolean $allowDefault
*
* @return mixed
*/
public function getClient($realm = self::REALM_DEFAULT, $allowDefault = true)
{
if (!isset($this->clients[$realm])) {
$client = $this->createClient($realm);
if (false === $client) {
if (self::REALM_DEFAULT !== $realm && $allowDefault) {
$this->clients[$realm] = $this->getClient(self::REALM_DEFAULT);
} else {
throw new InvalidArgumentException(sprintf("Could not find client for realm '%s'", $realm));
}
} else {
$this->clients[$realm] = $client;
}
}
return $this->clients[$realm];
}
/**
* Build connection parameters array from current Drupal settings
*
* @param string $realm
*
* @return boolean|string[]
* A key-value pairs of configuration values or false if realm is
* not defined per-configuration
*/
private function buildOptions($realm)
{
$info = null;
if (isset($this->serverList[$realm])) {
$info = $this->serverList[$realm];
} else {
return false;
}
$info += array(
'host' => self::REDIS_DEFAULT_HOST,
'port' => self::REDIS_DEFAULT_PORT,
'base' => self::REDIS_DEFAULT_BASE,
'password' => self::REDIS_DEFAULT_PASSWORD,
'socket' => self::REDIS_DEFAULT_SOCKET
);
return array_filter($info);
}
/**
* Get client singleton
*/
private function createClient($realm)
{
$info = $this->buildOptions($realm);
if (false === $info) {
return false;
}
return $this->factory->getClient($info);
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* PhpRedis client specific implementation.
*/
class Redis_Client_PhpRedis implements Redis_Client_FactoryInterface {
public function getClient($options = array()) {
$client = new Redis;
if (!empty($options['socket'])) {
$client->connect($options['socket']);
} else {
$client->connect($options['host'], $options['port']);
}
if (isset($options['password'])) {
$client->auth($options['password']);
}
if (isset($options['base'])) {
$client->select($options['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;
}
public function getName() {
return 'PhpRedis';
}
}

View File

@ -0,0 +1,145 @@
<?php
/**
* Predis client specific implementation.
*/
class Redis_Client_Predis implements Redis_Client_FactoryInterface {
/**
* Circular depedency breaker.
*/
static protected $autoloaderRegistered = false;
/**
* If the first cache get operation happens after the core autoloader has
* been registered to PHP, during our autoloader registration we will
* trigger it when calling class_exists(): core autoloader will then run
* cache_get() during autoloading but sadly this will run our autoloader
* registration once again. The second time we are called the circular
* dependency breaker will act and we will do nothing, ending up in a
* class instanciation attempt while the autoloader is still not loaded.
*/
static protected $stupidCoreWorkaround = 0;
/**
* Define Predis base path if not already set, and if we need to set the
* autoloader by ourself. This will ensure no crash. Best way would have
* been that Drupal ships a PSR-0 autoloader, in which we could manually
* add our library path.
*
* We cannot do that in the file header, PHP class_exists() function wont
* see classes being loaded during the autoloading because this file is
* loaded by another autoloader: attempting the class_exists() during a
* pending autoloading would cause PHP to crash and ignore the rest of the
* file silentely (WTF!?). By delaying this at the getClient() call we
* ensure we are not in the class loading process anymore.
*/
public static function setPredisAutoload() {
if (self::$autoloaderRegistered) {
return;
}
self::$stupidCoreWorkaround++;
// If you attempt to set Drupal's bin cache_bootstrap using Redis, you
// will experience an infinite loop (breaking by itself the second time
// it passes by): the following call will wake up autoloaders (and we
// want that to work since user may have set its own autoloader) but
// will wake up Drupal's one too, and because Drupal core caches its
// file map, this will trigger this method to be called a second time
// and boom! Adios bye bye. That's why this will be called early in the
// 'redis.autoload.inc' file instead.
if (1 < self::$stupidCoreWorkaround || !class_exists('Predis\Client')) {
if (!defined('PREDIS_BASE_PATH')) {
$search = DRUPAL_ROOT . '/sites/all/libraries/predis';
define('PREDIS_BASE_PATH', $search);
} else {
$search = PREDIS_BASE_PATH;
}
if (is_dir($search . '/src')) { // Predis v1.x
define('PREDIS_VERSION_MAJOR', 1);
} else if (is_dir($search . '/lib')) { // Predis v0.x
define('PREDIS_VERSION_MAJOR', 0);
} else {
throw new Exception("PREDIS_BASE_PATH constant must be set, Predis library must live in sites/all/libraries/predis.");
}
// Register a simple autoloader for Predis library. Since the Predis
// library is PHP 5.3 only, we can afford doing closures safely.
switch (PREDIS_VERSION_MAJOR) {
case 0:
$autoload = function($classname) { // PSR-0 autoloader.
if (0 === strpos($classname, 'Predis\\')) {
$filename = PREDIS_BASE_PATH . '/lib/' . str_replace('\\', '/', $classname) . '.php';
return (bool)require_once $filename;
}
return false;
};
break;
case 1:
// Register a simple autoloader for Predis library. Since the Predis
// library is PHP 5.3 only, we can afford doing closures safely.
$autoload = function($classname) { // PSR-4 autoloader
if (0 === strpos($classname, 'Predis\\')) {
$filename = PREDIS_BASE_PATH . '/src/' . str_replace('\\', '/', substr($classname, 7)) . '.php';
return (bool)require_once $filename;
}
return false;
};
break;
}
if ($autoload) {
spl_autoload_register($autoload);
}
// Same reason why we have the stupid core workaround, if this happens
// during a second autoload call, PHP won't call the newly registered
// autoloader function, so just load the file.
if (1 < self::$stupidCoreWorkaround) {
call_user_func($autoload, 'Predis\Client');
}
}
self::$autoloaderRegistered = true;
}
public function getClient($options = array()) {
self::setPredisAutoload();
if (!empty($options['socket'])) {
$options['scheme'] = 'unix';
$options['path'] = $options['socket'];
}
foreach ($options as $key => $value) {
if (!isset($value)) {
unset($options[$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());
$client = new \Predis\Client($options);
if (isset($options['base']) && 0 !== $options['base']) {
$client->select((int)$options['base']);
}
return $client;
}
public function getName() {
return 'Predis';
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* Lock backend singleton handling.
*/
class Redis_Lock {
/**
* @var Redis_Lock_BackendInterface
*/
private static $instance;
/**
* Get actual lock backend.
*
* @return Redis_Lock_BackendInterface
*/
public static function getBackend()
{
if (!isset(self::$instance)) {
$className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_LOCK);
self::$instance = new $className(
Redis_Client::getClient(),
Redis_Client::getDefaultPrefix('lock')
);
}
return self::$instance;
}
}

View File

@ -0,0 +1,61 @@
<?php
/**
* Lock backend interface.
*/
interface Redis_Lock_BackendInterface {
/**
* Acquire lock.
*
* @param string $name
* Lock name.
* @param float $timeout = 30.0
* (optional) Lock lifetime in seconds.
*
* @return bool
*/
public function lockAcquire($name, $timeout = 30.0);
/**
* Check if lock is available for acquire.
*
* @param string $name
* Lock to acquire.
*
* @return bool
*/
public function lockMayBeAvailable($name);
/**
* Wait a short amount of time before a second lock acquire attempt.
*
* @param string $name
* Lock name currently being locked.
* @param int $delay = 30
* Miliseconds to wait for.
*/
public function lockWait($name, $delay = 30);
/**
* Release given lock.
*
* @param string $name
*/
public function lockRelease($name);
/**
* Release all locks for the given lock token identifier.
*
* @param string $lockId = NULL
* (optional) If none given, remove all lock from the current page.
*/
public function lockReleaseAll($lock_id = NULL);
/**
* Get the unique page token for locks. Locks will be wipeout at each end of
* page request on a token basis.
*
* @return string
*/
public function getLockId();
}

View File

@ -0,0 +1,89 @@
<?php
/**
* Lock backend shared methods.
*/
abstract class Redis_Lock_DefaultBackend
extends Redis_AbstractBackend
implements Redis_Lock_BackendInterface
{
/**
* Current page lock token identifier.
*
* @var string
*/
protected $_lockId;
/**
* Existing locks for this page.
*
* @var array
*/
protected $_locks = array();
/**
* Default implementation from actual Drupal core.
*
* @see Redis_Lock_BackendInterface::lockWait()
*/
public function lockWait($name, $delay = 30) {
// Pause the process for short periods between calling
// lock_may_be_available(). This prevents hitting the database with constant
// database queries while waiting, which could lead to performance issues.
// However, if the wait period is too long, there is the potential for a
// large number of processes to be blocked waiting for a lock, especially
// if the item being rebuilt is commonly requested. To address both of these
// concerns, begin waiting for 25ms, then add 25ms to the wait period each
// time until it reaches 500ms. After this point polling will continue every
// 500ms until $delay is reached.
// $delay is passed in seconds, but we will be using usleep(), which takes
// microseconds as a parameter. Multiply it by 1 million so that all
// further numbers are equivalent.
$delay = (int) $delay * 1000000;
// Begin sleeping at 25ms.
$sleep = 25000;
while ($delay > 0) {
// This function should only be called by a request that failed to get a
// lock, so we sleep first to give the parallel request a chance to finish
// and release the lock.
usleep($sleep);
// After each sleep, increase the value of $sleep until it reaches
// 500ms, to reduce the potential for a lock stampede.
$delay = $delay - $sleep;
$sleep = min(500000, $sleep + 25000, $delay);
if ($this->lockMayBeAvailable($name)) {
// No longer need to wait.
return FALSE;
}
}
// The caller must still wait longer to get the lock.
return TRUE;
}
/**
* Default implementation from actual Drupal core.
*
* @see Redis_Lock_BackendInterface::getLockId()
*/
public function getLockId() {
if (!isset($this->_lockId)) {
$this->_lockId = uniqid(mt_rand(), TRUE);
// We only register a shutdown function if a lock is used.
drupal_register_shutdown_function('lock_release_all', $this->_lockId);
}
return $this->_lockId;
}
/**
* Generate a redis key name for the current lock name
*/
public function getKey($name = null) {
if (null === $name) {
return parent::getKey('lock');
} else {
return parent::getKey(array('lock', $name));
}
}
}

View File

@ -0,0 +1,138 @@
<?php
/**
* Predis lock backend implementation.
*
* This implementation works with a single key per lock so is viable when
* doing client side sharding and/or using consistent hashing algorithm.
*/
class Redis_Lock_PhpRedis extends Redis_Lock_DefaultBackend {
public function lockAcquire($name, $timeout = 30.0) {
$client = $this->getClient();
$key = $this->getKey($name);
$id = $this->getLockId();
// Insure that the timeout is at least 1 second, we cannot do otherwise with
// Redis, this is a minor change to the function signature, but in real life
// nobody will notice with so short duration.
$timeout = ceil(max($timeout, 1));
// 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.
$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 ($client->get($key) != $id) {
// Explicit UNWATCH we are not going to run the MULTI/EXEC block.
$client->unwatch();
unset($this->_locks[$name]);
return FALSE;
}
// See https://github.com/phpredis/phpredis#watch-unwatch
// MULTI and other commands can fail, so we can't chain calls.
if (FALSE !== ($result = $client->multi())) {
$client->setex($key, $timeout, $id);
$result = $client->exec();
}
// Did it broke?
if (FALSE === $result) {
unset($this->_locks[$name]);
// Explicit transaction release which also frees the WATCH'ed key.
$client->discard();
return FALSE;
}
return ($this->_locks[$name] = TRUE);
}
else {
$client->watch($key);
$owner = $client->get($key);
// If the $key is set they lock is not available
if (!empty($owner) && $id != $owner) {
$client->unwatch();
return FALSE;
}
// See https://github.com/phpredis/phpredis#watch-unwatch
// MULTI and other commands can fail, so we can't chain calls.
if (FALSE !== ($result = $client->multi())) {
$client->setex($key, $timeout, $id);
$result->exec();
}
// If another client modified the $key value, transaction will be discarded
// $result will be set to FALSE. This means atomicity have been broken and
// the other client took the lock instead of us.
if (FALSE === $result) {
// Explicit transaction release which also frees the WATCH'ed key.
$client->discard();
return FALSE;
}
// Register the lock.
return ($this->_locks[$name] = TRUE);
}
return FALSE;
}
public function lockMayBeAvailable($name) {
$client = $this->getClient();
$key = $this->getKey($name);
$id = $this->getLockId();
$value = $client->get($key);
return FALSE === $value || $id == $value;
}
public function lockRelease($name) {
$client = $this->getClient();
$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.
$client->watch($key);
if ($client->get($key) == $id) {
$client->multi();
$client->delete($key);
$client->exec();
}
else {
$client->unwatch();
}
}
public function lockReleaseAll($lock_id = NULL) {
if (!isset($lock_id) && empty($this->_locks)) {
return;
}
$client = $this->getClient();
$id = isset($lock_id) ? $lock_id : $this->getLockId();
// 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 (array_keys($this->_locks) as $name) {
$key = $this->getKey($name);
$owner = $client->get($key);
if (empty($owner) || $owner == $id) {
$client->delete($key);
}
}
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* Predis lock backend implementation.
*
* This implementation works with a single key per lock so is viable when
* doing client side sharding and/or using consistent hashing algorithm.
*/
class Redis_Lock_Predis extends Redis_Lock_DefaultBackend {
public function lockAcquire($name, $timeout = 30.0) {
$client = $this->getClient();
$key = $this->getKey($name);
$id = $this->getLockId();
// Insure that the timeout is at least 1 second, we cannot do otherwise with
// Redis, this is a minor change to the function signature, but in real life
// nobody will notice with so short duration.
$timeout = ceil(max($timeout, 1));
// 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.
$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 ($client->get($key) != $id) {
$client->unwatch();
unset($this->_locks[$name]);
return FALSE;
}
$replies = $client->pipeline(function($pipe) use ($key, $timeout, $id) {
$pipe->multi();
$pipe->setex($key, $timeout, $id);
$pipe->exec();
});
$execReply = array_pop($replies);
if (FALSE === $execReply[0]) {
unset($this->_locks[$name]);
return FALSE;
}
return TRUE;
}
else {
$client->watch($key);
$owner = $client->get($key);
if (!empty($owner) && $owner != $id) {
$client->unwatch();
unset($this->_locks[$name]);
return FALSE;
}
$replies = $client->pipeline(function($pipe) use ($key, $timeout, $id) {
$pipe->multi();
$pipe->setex($key, $timeout, $id);
$pipe->exec();
});
$execReply = array_pop($replies);
// If another client modified the $key value, transaction will be discarded
// $result will be set to FALSE. This means atomicity have been broken and
// the other client took the lock instead of us.
// EXPIRE and SETEX won't return something here, EXEC return is index 0
// This was determined debugging, seems to be Predis specific.
if (FALSE === $execReply[0]) {
return FALSE;
}
// Register the lock and return.
return ($this->_locks[$name] = TRUE);
}
return FALSE;
}
public function lockMayBeAvailable($name) {
$client = $this->getClient();
$key = $this->getKey($name);
$id = $this->getLockId();
$value = $client->get($key);
return empty($value) || $id == $value;
}
public function lockRelease($name) {
$client = $this->getClient();
$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.
$client->watch($key);
if ($client->get($key) == $id) {
$client->multi();
$client->del(array($key));
$client->exec();
}
else {
$client->unwatch();
}
}
public function lockReleaseAll($lock_id = NULL) {
if (!isset($lock_id) && empty($this->_locks)) {
return;
}
$client = $this->getClient();
$id = isset($lock_id) ? $lock_id : $this->getLockId();
// 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 (array_keys($this->_locks) as $name) {
$key = $this->getKey($name);
$owner = $client->get($key);
if (empty($owner) || $owner == $id) {
$client->del(array($key));
}
}
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* Common implementation for Redis-based implementations
*/
abstract class Redis_Path_AbstractHashLookup extends Redis_AbstractBackend implements
Redis_Path_HashLookupInterface
{
/**
* @todo document me
*
* @param string $key
* @param string $hkey
* @param string $hvalue
*/
abstract protected function saveInHash($key, $hkey, $hvalue);
/**
* @todo document me
*
* @param string $key
* @param string $hkey
* @param string $hvalue
*/
abstract protected function deleteInHash($key, $hkey, $hvalue);
/**
* @todo document me
*
* @param string $keyPrefix
* @param string $hkey
* @param string $language
*/
abstract protected function lookupInHash($keyPrefix, $hkey, $language = null);
/**
* Normalize value to avoid duplicate or false negatives
*
* @param string $value
*
* @return string
*/
private function normalize($value)
{
if (null !== $value) {
return strtolower(trim($value));
}
}
/**
* {@inheritdoc}
*/
public function saveAlias($source, $alias, $language = null)
{
$alias = $this->normalize($alias);
$source = $this->normalize($source);
if (null === $language) {
$language = LANGUAGE_NONE;
}
if (!empty($source)) {
$this->saveInHash($this->getKey(array(self::KEY_ALIAS, $language)), $source, $alias);
}
if (!empty($alias)) {
$this->saveInHash($this->getKey(array(self::KEY_SOURCE, $language)), $alias, $source);
}
}
/**
* {@inheritdoc}
*/
public function deleteAlias($source, $alias, $language = null)
{
$alias = $this->normalize($alias);
$source = $this->normalize($source);
if (null === $language) {
$language = LANGUAGE_NONE;
}
$this->deleteInHash($this->getKey(array(self::KEY_ALIAS, $language)), $source, $alias);
$this->deleteInHash($this->getKey(array(self::KEY_SOURCE, $language)), $alias, $source);
}
/**
* {@inheritdoc}
*/
public function lookupAlias($source, $language = null)
{
$source = $this->normalize($source);
return $this->lookupInHash(self::KEY_ALIAS, $source, $language);
}
/**
* {@inheritdoc}
*/
public function lookupSource($alias, $language = null)
{
$alias = $this->normalize($alias);
return $this->lookupInHash(self::KEY_SOURCE, $alias, $language);
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* Very fast hash based lookup interface.
*
* This will work for any key-value store whether it's APC, Redis, memcache...
* Rationale behind this is that Drupal calls hundreds of time per request the
* drupal_lookup_path() function and we need it to be very fast. The key of
* success to keep it stupid simple and coherent as the same time is that we
* consider this backend as a cache (more or less permanent) that might be
* cleared at any time, and synchronized as when necessary or incrementally.
* This should be very fast.
*
* Redis implementation will be the following:
*
* Aliases are stored into a Redis HASH and are stored per language basis.
* Key is:
* [SITEPREFIX:]path:dst:LANGUAGE
* Keys inside the hash are a MD5() of the source and values are the alias
*
* Sources are also stored the same way except the HASH key is the following:
* [SITEPREFIX:]path:src:LANGUAGE
* Keys inside the hash are a MD5() of the alias and values are the sources.
*
* In both case values are a comma separated list of string values.
*
* The MD5() should give us low collision algorithm and we'll keep it until
* no one experiences any problem.
*
* Alias and sources are always looked up using the language, hence the
* different keys for different languages.
*/
interface Redis_Path_HashLookupInterface
{
/**
* Alias HASH key prefix
*/
const KEY_ALIAS = 'path:a';
/**
* Source HASH key prefix
*/
const KEY_SOURCE = 'path:s';
/**
* Null value (not existing yet cached value)
*/
const VALUE_NULL = '!';
/**
* Values separator for hash values
*/
const VALUE_SEPARATOR = '#';
/**
* Alias is being inserted with the given source
*
* @param string $source
* @param string $alias
* @param string $language
*/
public function saveAlias($source, $alias, $language = null);
/**
* Alias is being deleted for the given source
*
* @param string $source
* @param string $alias
* @param string $language
*/
public function deleteAlias($source, $alias, $language = null);
/**
* A language is being deleted
*
* @param string $language
*/
public function deleteLanguage($language);
/**
* Lookup any alias for the given source
*
* First that has been inserted wins over the others
*
* @param string $source
* @param string $language
*
* @return string|null|false
* - The string value if found
* - null if not found
* - false if set as non existing
*/
public function lookupAlias($source, $language = null);
/**
* Lookup any source for the given alias
*
* First that has been inserted wins over the others
*
* @param string $alias
* @param string $language
*
* @return string|null|false
* - The string value if found
* - null if not found
* - false if set as non existing
*/
public function lookupSource($alias, $language = null);
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Null implementation.
*/
class Redis_Path_NullHashLookup implements Redis_Path_HashLookupInterface
{
public function saveAlias($source, $alias, $language = null)
{
}
public function deleteAlias($source, $alias, $language = null)
{
}
public function deleteLanguage($language)
{
}
public function lookupAlias($source, $language = null)
{
}
public function lookupSource($alias, $language = null)
{
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* PhpRedis implementation.
*
* @todo
* Set high expire value to the hash for rotation when memory is empty
* React upon cache clear all and rebuild path list?
*/
class Redis_Path_PhpRedis extends Redis_Path_AbstractHashLookup
{
protected function saveInHash($key, $hkey, $hvalue)
{
$client = $this->getClient();
$value = $client->hget($key, $hkey);
if ($value === self::VALUE_NULL) { // Remove any null values
$value = null;
}
if ($value) {
$existing = explode(self::VALUE_SEPARATOR, $value);
if (!in_array($hvalue, $existing)) {
// Prepend the most recent path to ensure it always be
// first fetched one
// @todo Ensure in case of update that its position does
// not changes (pid ordering in Drupal core)
$value = $hvalue . self::VALUE_SEPARATOR . $value;
} else { // Do nothing on empty value
$value = null;
}
} else if (empty($hvalue)) {
$value = self::VALUE_NULL;
} else {
$value = $hvalue;
}
if (!empty($value)) {
$client->hset($key, $hkey, $value);
}
// Empty value here means that we already got it
}
protected function deleteInHash($key, $hkey, $hvalue)
{
$client = $this->getClient();
$value = $client->hget($key, $hkey);
if ($value) {
$existing = explode(self::VALUE_SEPARATOR, $value);
if (false !== ($index = array_search($hvalue, $existing))) {
if (1 === count($existing)) {
$client->hdel($key, $hkey);
} else {
unset($existing[$index]);
$client->hset($key, $hkey, implode(self::VALUE_SEPARATOR, $existing));
}
}
}
}
protected function lookupInHash($keyPrefix, $hkey, $language = null)
{
$client = $this->getClient();
if (null === $language) {
$language = LANGUAGE_NONE;
$doNoneLookup = false;
} else if (LANGUAGE_NONE === $language) {
$doNoneLookup = false;
} else {
$doNoneLookup = true;
}
$ret = $client->hget($this->getKey(array($keyPrefix, $language)), $hkey);
if ($doNoneLookup && (!$ret || self::VALUE_NULL === $ret)) {
$previous = $ret;
$ret = $client->hget($this->getKey(array($keyPrefix, LANGUAGE_NONE)), $hkey);
if (!$ret || self::VALUE_NULL === $ret) {
// Restore null placeholder else we loose conversion to false
// and drupal_lookup_path() would attempt saving it once again
$ret = $previous;
}
}
if (self::VALUE_NULL === $ret) {
return false; // Needs conversion
}
if (empty($ret)) {
return null; // Value not found
}
$existing = explode(self::VALUE_SEPARATOR, $ret);
return reset($existing);
}
/**
* {@inheritdoc}
*/
public function deleteLanguage($language)
{
$client = $this->getClient();
$client->del($this->getKey(array(self::KEY_ALIAS, $language)));
$client->del($this->getKey(array(self::KEY_SOURCE, $language)));
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* PhpRedis implementation.
*
* @todo
* Set high expire value to the hash for rotation when memory is empty
* React upon cache clear all and rebuild path list?
*/
class Redis_Path_Predis extends Redis_Path_AbstractHashLookup
{
protected function saveInHash($key, $hkey, $hvalue)
{
$client = $this->getClient();
$value = $client->hget($key, $hkey);
if ($value === self::VALUE_NULL) { // Remove any null values
$value = null;
}
if ($value) {
$existing = explode(self::VALUE_SEPARATOR, $value);
if (!in_array($hvalue, $existing)) {
// Prepend the most recent path to ensure it always be
// first fetched one
// @todo Ensure in case of update that its position does
// not changes (pid ordering in Drupal core)
$value = $hvalue . self::VALUE_SEPARATOR . $value;
} else { // Do nothing on empty value
$value = null;
}
} else if (empty($hvalue)) {
$value = self::VALUE_NULL;
} else {
$value = $hvalue;
}
if (!empty($value)) {
$client->hset($key, $hkey, $value);
}
// Empty value here means that we already got it
}
protected function deleteInHash($key, $hkey, $hvalue)
{
$client = $this->getClient();
$value = $client->hget($key, $hkey);
if ($value) {
$existing = explode(self::VALUE_SEPARATOR, $value);
if (false !== ($index = array_search($hvalue, $existing))) {
if (1 === count($existing)) {
$client->hdel($key, $hkey);
} else {
unset($existing[$index]);
$client->hset($key, $hkey, implode(self::VALUE_SEPARATOR, $existing));
}
}
}
}
protected function lookupInHash($keyPrefix, $hkey, $language = null)
{
$client = $this->getClient();
if (null === $language) {
$language = LANGUAGE_NONE;
$doNoneLookup = false;
} else if (LANGUAGE_NONE === $language) {
$doNoneLookup = false;
} else {
$doNoneLookup = true;
}
$ret = $client->hget($this->getKey(array($keyPrefix, $language)), $hkey);
if ($doNoneLookup && (!$ret || self::VALUE_NULL === $ret)) {
$previous = $ret;
$ret = $client->hget($this->getKey(array($keyPrefix, LANGUAGE_NONE)), $hkey);
if (!$ret || self::VALUE_NULL === $ret) {
// Restore null placeholder else we loose conversion to false
// and drupal_lookup_path() would attempt saving it once again
$ret = $previous;
}
}
if (self::VALUE_NULL === $ret) {
return false; // Needs conversion
}
if (empty($ret)) {
return null; // Value not found
}
$existing = explode(self::VALUE_SEPARATOR, $ret);
return reset($existing);
}
/**
* {@inheritdoc}
*/
public function deleteLanguage($language)
{
$client = $this->getClient();
$client->del($this->getKey(array(self::KEY_ALIAS, $language)));
$client->del($this->getKey(array(self::KEY_SOURCE, $language)));
}
}

View File

@ -0,0 +1,58 @@
<?php
class Redis_Queue implements DrupalReliableQueueInterface
{
/**
* @var DrupalQueueInterface
*/
protected $backend;
/**
* Default contructor
*
* Beware that DrupalQueueInterface does not defines the __construct
* method in the interface yet is being used from DrupalQueue::get()
*
* @param unknown $name
*/
public function __construct($name)
{
$className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_QUEUE);
$this->backend = new $className(Redis_Client::getClient(), $name);
}
public function createItem($data)
{
return $this->backend->createItem($data);
}
public function numberOfItems()
{
return $this->backend->numberOfItems();
}
public function claimItem($lease_time = 3600)
{
return $this->backend->claimItem($lease_time);
}
public function deleteItem($item)
{
return $this->backend->deleteItem($item);
}
public function releaseItem($item)
{
return $this->backend->releaseItem($item);
}
public function createQueue()
{
return $this->backend->createQueue();
}
public function deleteQueue()
{
return $this->backend->deleteQueue();
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* Redis allows implementing reliable queues, here is the spec:
*
* - For each queue, you have 4 different HASH:
*
* - One for queued items queue:NAME:queued
*
* - One for claimed items being processed: queue:NAME:claimed
*
* - One for claimed items leave time: queue:NAME:leave
* Items from this one will be arbitrarily fetched at cron
* time and released when leave is outdated.
*
* - One containing the item values and other valuable stateful
* information: queue:NAME:data ;
*
* - For example, current job maximum identifier (auto increment
* emulation) will be stored in the "sequence" HASH key
*
* - All other keys within the HASH will be the items themselves,
* keys for those will always be numeric
*
* - Each time a queue will be emptied, even during a pragmatic process,
* it will be automatically deleted, reseting the sequence counter to
* the 0 value each time
*
* - Algorithm is a variation of the one described in "Reliable queue"
* section of http://redis.io/commands/rpoplpush and partial port of what
* you can find in the http://drupal.org/project/redis_queue module.
*
* You will find the driver specific implementation in the Redis_Queue_*
* classes as they may differ in how the API handles transaction, pipelining
* and return values.
*/
abstract class Redis_Queue_Base extends Redis_AbstractBackend implements
DrupalReliableQueueInterface
{
/**
* Key prefix for queue data.
*/
const QUEUE_KEY_PREFIX = 'queue';
/**
* Data HASH sequence key name.
*/
const QUEUE_HKEY_SEQ = 'seq';
/**
* Get data HASH key
*
* Key will already be prefixed
*
* @return string
*/
public function getKeyForData()
{
return $this->getKey('data');
}
/**
* Get queued items LIST key
*
* Key will already be prefixed
*
* @return string
*/
public function getKeyForQueue()
{
return $this->getKey('queued');
}
/**
* Get claimed LIST key
*
* Key will already be prefixed
*
* @return string
*/
public function getKeyForClaimed()
{
return $this->getKey('claimed');
}
/**
* Default contructor
*
* Beware that DrupalQueueInterface does not defines the __construct
* method in the interface yet is being used from DrupalQueue::get()
*
* @param mixed $client
* @param string $name
*/
public function __construct($client, $name)
{
parent::__construct($client, self::QUEUE_KEY_PREFIX . $name);
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* @todo
* Set high expire value to the hash for rotation when memory is empty
* React upon cache clear all and rebuild path list?
*/
class Redis_Queue_PhpRedis extends Redis_Queue_Base
{
public function createItem($data)
{
$client = $this->getClient();
$dKey = $this->getKeyForData();
$qKey = $this->getKeyForQueue();
// Identifier does not not need to be in the transaction,
// in case of any error we'll just skip a value in the sequence.
$id = $client->hincrby($dKey, self::QUEUE_HKEY_SEQ, 1);
$record = new stdClass();
$record->qid = $id;
$record->data = $data;
$record->timestamp = time();
$pipe = $client->multi(Redis::PIPELINE);
// Thanks to the redis_queue standalone module maintainers for
// this piece of code, very effective. Note that we added the
// pipeline thought.
$pipe->hsetnx($dKey, $id, serialize($record));
$pipe->llen($qKey);
$pipe->lpush($qKey, $id);
$ret = $pipe->exec();
if (!$success = ($ret[0] && $ret[1] < $ret[2])) {
if ($ret[0]) {
// HSETNEX worked but not the PUSH command we therefore
// need to drop the inserted data. I would have prefered
// a DISCARD instead but we are in pipelined transaction
// we cannot actually do a DISCARD here.
$client->hdel($dKey, $id);
}
}
return $success;
}
public function numberOfItems()
{
return $this->getClient()->llen($this->getKeyForQueue());
}
public function claimItem($lease_time = 30)
{
// @todo Deal with lease
$client = $this->getClient();
$id = $client->rpoplpush(
$this->getKeyForQueue(),
$this->getKeyForClaimed()
);
if ($id) {
if ($item = $client->hget($this->getKeyForData(), $id)) {
if ($item = unserialize($item)) {
return $item;
}
}
}
return false;
}
public function deleteItem($item)
{
$pipe = $this->getClient()->multi(Redis::PIPELINE);
$pipe->lrem($this->getKeyForQueue(), $item->qid);
$pipe->lrem($this->getKeyForClaimed(), $item->qid);
$pipe->hdel($this->getKeyForData(), $item->qid);
$pipe->exec();
}
public function releaseItem($item)
{
$pipe = $this->getClient()->multi(Redis::PIPELINE);
$pipe->lrem($this->getKeyForClaimed(), $item->qid, -1);
$pipe->lpush($this->getKeyForQueue(), $item->qid);
$ret = $pipe->exec();
return $ret[0] && $ret[1];
}
public function createQueue()
{
}
public function deleteQueue()
{
$this->getClient()->del(
$this->getKeyForQueue(),
$this->getKeyForClaimed(),
$this->getKeyForData()
);
}
}

View File

@ -0,0 +1,141 @@
<?php
abstract class Redis_Tests_AbstractUnitTestCase extends DrupalUnitTestCase
{
/**
* @var boolean
*/
static protected $loaderEnabled = false;
/**
* Enable the autoloader
*
* This exists in this class in case the autoloader is not set into the
* settings.php file or another way
*
* @return void|boolean
*/
static protected function enableAutoload()
{
if (self::$loaderEnabled) {
return;
}
if (class_exists('Redis_Client')) {
return;
}
spl_autoload_register(function ($className) {
$parts = explode('_', $className);
if ('Redis' === $parts[0]) {
$filename = __DIR__ . '/../lib/' . implode('/', $parts) . '.php';
return (bool) include_once $filename;
}
return false;
}, null, true);
self::$loaderEnabled = true;
}
/**
* Drupal $conf array backup
*
* @var array
*/
private $originalConf = array(
'cache_lifetime' => null,
'cache_prefix' => null,
'redis_client_interface' => null,
'redis_eval_enabled' => null,
'redis_flush_mode' => null,
'redis_perm_ttl' => null,
);
/**
* Prepare Drupal environmment for testing
*/
final private function prepareDrupalEnvironment()
{
// Site on which the tests are running may define this variable
// in their own settings.php file case in which it will be merged
// with testing site
global $conf;
foreach (array_keys($this->originalConf) as $key) {
if (isset($conf[$key])) {
$this->originalConf[$key] = $conf[$key];
unset($conf[$key]);
}
}
$conf['cache_prefix'] = $this->testId;
}
/**
* Restore Drupal environment after testing.
*/
final private function restoreDrupalEnvironment()
{
$GLOBALS['conf'] = $this->originalConf + $GLOBALS['conf'];
}
/**
* Prepare client manager
*/
final private function prepareClientManager()
{
$interface = $this->getClientInterface();
if (null === $interface) {
throw new \Exception("Test skipped due to missing driver");
}
$GLOBALS['conf']['redis_client_interface'] = $interface;
Redis_Client::reset();
}
/**
* Restore client manager
*/
final private function restoreClientManager()
{
Redis_Client::reset();
}
/**
* Set up the Redis configuration.
*
* Set up the needed variables using variable_set() if necessary.
*
* @return string
* Client interface or null if not exists
*/
abstract protected function getClientInterface();
/**
* {@inheritdoc}
*/
public function setUp()
{
self::enableAutoload();
$this->prepareDrupalEnvironment();
$this->prepareClientManager();
parent::setUp();
drupal_install_schema('system');
drupal_install_schema('locale');
}
/**
* {@inheritdoc}
*/
public function tearDown()
{
drupal_uninstall_schema('locale');
drupal_uninstall_schema('system');
$this->restoreDrupalEnvironment();
$this->restoreClientManager();
parent::tearDown();
}
}

View File

@ -0,0 +1,60 @@
<?php
class Redis_Tests_Admin_VariableTestCase extends DrupalWebTestCase
{
public static function getInfo()
{
return array(
'name' => 'Redis variables',
'description' => 'Checks that Redis module variables are correctly type hinted when saved.',
'group' => 'Redis',
);
}
protected $adminUser;
public function setUp()
{
parent::setUp('redis');
}
public function testSave()
{
$this->adminUser = $this->drupalCreateUser(array('administer site configuration'));
$this->drupalLogin($this->adminUser);
// Tests port is an int.
$this->drupalGet('admin/config/development/performance/redis');
$edit = array(
'redis_client_base' => '',
'redis_client_port' => '1234',
'redis_client_host' => 'localhost',
'redis_client_interface' => '',
);
$this->drupalPost('admin/config/development/performance/redis', $edit, t('Save configuration'));
// Force variable cache to refresh.
$test = variable_initialize();
$conf = &$GLOBALS['conf'];
$this->assertFalse(array_key_exists('redis_client_base', $conf), "Empty int value has been removed");
$this->assertFalse(array_key_exists('redis_client_interface', $conf), "Empty string value has been removed");
$this->assertIdentical($conf['redis_client_port'], 1234, "Saved int is an int");
$this->assertIdentical($conf['redis_client_host'], 'localhost', "Saved string is a string");
$this->drupalGet('admin/config/development/performance/redis');
$edit = array(
'redis_client_base' => '0',
'redis_client_port' => '1234',
'redis_client_host' => 'localhost',
'redis_client_interface' => '',
);
$this->drupalPost('admin/config/development/performance/redis', $edit, t('Save configuration'));
// Force variable cache to refresh.
$test = variable_initialize();
$conf = &$GLOBALS['conf'];
$this->assertIdentical($conf['redis_client_base'], 0, "Saved 0 valueed int is an int");
}
}

View File

@ -0,0 +1,27 @@
<?php
if (!class_exists('Redis_Tests_Cache_FixesUnitTestCase')) {
require_once(__DIR__ . '/FixesUnitTestCase.php');
}
class Redis_Tests_Cache_CompressedPhpRedisFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Compressed PhpRedis cache fixes',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function createCacheInstance($name = null)
{
return new Redis_CacheCompressed($name);
}
protected function getClientInterface()
{
return 'PhpRedis';
}
}

View File

@ -0,0 +1,27 @@
<?php
if (!class_exists('Redis_Tests_Cache_FlushUnitTestCase')) {
require_once(__DIR__ . '/FlushUnitTestCase.php');
}
class Redis_Tests_Cache_CompressedPhpRedisFlushUnitTestCase extends Redis_Tests_Cache_FlushUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Compressed PhpRedis cache flush',
'description' => 'Tests Redis module cache flush modes feature.',
'group' => 'Redis',
);
}
protected function createCacheInstance($name = null)
{
return new Redis_CacheCompressed($name);
}
protected function getClientInterface()
{
return 'PhpRedis';
}
}

View File

@ -0,0 +1,25 @@
<?php
class Redis_Tests_Cache_CompressedPhpRedisShardedFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Compressed PhpRedis cache fixes (S)',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function createCacheInstance($name = null)
{
return new Redis_CacheCompressed($name);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD;
return 'PhpRedis';
}
}

View File

@ -0,0 +1,25 @@
<?php
class Redis_Tests_Cache_CompressedPhpRedisShardedFlushUnitTestCase extends Redis_Tests_Cache_FlushUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Compressed PhpRedis cache flush (S)',
'description' => 'Tests Redis module cache flush modes feature.',
'group' => 'Redis',
);
}
protected function createCacheInstance($name = null)
{
return new Redis_CacheCompressed($name);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD;
return 'PhpRedis';
}
}

View File

@ -0,0 +1,25 @@
<?php
class Redis_Tests_Cache_CompressedPhpRedisShardedWithPipelineFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Compressed PhpRedis cache fixes (SP)',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function createCacheInstance($name = null)
{
return new Redis_CacheCompressed($name);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD_WITH_PIPELINING;
return 'PhpRedis';
}
}

View File

@ -0,0 +1,209 @@
<?php
if (!class_exists('Redis_Tests_AbstractUnitTestCase')) {
require_once(__DIR__ . '/../AbstractUnitTestCase.php');
}
/**
* Bugfixes made over time test class.
*/
abstract class Redis_Tests_Cache_FixesUnitTestCase extends Redis_Tests_AbstractUnitTestCase
{
/**
* @var Cache bin identifier
*/
static private $id = 1;
protected function createCacheInstance($name = null)
{
return new Redis_Cache($name);
}
/**
* Get cache backend
*
* @return Redis_Cache
*/
final protected function getBackend($name = null)
{
if (null === $name) {
// This is needed to avoid conflict between tests, each test
// seems to use the same Redis namespace and conflicts are
// possible.
$name = 'cache' . (self::$id++);
}
$backend = $this->createCacheInstance($name);
$this->assert(true, "Redis client is " . ($backend->isSharded() ? '' : "NOT ") . " sharded");
$this->assert(true, "Redis client is " . ($backend->allowTemporaryFlush() ? '' : "NOT ") . " allowed to flush temporary entries");
$this->assert(true, "Redis client is " . ($backend->allowPipeline() ? '' : "NOT ") . " allowed to use pipeline");
return $backend;
}
public function testTemporaryCacheExpire()
{
global $conf; // We are in unit tests so variable table does not exist.
$backend = $this->getBackend();
// Permanent entry.
$backend->set('test1', 'foo', CACHE_PERMANENT);
$data = $backend->get('test1');
$this->assertNotEqual(false, $data);
$this->assertIdentical('foo', $data->data);
// Permanent entries should not be dropped on clear() call.
$backend->clear();
$data = $backend->get('test1');
$this->assertNotEqual(false, $data);
$this->assertIdentical('foo', $data->data);
// Expiring entry with permanent default lifetime.
$conf['cache_lifetime'] = 0;
$backend->set('test2', 'bar', CACHE_TEMPORARY);
sleep(2);
$data = $backend->get('test2');
$this->assertNotEqual(false, $data);
$this->assertIdentical('bar', $data->data);
sleep(2);
$data = $backend->get('test2');
$this->assertNotEqual(false, $data);
$this->assertIdentical('bar', $data->data);
// Expiring entry with negative lifetime.
$backend->set('test3', 'baz', time() - 100);
$data = $backend->get('test3');
$this->assertEqual(false, $data);
// Expiring entry with short lifetime.
$backend->set('test4', 'foobar', time() + 2);
$data = $backend->get('test4');
$this->assertNotEqual(false, $data);
$this->assertIdentical('foobar', $data->data);
sleep(4);
$data = $backend->get('test4');
$this->assertEqual(false, $data);
// Expiring entry with short default lifetime.
$conf['cache_lifetime'] = 1;
$backend->refreshMaxTtl();
$backend->set('test5', 'foobaz', CACHE_TEMPORARY);
$data = $backend->get('test5');
$this->assertNotEqual(false, $data);
$this->assertIdentical('foobaz', $data->data);
sleep(3);
$data = $backend->get('test5');
$this->assertEqual(false, $data);
}
public function testDefaultPermTtl()
{
global $conf;
unset($conf['redis_perm_ttl']);
$backend = $this->getBackend();
$this->assertIdentical(Redis_Cache::LIFETIME_PERM_DEFAULT, $backend->getPermTtl());
}
public function testUserSetDefaultPermTtl()
{
global $conf;
// This also testes string parsing. Not fully, but at least one case.
$conf['redis_perm_ttl'] = "3 months";
$backend = $this->getBackend();
$this->assertIdentical(7776000, $backend->getPermTtl());
}
public function testUserSetPermTtl()
{
global $conf;
// This also testes string parsing. Not fully, but at least one case.
$conf['redis_perm_ttl'] = "1 months";
$backend = $this->getBackend();
$this->assertIdentical(2592000, $backend->getPermTtl());
}
public function testGetMultiple()
{
$backend = $this->getBackend();
$backend->set('multiple1', 1);
$backend->set('multiple2', 2);
$backend->set('multiple3', 3);
$backend->set('multiple4', 4);
$cidList = array('multiple1', 'multiple2', 'multiple3', 'multiple4', 'multiple5');
$ret = $backend->getMultiple($cidList);
$this->assertEqual(1, count($cidList));
$this->assertFalse(isset($cidList[0]));
$this->assertFalse(isset($cidList[1]));
$this->assertFalse(isset($cidList[2]));
$this->assertFalse(isset($cidList[3]));
$this->assertTrue(isset($cidList[4]));
$this->assertEqual(4, count($ret));
$this->assertTrue(isset($ret['multiple1']));
$this->assertTrue(isset($ret['multiple2']));
$this->assertTrue(isset($ret['multiple3']));
$this->assertTrue(isset($ret['multiple4']));
$this->assertFalse(isset($ret['multiple5']));
}
public function testPermTtl()
{
global $conf;
// This also testes string parsing. Not fully, but at least one case.
$conf['redis_perm_ttl'] = "2 seconds";
$backend = $this->getBackend();
$this->assertIdentical(2, $backend->getPermTtl());
$backend->set('test6', 'cats are mean');
$this->assertIdentical('cats are mean', $backend->get('test6')->data);
sleep(3);
$item = $backend->get('test6');
$this->assertTrue(empty($item));
}
public function testClearAsArray()
{
$backend = $this->getBackend();
$backend->set('test7', 1);
$backend->set('test8', 2);
$backend->set('test9', 3);
$backend->clear(array('test7', 'test9'));
$item = $backend->get('test7');
$this->assertTrue(empty($item));
$item = $backend->get('test8');
$this->assertEqual(2, $item->data);
$item = $backend->get('test9');
$this->assertTrue(empty($item));
}
public function testGetMultipleAlterCidsWhenCacheHitsOnly()
{
$backend = $this->getBackend();
$backend->clear('*', true); // It seems that there are leftovers.
$backend->set('mtest1', 'pouf');
$cids_partial_hit = array('foo' => 'mtest1', 'bar' => 'mtest2');
$entries = $backend->getMultiple($cids_partial_hit);
$this->assertIdentical(1, count($entries));
// Note that the key is important because the method should
// keep the keys synchronized.
$this->assertEqual(array('bar' => 'mtest2'), $cids_partial_hit);
$backend->clear('mtest1');
$cids_no_hit = array('cat' => 'mtest1', 'dog' => 'mtest2');
$entries = $backend->getMultiple($cids_no_hit);
$this->assertIdentical(0, count($entries));
$this->assertEqual(array('cat' => 'mtest1', 'dog' => 'mtest2'), $cids_no_hit);
}
}

View File

@ -0,0 +1,185 @@
<?php
abstract class Redis_Tests_Cache_FlushUnitTestCase extends Redis_Tests_AbstractUnitTestCase
{
/**
* @var Cache bin identifier
*/
static private $id = 1;
protected function createCacheInstance($name = null)
{
return new Redis_Cache($name);
}
/**
* Get cache backend
*
* @return Redis_Cache
*/
final protected function getBackend($name = null)
{
if (null === $name) {
// This is needed to avoid conflict between tests, each test
// seems to use the same Redis namespace and conflicts are
// possible.
$name = 'cache' . (self::$id++);
}
$backend = $this->createCacheInstance($name);
$this->assert(true, "Redis client is " . ($backend->isSharded() ? '' : "NOT ") . " sharded");
$this->assert(true, "Redis client is " . ($backend->allowTemporaryFlush() ? '' : "NOT ") . " allowed to flush temporary entries");
$this->assert(true, "Redis client is " . ($backend->allowPipeline() ? '' : "NOT ") . " allowed to use pipeline");
return $backend;
}
/**
* Tests that with a default cache lifetime temporary non expired
* items are kept even when in temporary flush mode.
*/
public function testFlushIsTemporaryWithLifetime()
{
$GLOBALS['conf']['cache_lifetime'] = 112;
$backend = $this->getBackend();
// Even though we set a flush mode into this bin, Drupal default
// behavior when a cache_lifetime is set is to override the backend
// one in order to keep the core behavior and avoid potential
// nasty bugs.
$this->assertFalse($backend->allowTemporaryFlush());
$backend->set('test7', 42, CACHE_PERMANENT);
$backend->set('test8', 'foo', CACHE_TEMPORARY);
$backend->set('test9', 'bar', time() + 1000);
$backend->clear();
$cache = $backend->get('test7');
$this->assertNotEqual(false, $cache);
$this->assertEqual($cache->data, 42);
$cache = $backend->get('test8');
$this->assertNotEqual(false, $cache);
$this->assertEqual($cache->data, 'foo');
$cache = $backend->get('test9');
$this->assertNotEqual(false, $cache);
$this->assertEqual($cache->data, 'bar');
}
/**
* Tests that with no default cache lifetime all temporary items are
* droppped when in temporary flush mode.
*/
public function testFlushIsTemporaryWithoutLifetime()
{
$backend = $this->getBackend();
$this->assertTrue($backend->allowTemporaryFlush());
$backend->set('test10', 42, CACHE_PERMANENT);
// Ugly concatenation with the mode, but it will be visible in tests
// reports if the entry shows up, thus allowing us to know which real
// test case is run at this time
$backend->set('test11', 'foo' . $backend->isSharded(), CACHE_TEMPORARY);
$backend->set('test12', 'bar' . $backend->isSharded(), time() + 10);
$backend->clear();
$cache = $backend->get('test10');
$this->assertNotEqual(false, $cache);
$this->assertEqual($cache->data, 42);
$this->assertFalse($backend->get('test11'));
$cache = $backend->get('test12');
$this->assertNotEqual(false, $cache);
}
public function testNormalFlushing()
{
$backend = $this->getBackend();
$backendUntouched = $this->getBackend();
// Set a few entries.
$backend->set('test13', 'foo');
$backend->set('test14', 'bar', CACHE_TEMPORARY);
$backend->set('test15', 'baz', time() + 3);
$backendUntouched->set('test16', 'dog');
$backendUntouched->set('test17', 'cat', CACHE_TEMPORARY);
$backendUntouched->set('test18', 'xor', time() + 5);
// This should not do anything (bugguy command)
$backend->clear('', true);
$backend->clear('', false);
$this->assertNotIdentical(false, $backend->get('test13'));
$this->assertNotIdentical(false, $backend->get('test14'));
$this->assertNotIdentical(false, $backend->get('test15'));
$this->assertNotIdentical(false, $backendUntouched->get('test16'));
$this->assertNotIdentical(false, $backendUntouched->get('test17'));
$this->assertNotIdentical(false, $backendUntouched->get('test18'));
// This should clear every one, permanent and volatile
$backend->clear('*', true);
$this->assertFalse($backend->get('test13'));
$this->assertFalse($backend->get('test14'));
$this->assertFalse($backend->get('test15'));
$this->assertNotIdentical(false, $backendUntouched->get('test16'));
$this->assertNotIdentical(false, $backendUntouched->get('test17'));
$this->assertNotIdentical(false, $backendUntouched->get('test18'));
}
public function testPrefixDeletionWithSeparatorChar()
{
$backend = $this->getBackend();
$backend->set('testprefix10', 'foo');
$backend->set('testprefix11', 'foo');
$backend->set('testprefix:12', 'bar');
$backend->set('testprefix:13', 'baz');
$backend->set('testnoprefix14', 'giraffe');
$backend->set('testnoprefix:15', 'elephant');
$backend->clear('testprefix:', true);
$this->assertFalse($backend->get('testprefix:12'));
$this->assertFalse($backend->get('testprefix:13'));
// @todo Temporary fix
// At the moment shard enabled backends will erase all data instead
// of just removing by prefix, so those tests won't pass
if (!$backend->isSharded()) {
$this->assertNotIdentical(false, $backend->get('testprefix10'));
$this->assertNotIdentical(false, $backend->get('testprefix11'));
$this->assertNotIdentical(false, $backend->get('testnoprefix14'));
$this->assertNotIdentical(false, $backend->get('testnoprefix:15'));
}
$backend->clear('testprefix', true);
$this->assertFalse($backend->get('testprefix10'));
$this->assertFalse($backend->get('testprefix11'));
// @todo Temporary fix
// At the moment shard enabled backends will erase all data instead
// of just removing by prefix, so those tests won't pass
if (!$backend->isSharded()) {
$this->assertNotIdentical(false, $backend->get('testnoprefix14'));
$this->assertNotIdentical(false, $backend->get('testnoprefix:15'));
}
}
public function testOrder()
{
$backend = $this->getBackend();
for ($i = 0; $i < 10; ++$i) {
$id = 'speedtest' . $i;
$backend->set($id, 'somevalue');
$this->assertNotIdentical(false, $backend->get($id));
$backend->clear('*', true);
// Value created the same second before is dropped
$this->assertFalse($backend->get($id));
$backend->set($id, 'somevalue');
// Value created the same second after is kept
$this->assertNotIdentical(false, $backend->get($id));
}
}
}

View File

@ -0,0 +1,22 @@
<?php
if (!class_exists('Redis_Tests_Cache_FixesUnitTestCase')) {
require_once(__DIR__ . '/FixesUnitTestCase.php');
}
class Redis_Tests_Cache_PhpRedisFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis cache fixes',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'PhpRedis';
}
}

View File

@ -0,0 +1,22 @@
<?php
if (!class_exists('Redis_Tests_Cache_FlushUnitTestCase')) {
require_once(__DIR__ . '/FlushUnitTestCase.php');
}
class Redis_Tests_Cache_PhpRedisFlushUnitTestCase extends Redis_Tests_Cache_FlushUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis cache flush',
'description' => 'Tests Redis module cache flush modes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'PhpRedis';
}
}

View File

@ -0,0 +1,20 @@
<?php
class Redis_Tests_Cache_PhpRedisShardedFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis cache fixes (S)',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD;
return 'PhpRedis';
}
}

View File

@ -0,0 +1,20 @@
<?php
class Redis_Tests_Cache_PhpRedisShardedFlushUnitTestCase extends Redis_Tests_Cache_FlushUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis cache flush (S)',
'description' => 'Tests Redis module cache flush modes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD;
return 'PhpRedis';
}
}

View File

@ -0,0 +1,20 @@
<?php
class Redis_Tests_Cache_PhpRedisShardedWithPipelineFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis cache fixes (SP)',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD_WITH_PIPELINING;
return 'PhpRedis';
}
}

View File

@ -0,0 +1,18 @@
<?php
class Redis_Tests_Cache_PredisFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis cache fixes',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'Predis';
}
}

View File

@ -0,0 +1,18 @@
<?php
class Redis_Tests_Cache_PredisFlushUnitTestCase extends Redis_Tests_Cache_FlushUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis cache flush',
'description' => 'Tests Redis module cache flush modes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'Predis';
}
}

View File

@ -0,0 +1,20 @@
<?php
class Redis_Tests_Cache_PredisShardedFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis cache fixes (S)',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD;
return 'Predis';
}
}

View File

@ -0,0 +1,20 @@
<?php
class Redis_Tests_Cache_PredisShardedFlushUnitTestCase extends Redis_Tests_Cache_FlushUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis cache flush (S)',
'description' => 'Tests Redis module cache flush modes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD;
return 'Predis';
}
}

View File

@ -0,0 +1,20 @@
<?php
class Redis_Tests_Cache_PredisShardedWithPipelineFixesUnitTestCase extends Redis_Tests_Cache_FixesUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis cache fixes (SP)',
'description' => 'Tests Redis module cache fixes feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
$GLOBALS['conf']['redis_flush_mode'] = Redis_Cache::FLUSH_SHARD_WITH_PIPELINING;
return 'Predis';
}
}

View File

@ -0,0 +1,66 @@
<?php
class Redis_Tests_Client_UnitTestCase extends Redis_Tests_AbstractUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Redis client manager',
'description' => 'Tests Redis module client manager feature.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'PhpRedis';
}
public function getManager()
{
return new Redis_Client_Manager(
new Redis_Tests_Client_MockFactory(),
array(
'default' => array(),
'foo' => array(
'host' => 'foo.com',
'port' => 666,
),
'bar' => array(
'host' => 'bar.com',
),
)
);
}
public function testManagerServerList()
{
$manager = $this->getManager();
$defaultClient = $manager->getClient();
$this->assertTrue(is_object($defaultClient));
// Ensure defaults are OK
$this->assertIdentical(Redis_Client_Manager::REDIS_DEFAULT_HOST, $defaultClient->host);
$this->assertIdentical(Redis_Client_Manager::REDIS_DEFAULT_PORT, $defaultClient->port);
$this->assertFalse(property_exists($defaultClient, 'base'));
$this->assertFalse(property_exists($defaultClient, 'password'));
$client = $manager->getClient('foo');
$this->assertIdentical('foo.com', $client->host);
$this->assertIdentical(666, $client->port);
$client = $manager->getClient('bar');
$this->assertIdentical('bar.com', $client->host);
$this->assertIdentical(Redis_Client_Manager::REDIS_DEFAULT_PORT, $client->port);
$this->assertIdentical($defaultClient, $manager->getClient('non_existing'));
try {
$manager->getClient('other_non_existing', false);
$this->assert(false);
} catch (\InvalidArgumentException $e) {
$this->assert(true);
}
}
}

View File

@ -0,0 +1,14 @@
<?php
class Redis_Tests_Client_MockFactory implements Redis_Client_FactoryInterface
{
public function getClient($options = array())
{
return (object)$options;
}
public function getName()
{
return 'Mock';
}
}

View File

@ -0,0 +1,119 @@
<?php
abstract class Redis_Tests_Lock_LockingUnitTestCase extends Redis_Tests_AbstractUnitTestCase
{
/**
* Ensure lock flush at tear down
*
* @var array
*/
protected $backends = array();
/**
* Get the lock client class name
*
* @return string
* Lock backend class name or null if cannot spawn it
*/
abstract protected function getLockBackendClass();
public function tearDown()
{
if (!empty($this->backends)) {
foreach ($this->backends as $backend) {
$backend->lockReleaseAll();
}
$this->backends = array();
}
parent::tearDown();
}
/**
* Create a new lock backend with a generated lock id
*
* @return Redis_Lock_BackendInterface
*/
public function createLockBackend()
{
if (!$this->getLockBackendClass()) {
throw new \Exception("Lock backend class does not exist");
}
$className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_LOCK);
return $this->backends[] = new $className(
Redis_Client::getClient(),
Redis_Client::getDefaultPrefix('lock')
);
}
public function testLock()
{
$b1 = $this->createLockBackend();
$b2 = $this->createLockBackend();
$s = $b1->lockAcquire('test1', 20000);
$this->assertTrue($s, "Lock test1 acquired");
$s = $b1->lockAcquire('test1', 20000);
$this->assertTrue($s, "Lock test1 acquired a second time by the same thread");
$s = $b2->lockAcquire('test1', 20000);
$this->assertFalse($s, "Lock test1 could not be acquired by another thread");
$b2->lockRelease('test1');
$s = $b2->lockAcquire('test1');
$this->assertFalse($s, "Lock test1 could not be released by another thread");
$b1->lockRelease('test1');
$s = $b2->lockAcquire('test1');
$this->assertTrue($s, "Lock test1 has been released by the first thread");
}
public function testReleaseAll()
{
$b1 = $this->createLockBackend();
$b2 = $this->createLockBackend();
$b1->lockAcquire('test1', 200);
$b1->lockAcquire('test2', 2000);
$b1->lockAcquire('test3', 20000);
$s = $b2->lockAcquire('test2');
$this->assertFalse($s, "Lock test2 could not be released by another thread");
$s = $b2->lockAcquire('test3');
$this->assertFalse($s, "Lock test4 could not be released by another thread");
$b1->lockReleaseAll();
$s = $b2->lockAcquire('test1');
$this->assertTrue($s, "Lock test1 has been released");
$s = $b2->lockAcquire('test2');
$this->assertTrue($s, "Lock test2 has been released");
$s = $b2->lockAcquire('test3');
$this->assertTrue($s, "Lock test3 has been released");
$b2->lockReleaseAll();
}
public function testConcurentLock()
{
/*
* Code for web test case
*
$this->drupalGet('redis/acquire/test1/1000');
$this->assertText("REDIS_ACQUIRED", "Lock test1 acquired");
$this->drupalGet('redis/acquire/test1/1');
$this->assertText("REDIS_FAILED", "Lock test1 could not be acquired by a second thread");
$this->drupalGet('redis/acquire/test2/1000');
$this->assertText("REDIS_ACQUIRED", "Lock test2 acquired");
$this->drupalGet('redis/acquire/test2/1');
$this->assertText("REDIS_FAILED", "Lock test2 could not be acquired by a second thread");
*/
}
}

View File

@ -0,0 +1,27 @@
<?php
if (!class_exists('Redis_Tests_Lock_LockingUnitTestCase')) {
require_once(__DIR__ . '/LockingUnitTestCase.php');
}
class Redis_Tests_Lock_PhpRedisLockingUnitTestCase extends Redis_Tests_Lock_LockingUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis Redis locking',
'description' => 'Ensure that Redis locking feature is working OK.',
'group' => 'Redis',
);
}
protected function getLockBackendClass()
{
return 'Redis_Lock_PhpRedis';
}
protected function getClientInterface()
{
return 'PhpRedis';
}
}

View File

@ -0,0 +1,23 @@
<?php
class Redis_Tests_Lock_PredisLockingUnitTestCase extends Redis_Tests_Lock_LockingUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis Redis locking',
'description' => 'Ensure that Redis locking feature is working OK.',
'group' => 'Redis',
);
}
protected function getLockBackendClass()
{
return 'Redis_Lock_Predis';
}
protected function getClientInterface()
{
return 'Predis';
}
}

View File

@ -0,0 +1,148 @@
<?php
/**
* Bugfixes made over time test class.
*/
abstract class Redis_Tests_Path_PathUnitTestCase extends Redis_Tests_AbstractUnitTestCase
{
/**
* @var Cache bin identifier
*/
static private $id = 1;
/**
* Get cache backend
*
* @return Redis_Path_HashLookupInterface
*/
final protected function getBackend($name = null)
{
if (null === $name) {
// This is needed to avoid conflict between tests, each test
// seems to use the same Redis namespace and conflicts are
// possible.
$name = 'cache' . (self::$id++);
}
$className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_PATH);
$hashLookup = new $className(Redis_Client::getClient(), 'path', Redis_Client::getDefaultPrefix('path'));
return $hashLookup;
}
/**
* Tests basic functionnality
*/
public function testPathLookup()
{
$backend = $this->getBackend();
$source = $backend->lookupSource('node-1-fr', 'fr');
$this->assertIdentical(null, $source);
$alias = $backend->lookupAlias('node/1', 'fr');
$this->assertIdentical(null, $source);
$backend->saveAlias('node/1', 'node-1-fr', 'fr');
$source = $backend->lookupSource('node-1-fr', 'fr');
$source = $backend->lookupSource('node-1-fr', 'fr');
$this->assertIdentical('node/1', $source);
$alias = $backend->lookupAlias('node/1', 'fr');
$this->assertIdentical('node-1-fr', $alias);
// Delete and ensure it does not exist anymore.
$backend->deleteAlias('node/1', 'node-1-fr', 'fr');
$source = $backend->lookupSource('node-1-fr', 'fr');
$this->assertIdentical(null, $source);
$alias = $backend->lookupAlias('node/1', 'fr');
$this->assertIdentical(null, $source);
// Set more than one aliases and ensure order at loading.
$backend->saveAlias('node/1', 'node-1-fr-1', 'fr');
$backend->saveAlias('node/1', 'node-1-fr-2', 'fr');
$backend->saveAlias('node/1', 'node-1-fr-3', 'fr');
$alias = $backend->lookupAlias('node/1', 'fr');
$this->assertIdentical('node-1-fr-3', $alias);
// Add another alias to test the delete language feature.
// Also add some other languages aliases.
$backend->saveAlias('node/1', 'node-1');
$backend->saveAlias('node/2', 'node-2-en', 'en');
$backend->saveAlias('node/3', 'node-3-ca', 'ca');
// Ok, delete fr and tests every other are still there.
$backend->deleteLanguage('fr');
$alias = $backend->lookupAlias('node/1');
$this->assertIdentical('node-1', $alias);
$alias = $backend->lookupAlias('node/2', 'en');
$this->assertIdentical('node-2-en', $alias);
$alias = $backend->lookupAlias('node/3', 'ca');
$this->assertIdentical('node-3-ca', $alias);
// Now create back a few entries in some langage and
// ensure fallback to no language also works.
$backend->saveAlias('node/4', 'node-4');
$backend->saveAlias('node/4', 'node-4-es', 'es');
$alias = $backend->lookupAlias('node/4');
$this->assertIdentical('node-4', $alias);
$alias = $backend->lookupAlias('node/4', 'es');
$this->assertIdentical('node-4-es', $alias);
$alias = $backend->lookupAlias('node/4', 'fr');
$this->assertIdentical('node-4', $alias);
}
/**
* Tests https://www.drupal.org/node/2728831
*/
public function testSomeEdgeCaseFalseNegative()
{
$backend = $this->getBackend();
$backend->deleteLanguage('fr');
$backend->deleteLanguage('und');
$backend->saveAlias('node/123', 'node-123');
// Language lookup should return the language neutral value if no value
$source = $backend->lookupSource('node-123', 'fr');
$this->assertIdentical($source, 'node/123');
$source = $backend->lookupAlias('node/123', 'fr');
$this->assertIdentical($source, 'node-123');
// Now, let's consider we have an item we don't know if it exists or
// not, per definition we should not return a strict FALSE but a NULL
// value instead to tell "we don't know anything about this". In a
// very specific use-case, if the language neutral value is a strict
// "not exists" value, it should still return NULL instead of FALSE
// if another language was asked for.
// Store "value null" for the language neutral entry
$backend->saveAlias('node/456', Redis_Path_HashLookupInterface::VALUE_NULL);
$source = $backend->lookupAlias('node/456');
$this->assertIdentical(false, $source);
$source = $backend->lookupAlias('node/456', 'fr');
$this->assertIdentical(null, $source);
}
/**
* Tests that lookup is case insensitive
*/
public function testCaseInsensitivePathLookup()
{
$backend = $this->getBackend();
$backend->saveAlias('node/1', 'Node-1-FR', 'fr');
$source = $backend->lookupSource('NODE-1-fr', 'fr');
$this->assertIdentical('node/1', $source);
$source = $backend->lookupSource('node-1-FR', 'fr');
$this->assertIdentical('node/1', $source);
$alias = $backend->lookupAlias('node/1', 'fr');
$this->assertIdentical('node-1-fr', strtolower($alias));
// Delete and ensure it does not exist anymore.
$backend->deleteAlias('node/1', 'node-1-FR', 'fr');
$source = $backend->lookupSource('Node-1-FR', 'fr');
$this->assertIdentical(null, $source);
$alias = $backend->lookupAlias('node/1', 'fr');
$this->assertIdentical(null, $source);
}
}

View File

@ -0,0 +1,22 @@
<?php
if (!class_exists('Redis_Tests_Path_PathUnitTestCase')) {
require_once(__DIR__ . '/PathUnitTestCase.php');
}
class Redis_Tests_Path_PhpRedisPathUnitTestCase extends Redis_Tests_Path_PathUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis path inc replacement',
'description' => 'Tests PhpRedis path inc replacement.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'PhpRedis';
}
}

View File

@ -0,0 +1,18 @@
<?php
class Redis_Tests_Path_PredisPathUnitTestCase extends Redis_Tests_Path_PathUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis path inc replacement',
'description' => 'Tests Predis path inc replacement.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'Predis';
}
}

View File

@ -0,0 +1,22 @@
<?php
if (!class_exists('Redis_Tests_Queue_QueueUnitTestCase')) {
require_once(__DIR__ . '/QueueUnitTestCase.php');
}
class Redis_Tests_Queue_PhpRedisQueueUnitTestCase extends Redis_Tests_Queue_QueueUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'PhpRedis Redis queue',
'description' => 'Ensure that Redis queue feature is working OK.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'PhpRedis';
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
class Redis_Tests_Queue_PredisQueueUnitTestCase extends Redis_Tests_Queue_QueueUnitTestCase
{
public static function getInfo()
{
return array(
'name' => 'Predis Redis queue',
'description' => 'Ensure that Redis queue feature is working OK.',
'group' => 'Redis',
);
}
protected function getClientInterface()
{
return 'Predis';
}
}
*/

View File

@ -0,0 +1,133 @@
<?php
/**
* Some tests in there credits goes to the redis_queue module.
* Thanks to their author.
*/
abstract class Redis_Tests_Queue_QueueUnitTestCase extends Redis_Tests_AbstractUnitTestCase
{
/**
* @var Redis_Queue
*/
public $queue;
/**
* @var string
*/
public $name;
public function setUp()
{
parent::setUp();
module_load_include('inc', 'system', 'system.queue');
$this->name = 'redis-queue-test-' . time();
$this->queue = new Redis_Queue($this->name);
}
public function tearDown()
{
$this->queue->deleteQueue();
$this->name = null;
parent::tearDown();
}
public function testCreate()
{
$res = $this->queue->createItem('test-queue-item-create');
$num = $this->queue->numberOfItems();
$this->assertEqual(1, $num);
}
public function testClaim()
{
$data = 'test-queue-item-claimed';
$res = $this->queue->createItem($data);
$item = $this->queue->claimItem();
$this->assertEqual($data, $item->data);
}
/*
public function testClaimBlocking()
{
$data = 'test-queue-item-claimed';
$res = $this->queue->createItem($data);
$this->assertTrue($res);
$item = $this->queue->claimItemBlocking(10);
$this->assertEqual($data, $item->data);
}
*/
public function testRelease()
{
$data = 'test-queue-item';
$res = $this->queue->createItem($data);
$item = $this->queue->claimItem();
$num = $this->queue->numberOfItems();
$this->assertEqual(0, $num);
$res = $this->queue->releaseItem($item);
$num = $this->queue->numberOfItems();
$this->assertEqual(1, $num);
}
public function testOrder()
{
$keys = array('test1', 'test2', 'test3');
foreach ($keys as $k) {
$this->queue->createItem($k);
}
$first = $this->queue->claimItem();
$this->assertEqual($first->data, $keys[0]);
$second = $this->queue->claimItem();
$this->assertEqual($second->data, $keys[1]);
$this->queue->releaseItem($first);
$third = $this->queue->claimItem();
$this->assertEqual($third->data, $keys[2]);
$first_again = $this->queue->claimItem();
$this->assertEqual($first_again->data, $keys[0]);
$num = $this->queue->numberOfItems();
$this->assertEqual(0, $num);
}
/*
public function lease()
{
$data = 'test-queue-item';
$res = $this->queue->createItem($data);
$num = $this->queue->numberOfItems();
$this->assertEquals(1, $num);
$item = $this->queue->claimItem(1);
// In Redis 2.4 the expire could be between zero to one seconds off.
sleep(2);
$expired = $this->queue->expire();
$this->assertEquals(1, $expired);
$this->assertEquals(1, $this->queue->numberOfItems());
// Create a second queue to test expireAll()
$q2 = new RedisQueue($this->name . '_2');
$q2->createItem($data);
$q2->createItem($data);
$this->assertEquals(2, $q2->numberOfItems());
$item = $this->queue->claimItem(1);
$item2 = $q2->claimItem(1);
$this->assertEquals(1, $q2->numberOfItems());
sleep(2);
$expired = $this->queue->expireAll();
$this->assertEquals(2, $expired);
$this->assertEquals(1, $this->queue->numberOfItems());
$this->assertEquals(2, $q2->numberOfItems());
$q2->deleteQueue();
}
*/
}

View File

@ -0,0 +1,104 @@
<?php
/**
* @file
* Redis module administration pages.
*/
/**
* Main settings and review administration screen.
*/
function redis_settings_form($form, &$form_state) {
$form['connection'] = array(
'#type' => 'fieldset',
'#title' => t("Connection information"),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['connection']['scheme'] = array(
'#type' => 'textfield',
'#title' => t("Scheme"),
'#default_value' => 'tcp',
'#disabled' => TRUE,
'#description' => t("Connection scheme.") . " " . t("Only <em>tcp</em> is currently supported. This is ignored when using a UNIX socket."),
);
$form['connection']['redis_client_host'] = array(
'#type' => 'textfield',
'#title' => t("Host"),
'#default_value' => variable_get('redis_client_host', NULL),
'#description' => t("Redis server host. Default is <em>@default</em>.", array('@default' => Redis_Client_Manager::REDIS_DEFAULT_HOST)),
);
$form['connection']['redis_client_port'] = array(
'#type' => 'textfield',
'#title' => t("Port"),
'#default_value' => variable_get('redis_client_port', NULL),
'#description' => t("Redis server port. Default is <em>@default</em>.", array('@default' => Redis_Client_Manager::REDIS_DEFAULT_PORT)),
);
$form['connection']['redis_client_socket'] = array(
'#type' => 'textfield',
'#title' => t("UNIX socket"),
'#default_value' => variable_get('redis_client_socket', NULL),
'#description' => t("Redis UNIX socket for connection. If set remote server host and port will be ignored."),
);
$form['connection']['redis_client_base'] = array(
'#type' => 'textfield',
'#title' => t("Database"),
'#default_value' => variable_get('redis_client_base', NULL),
'#description' => t("Redis server database. Default is none, Redis server will autoselect the database 0."),
);
$form['connection']['redis_client_interface'] = array(
'#type' => 'radios',
'#title' => t("Client"),
'#options' => array(
NULL => t("None or automatic"),
'PhpRedis' => t("PhpRedis PHP extension"),
'Predis' => t("Predis PHP library"),
),
'#default_value' => variable_get('redis_client_interface', NULL),
'#description' => t("Redis low level backend."),
);
$form = system_settings_form($form);
// Enforce empty values drop from the $form_state in order to avoid empty
// values saving. Empty values would cause the isset() checks in client
// options to see false positives and fail upon connection.
array_unshift($form['#submit'], 'redis_settings_form_submit_clean_values');
return $form;
}
/**
* Deep clean of $form_state values.
*/
function redis_settings_form_submit_clean_values($form, &$form_state) {
$string_values = array('redis_client_host', 'redis_client_interface');
foreach ($string_values as $name) {
// Empty check is sufficient to verify that the field is indeed empty.
if (empty($form_state['values'][$name])) {
// Using unset() will keep the key in the array, with an associated NULL
// value. While this wouldn't really matter, it's safer to remove it so
// that system_settings_form_submit() won't find it and attempt to save
// it.
$form_state['values'] = array_diff_key($form_state['values'], array($name => NULL));
variable_del($name);
}
}
$numeric_values = array('redis_client_base', 'redis_client_port');
foreach ($numeric_values as $name) {
// Numeric values can be both of NULL or 0 (NULL meaning the value is not
// not set and the client will use the default, while 0 has a business
// meaning and should be kept as is).
if ('0' !== $form_state['values'][$name] && empty($form_state['values'][$name])) {
$form_state['values'] = array_diff_key($form_state['values'], array($name => NULL));
variable_del($name);
} else {
$form_state['values'][$name] = (int)$form_state['values'][$name];
}
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Redis module autoloader.
*/
/**
* Autoloader micro optimization, work with constant as much as we can.
*/
define('REDIS_ROOT', dirname(__FILE__) . '/lib');
/**
* Redis module specific autoloader, compatible with spl_register_autoload().
*/
function redis_autoload($class_name) {
if ('Redis' === substr($class_name, 0, 5)) {
$filename = REDIS_ROOT . '/' . str_replace('_', '/', $class_name) . '.php';
return @include_once $filename;
}
return FALSE;
}
// Register our custom autoloader.
spl_autoload_register('redis_autoload');

View File

@ -0,0 +1,44 @@
name = "Redis"
description = "Provide a module placeholder, for using as dependency for module that needs Redis."
package = Performance
version = VERSION
core = 7.x
configure = admin/config/development/performance/redis
; Drupal Simpletest cannot rely on a real autoloader
files[] = lib/Redis/Tests/AbstractUnitTestCase.php
files[] = lib/Redis/Tests/Admin/VariableTestCase.test
files[] = lib/Redis/Tests/Cache/FixesUnitTestCase.php
files[] = lib/Redis/Tests/Cache/FlushUnitTestCase.php
files[] = lib/Redis/Tests/Cache/CompressedPhpRedisFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/CompressedPhpRedisFlushUnitTestCase.test
files[] = lib/Redis/Tests/Cache/CompressedPhpRedisShardedFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/CompressedPhpRedisShardedFlushUnitTestCase.test
files[] = lib/Redis/Tests/Cache/CompressedPhpRedisShardedWithPipelineFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PhpRedisFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PhpRedisFlushUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PhpRedisShardedFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PhpRedisShardedFlushUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PhpRedisShardedWithPipelineFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PredisFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PredisFlushUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PredisShardedFixesUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PredisShardedFlushUnitTestCase.test
files[] = lib/Redis/Tests/Cache/PredisShardedWithPipelineFixesUnitTestCase.test
files[] = lib/Redis/Tests/Client/ClientUnitTestCase.test
files[] = lib/Redis/Tests/Client/MockFactory.php
files[] = lib/Redis/Tests/Lock/LockingUnitTestCase.php
files[] = lib/Redis/Tests/Lock/PhpRedisLockingUnitTestCase.test
files[] = lib/Redis/Tests/Lock/PredisLockingUnitTestCase.test
files[] = lib/Redis/Tests/Path/PathUnitTestCase.php
files[] = lib/Redis/Tests/Path/PhpRedisPathUnitTestCase.test
files[] = lib/Redis/Tests/Path/PredisPathUnitTestCase.test
files[] = lib/Redis/Tests/Queue/QueueUnitTestCase.php
files[] = lib/Redis/Tests/Queue/PhpRedisQueueUnitTestCase.test
files[] = lib/Redis/Tests/Queue/PredisQueueUnitTestCase.test
; Information added by Drupal.org packaging script on 2017-12-22
version = "7.x-3.17"
core = "7.x"
project = "redis"
datestamp = "1513939095"

View File

@ -0,0 +1,38 @@
<?php
/**
* @file
* Redis install related functions.
*/
/**
* 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 array();
}
$requirements = array();
try {
Redis_Client::getClient();
$requirements['redis'] = array(
'title' => "Redis",
'value' => t("Connected, using the <em>@name</em> client.", array('@name' => Redis_Client::getClientInterfaceName())),
'severity' => REQUIREMENT_OK,
);
} catch (Exception $e) {
$requirements['redis'] = array(
'title' => "Redis",
'value' => t("Not connected."),
'severity' => REQUIREMENT_WARNING,
'description' => t("No Redis client connected. Please ensure that your Redis connection is working properly. If you are not using a Redis server connection you should disable this module."),
);
}
return $requirements;
}

View File

@ -0,0 +1,63 @@
<?php
/**
* @file
* Drupal core lock.inc replacement.
*
* Do not use this file directly, it will be included by the backend specific
* implementation when added to settings.php file.
*
* See README.txt file for details.
*/
// Include our own autoloader to ensure classes to be there.
// We cannot rely on core in case of early bootstrap phases.
require_once dirname(__FILE__) . '/redis.autoload.inc';
/**
* Foo function, keeping it for API consistency (Drupal 7).
*/
function lock_initialize() {}
/**
* Foo function, keeping it for API consistency (Drupal 6).
*/
function lock_init() {}
/**
* Foo function, keeping it for API consistency.
* Some insane people may actually use it.
*/
function _lock_id() {
return Redis_Lock::getBackend()->getLockId();
}
function lock_acquire($name, $timeout = 30.0) {
return Redis_Lock::getBackend()->lockAcquire($name, $timeout);
}
function lock_may_be_available($name) {
return Redis_Lock::getBackend()->lockMayBeAvailable($name);
}
function lock_wait($name, $delay = 30) {
return Redis_Lock::getBackend()->lockWait($name, $delay);
}
function lock_release($name) {
return Redis_Lock::getBackend()->lockRelease($name);
}
function lock_release_all($lock_id = NULL) {
return Redis_Lock::getBackend()->lockReleaseAll($lock_id);
}
// Since D6 doesn't have the drupal_register_shutdown_function
// that is called in lib/Redis/Lock/Backend/Default.php define
// the wrapper here.
if (!function_exists('drupal_register_shutdown_function')) {
function drupal_register_shutdown_function(){
$args = func_get_args();
call_user_func_array('register_shutdown_function', $args);
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* @file
* Redis module.
*
* This file is a placeholder for other modules that need the Redis client for
* something else than lock and cache.
*/
// Include our own autoloader to ensure classes to be there.
// We cannot rely on core in case of early bootstrap phases.
require_once dirname(__FILE__) . '/redis.autoload.inc';
/**
* Implements hook_menu().
*/
function redis_menu() {
$items = array();
$items['admin/config/development/performance/cache'] = array(
'title' => "Cache",
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/config/development/performance/redis'] = array(
'title' => "Redis",
'page callback' => 'drupal_get_form',
'page arguments' => array('redis_settings_form'),
'access arguments' => array('administer site configuration'),
'type' => MENU_LOCAL_TASK,
'file' => 'redis.admin.inc',
);
return $items;
}
/**
* Implements hook_help().
*/
function redis_help($path, $arg) {
switch ($path) {
case 'admin/config/development/performance/redis':
$messages =
'<p>' . t("Redis module is optional if you are using only a cache or lock backend. The full API will be automatically loaded and its configuration will live into the <em>settings.php</em> file. If you access to this screen, it's probably because another contrib module needs it as a dependency for using the Redis client. If you didn't enabled such module, you are strongly advised to disable the Redis module on the module page.") . '</p>' .
'<p>' . t("While Redis client configuration can be changed through the web, if you are using a cache or lock backend they must be set in the <em>settings.php</em> file. Once this done, any modification done using this form will be ignored, and real settings in use will be get at early bootstrap phase, before the configuration system is bootstrapped.") . '</p>';
if (Redis_Client::getClient()) {
$messages .= '<p><strong>' . t("Current connected client uses the <em>@name</em> library.", array('@name' => Redis_Client::getClientInterfaceName())) . '</strong></p>';
}
return $messages;
}
}
/**
* Get Redis client for php-redis extension.
*
* @return \Redis
*/
function phpredis_client_get() {
if ('PhpRedis' !== variable_get('redis_client_interface')) {
throw new \LogicException("Redis is not configured to use the php-redis client");
}
return Redis_Client::getClient();
}
/**
* Get Redis client for Predis library.
*
* @return \Predis\Client
*/
function predis_client_get() {
if ('Predis' !== variable_get('redis_client_interface')) {
throw new \LogicException("Redis is not configured to use the Predis client");
}
return Redis_Client::getClient();
}

View File

@ -0,0 +1,594 @@
<?php
/**
* @file
* Drupal default includes/path.inc file copy which only differs in:
* - drupal_lookup_path() which is the only performance critical.
* - path_*() functions for synchronization.
*/
/**
* Get Redis path lookup backend.
*
* @return Redis_Path_HashLookupInterface
*/
function redis_path_backend_get() {
$hashLookup = &drupal_static(__FUNCTION__, null);
if (null === $hashLookup) {
try {
$className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_PATH);
$hashLookup = new $className(Redis_Client::getClient(), 'path', Redis_Client::getDefaultPrefix('path'));
} catch (Exception $e) {
$hashLookup = new Redis_Path_NullHashLookup();
}
}
return $hashLookup;
}
/**
* Initialize the $_GET['q'] variable to the proper normal path.
*/
function drupal_path_initialize() {
// Ensure $_GET['q'] is set before calling drupal_normal_path(), to support
// path caching with hook_url_inbound_alter().
if (empty($_GET['q'])) {
$_GET['q'] = variable_get('site_frontpage', 'node');
}
$_GET['q'] = drupal_get_normal_path($_GET['q']);
}
/**
* Given an alias, return its Drupal system URL if one exists. Given a Drupal
* system URL return one of its aliases if such a one exists. Otherwise,
* return FALSE.
*
* @param $action
* One of the following values:
* - wipe: delete the alias cache.
* - alias: return an alias for a given Drupal system path (if one exists).
* - source: return the Drupal system URL for a path alias (if one exists).
* @param $path
* The path to investigate for corresponding aliases or system URLs.
* @param $path_language
* Optional language code to search the path with. Defaults to the page language.
* If there's no path defined for that language it will search paths without
* language.
*
* @return
* Either a Drupal system path, an aliased path, or FALSE if no path was
* found.
*/
function drupal_lookup_path($action, $path = '', $path_language = NULL) {
global $language_url;
static $cache, $denyAdmin;
if (null === $cache) {
$cache = array('whitelist' => variable_get('path_alias_whitelist'));
if (null === $cache['whitelist']) {
$cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
}
$denyAdmin = (bool)variable_get('path_alias_admin_blacklist', true);
}
// If no language is explicitly specified we default to the current URL
// language. If we used a language different from the one conveyed by the
// requested URL, we might end up being unable to check if there is a path
// alias matching the URL path.
if (!$path_language = $path_language ? $path_language : $language_url->language) {
$path_language = LANGUAGE_NONE;
}
if (!empty($path) && isset($cache[$path_language][$action][$path])) {
return $cache[$path_language][$action][$path];
}
if (!empty($path)) {
$path = strtolower(trim($path));
}
$ret = null;
$hashLookup = redis_path_backend_get();
switch ($action) {
case 'wipe':
$cache = array();
$cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
break;
case 'alias':
if (empty($path)) {
return false;
}
// Check the path whitelist, if the top_level part before the first /
// is not in the list, then there is no need to do anything further,
// it is not in the database.
if (!isset($cache['whitelist'][strtok($path, '/')])) {
return false;
}
// Deny admin paths.
if ($denyAdmin && path_is_admin($path)) {
return false;
}
$ret = $hashLookup->lookupAlias($path, $path_language);
if (null === $ret) {
// Original Drupal algorithm.
// This will also update the $path_language variable so Redis will store
// the right language (keeps track of LANGUAGE_NONE or specific language
// so that default fallback behavior is the same that core).
if ($path_language == LANGUAGE_NONE) {
list ($ret, $path_language) = db_query_range("SELECT alias, language FROM {url_alias} WHERE source = :source AND language = :language ORDER BY pid DESC", 0, 1, array(
':source' => $path,
':language' => $path_language,
))->fetch(PDO::FETCH_NUM);
} else if ($path_language > LANGUAGE_NONE) {
list ($ret, $path_language) = db_query_range("SELECT alias, language FROM {url_alias} WHERE source = :source AND language IN (:language) ORDER BY language DESC, pid DESC", 0, 1, array(
':source' => $path,
':language' => array($path_language, LANGUAGE_NONE),
))->fetch(PDO::FETCH_NUM);
} else {
list ($ret, $path_language) = db_query_range("SELECT alias, language FROM {url_alias} WHERE source = :source AND language IN (:language) ORDER BY language ASC, pid DESC", 0, 1, array(
':source' => $path,
':language' => array($path_language, LANGUAGE_NONE),
))->fetch(PDO::FETCH_NUM);
}
// Getting here with a value means we need to cache it
if (empty($ret)) {
$ret = false;
}
$hashLookup->saveAlias($path, $ret, $path_language);
}
$cache[$path_language]['alias'][$path] = $ret;
$cache[$path_language]['source'][$ret] = $path;
break;
case 'source':
if (empty($path)) {
return false;
}
// Even thought given entry is an alias, if it conflicts with an
// existing admin path just deny any lookup.
if ($denyAdmin && path_is_admin($path)) {
return false;
}
$ret = $hashLookup->lookupSource($path, $path_language);
if (null === $ret) {
// Original Drupal algorithm.
// This will also update the $path_language variable so Redis will store
// the right language (keeps track of LANGUAGE_NONE or specific language
// so that default fallback behavior is the same that core).
if ($path_language == LANGUAGE_NONE) {
list ($ret, $path_language) = db_query_range("SELECT source, language FROM {url_alias} WHERE alias = :alias AND language = :language ORDER BY pid DESC", 0, 1, array(
':alias' => $path,
':language' => LANGUAGE_NONE,
))->fetch(PDO::FETCH_NUM);
} else if ($path_language > LANGUAGE_NONE) {
list ($ret, $path_language) = db_query_range("SELECT source, language FROM {url_alias} WHERE alias = :alias AND language IN (:language) ORDER BY language DESC, pid DESC", 0, 1, array(
':alias' => $path,
':language' => array($path_language, LANGUAGE_NONE),
))->fetch(PDO::FETCH_NUM);
} else {
list ($ret, $path_language) = db_query_range("SELECT source, language FROM {url_alias} WHERE alias = :alias AND language IN (:language) ORDER BY language ASC, pid DESC", 0, 1, array(
':alias' => $path,
':language' => array($path_language, LANGUAGE_NONE),
))->fetch(PDO::FETCH_NUM);
}
// Getting here with a value means we need to cache it
if (empty($ret)) {
$ret = false;
} else {
$ret = strtolower(trim($ret));
}
$hashLookup->saveAlias($ret, $path, $path_language);
}
$cache[$path_language]['alias'][$ret] = $path;
$cache[$path_language]['source'][$path] = $ret;
break;
}
return $ret;
}
/**
* Cache system paths for a page.
*
* Cache an array of the system paths available on each page. We assume
* that aliases will be needed for the majority of these paths during
* subsequent requests, and load them in a single query during
* drupal_lookup_path().
*/
function drupal_cache_system_paths() {
// Check if the system paths for this page were loaded from cache in this
// request to avoid writing to cache on every request.
$cache = &drupal_static('drupal_lookup_path', array());
if (empty($cache['system_paths']) && !empty($cache['map'])) {
// Generate a cache ID (cid) specifically for this page.
$cid = current_path();
// The static $map array used by drupal_lookup_path() includes all
// system paths for the page request.
if ($paths = current($cache['map'])) {
$data = array_keys($paths);
$expire = REQUEST_TIME + (60 * 60 * 24);
cache_set($cid, $data, 'cache_path', $expire);
}
}
}
/**
* Given an internal Drupal path, return the alias set by the administrator.
*
* If no path is provided, the function will return the alias of the current
* page.
*
* @param $path
* An internal Drupal path.
* @param $path_language
* An optional language code to look up the path in.
*
* @return
* An aliased path if one was found, or the original path if no alias was
* found.
*/
function drupal_get_path_alias($path = NULL, $path_language = NULL) {
// If no path is specified, use the current page's path.
if ($path == NULL) {
$path = $_GET['q'];
}
$result = $path;
if ($alias = drupal_lookup_path('alias', $path, $path_language)) {
$result = $alias;
}
return $result;
}
/**
* Given a path alias, return the internal path it represents.
*
* @param $path
* A Drupal path alias.
* @param $path_language
* An optional language code to look up the path in.
*
* @return
* The internal path represented by the alias, or the original alias if no
* internal path was found.
*/
function drupal_get_normal_path($path, $path_language = NULL) {
$original_path = $path;
// Lookup the path alias first.
if ($source = drupal_lookup_path('source', $path, $path_language)) {
$path = $source;
}
// Allow other modules to alter the inbound URL. We cannot use drupal_alter()
// here because we need to run hook_url_inbound_alter() in the reverse order
// of hook_url_outbound_alter().
foreach (array_reverse(module_implements('url_inbound_alter')) as $module) {
$function = $module . '_url_inbound_alter';
$function($path, $original_path, $path_language);
}
return $path;
}
/**
* Check if the current page is the front page.
*
* @return
* Boolean value: TRUE if the current page is the front page; FALSE if otherwise.
*/
function drupal_is_front_page() {
// Use the advanced drupal_static() pattern, since this is called very often.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['is_front_page'] = &drupal_static(__FUNCTION__);
}
$is_front_page = &$drupal_static_fast['is_front_page'];
if (!isset($is_front_page)) {
// As drupal_path_initialize updates $_GET['q'] with the 'site_frontpage' path,
// we can check it against the 'site_frontpage' variable.
$is_front_page = ($_GET['q'] == variable_get('site_frontpage', 'node'));
}
return $is_front_page;
}
/**
* Check if a path matches any pattern in a set of patterns.
*
* @param $path
* The path to match.
* @param $patterns
* String containing a set of patterns separated by \n, \r or \r\n.
*
* @return
* Boolean value: TRUE if the path matches a pattern, FALSE otherwise.
*/
function drupal_match_path($path, $patterns) {
$regexps = &drupal_static(__FUNCTION__);
if (!isset($regexps[$patterns])) {
// Convert path settings to a regular expression.
// Therefore replace newlines with a logical or, /* with asterisks and the <front> with the frontpage.
$to_replace = array(
'/(\r\n?|\n)/', // newlines
'/\\\\\*/', // asterisks
'/(^|\|)\\\\<front\\\\>($|\|)/' // <front>
);
$replacements = array(
'|',
'.*',
'\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'
);
$patterns_quoted = preg_quote($patterns, '/');
$regexps[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
}
return (bool)preg_match($regexps[$patterns], $path);
}
/**
* Return the current URL path of the page being viewed.
*
* Examples:
* - http://example.com/node/306 returns "node/306".
* - http://example.com/drupalfolder/node/306 returns "node/306" while
* base_path() returns "/drupalfolder/".
* - http://example.com/path/alias (which is a path alias for node/306) returns
* "node/306" as opposed to the path alias.
*
* This function is not available in hook_boot() so use $_GET['q'] instead.
* However, be careful when doing that because in the case of Example #3
* $_GET['q'] will contain "path/alias". If "node/306" is needed, calling
* drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
*
* @return
* The current Drupal URL path.
*
* @see request_path()
*/
function current_path() {
return $_GET['q'];
}
/**
* Rebuild the path alias white list.
*
* @param $source
* An optional system path for which an alias is being inserted.
*
* @return
* An array containing a white list of path aliases.
*/
function drupal_path_alias_whitelist_rebuild($source = NULL) {
// When paths are inserted, only rebuild the whitelist if the system path
// has a top level component which is not already in the whitelist.
if (!empty($source)) {
$whitelist = variable_get('path_alias_whitelist', NULL);
if (isset($whitelist[strtok($source, '/')])) {
return $whitelist;
}
}
// For each alias in the database, get the top level component of the system
// path it corresponds to. This is the portion of the path before the first
// '/', if present, otherwise the whole path itself.
$whitelist = array();
$result = db_query("SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}");
foreach ($result as $row) {
$whitelist[$row->path] = TRUE;
}
variable_set('path_alias_whitelist', $whitelist);
return $whitelist;
}
/**
* Fetches a specific URL alias from the database.
*
* @param $conditions
* A string representing the source, a number representing the pid, or an
* array of query conditions.
*
* @return
* FALSE if no alias was found or an associative array containing the
* following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: Unique path alias identifier.
* - language: The language of the alias.
*/
function path_load($conditions) {
if (is_numeric($conditions)) {
$conditions = array('pid' => $conditions);
}
elseif (is_string($conditions)) {
$conditions = array('source' => $conditions);
}
elseif (!is_array($conditions)) {
return FALSE;
}
$select = db_select('url_alias');
foreach ($conditions as $field => $value) {
$select->condition($field, $value);
}
return $select
->fields('url_alias')
->execute()
->fetchAssoc();
}
/**
* Save a path alias to the database.
*
* @param $path
* An associative array containing the following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: (optional) Unique path alias identifier.
* - language: (optional) The language of the alias.
*/
function path_save(&$path) {
$path += array('language' => LANGUAGE_NONE);
// Load the stored alias, if any.
if (!empty($path['pid']) && !isset($path['original'])) {
$path['original'] = path_load($path['pid']);
}
if (empty($path['pid'])) {
drupal_write_record('url_alias', $path);
module_invoke_all('path_insert', $path);
}
else {
drupal_write_record('url_alias', $path, array('pid'));
module_invoke_all('path_update', $path);
}
if (!empty($path['original'])) {
redis_path_backend_get()->deleteAlias($path['original']['source'], $path['original']['alias'], $path['original']['language']);
}
redis_path_backend_get()->saveAlias($path['source'], $path['alias'], $path['language']);
// Clear internal properties.
unset($path['original']);
// Clear the static alias cache.
drupal_clear_path_cache($path['source']);
}
/**
* Delete a URL alias.
*
* @param $criteria
* A number representing the pid or an array of criteria.
*/
function path_delete($criteria) {
if (!is_array($criteria)) {
$criteria = array('pid' => $criteria);
}
$path = path_load($criteria);
$query = db_delete('url_alias');
foreach ($criteria as $field => $value) {
$query->condition($field, $value);
}
$query->execute();
module_invoke_all('path_delete', $path);
redis_path_backend_get()->deleteAlias($path['source'], $path['alias'], $path['language']);
drupal_clear_path_cache($path['source']);
}
/**
* Determines whether a path is in the administrative section of the site.
*
* By default, paths are considered to be non-administrative. If a path does
* not match any of the patterns in path_get_admin_paths(), or if it matches
* both administrative and non-administrative patterns, it is considered
* non-administrative.
*
* @param $path
* A Drupal path.
*
* @return
* TRUE if the path is administrative, FALSE otherwise.
*
* @see path_get_admin_paths()
* @see hook_admin_paths()
* @see hook_admin_paths_alter()
*/
function path_is_admin($path) {
$path_map = &drupal_static(__FUNCTION__);
if (!isset($path_map['admin'][$path])) {
$patterns = path_get_admin_paths();
$path_map['admin'][$path] = drupal_match_path($path, $patterns['admin']);
$path_map['non_admin'][$path] = drupal_match_path($path, $patterns['non_admin']);
}
return $path_map['admin'][$path] && !$path_map['non_admin'][$path];
}
/**
* Gets a list of administrative and non-administrative paths.
*
* @return array
* An associative array containing the following keys:
* 'admin': An array of administrative paths and regular expressions
* in a format suitable for drupal_match_path().
* 'non_admin': An array of non-administrative paths and regular expressions.
*
* @see hook_admin_paths()
* @see hook_admin_paths_alter()
*/
function path_get_admin_paths() {
$patterns = &drupal_static(__FUNCTION__);
if (!isset($patterns)) {
$paths = module_invoke_all('admin_paths');
drupal_alter('admin_paths', $paths);
// Combine all admin paths into one array, and likewise for non-admin paths,
// for easier handling.
$patterns = array();
$patterns['admin'] = array();
$patterns['non_admin'] = array();
foreach ($paths as $path => $enabled) {
if ($enabled) {
$patterns['admin'][] = $path;
}
else {
$patterns['non_admin'][] = $path;
}
}
$patterns['admin'] = implode("\n", $patterns['admin']);
$patterns['non_admin'] = implode("\n", $patterns['non_admin']);
}
return $patterns;
}
/**
* Checks a path exists and the current user has access to it.
*
* @param $path
* The path to check.
* @param $dynamic_allowed
* Whether paths with menu wildcards (like user/%) should be allowed.
*
* @return
* TRUE if it is a valid path AND the current user has access permission,
* FALSE otherwise.
*/
function drupal_valid_path($path, $dynamic_allowed = FALSE) {
global $menu_admin;
// We indicate that a menu administrator is running the menu access check.
$menu_admin = TRUE;
if ($path == '<front>' || url_is_external($path)) {
$item = array('access' => TRUE);
}
elseif ($dynamic_allowed && preg_match('/\/\%/', $path)) {
// Path is dynamic (ie 'user/%'), so check directly against menu_router table.
if ($item = db_query("SELECT * FROM {menu_router} where path = :path", array(':path' => $path))->fetchAssoc()) {
$item['link_path'] = $item['path'];
$item['link_title'] = $item['title'];
$item['external'] = FALSE;
$item['options'] = '';
_menu_link_translate($item);
}
}
else {
$item = menu_get_item($path);
}
$menu_admin = FALSE;
return $item && $item['access'];
}
/**
* Clear the path cache.
*
* @param $source
* An optional system path for which an alias is being changed.
*/
function drupal_clear_path_cache($source = NULL) {
// Clear the drupal_lookup_path() static cache.
drupal_static_reset('drupal_lookup_path');
drupal_path_alias_whitelist_rebuild($source);
}