From 02c2d6ea580d4e0a77d764ce78a6af05f57e167c Mon Sep 17 00:00:00 2001 From: Tim Steiner <tsteiner2@unl.edu> Date: Fri, 27 Jan 2012 00:08:22 +0000 Subject: [PATCH] [gh-284] Merging from testing into staging -c 1405 git-svn-id: file:///tmp/wdn_thm_drupal/branches/drupal-7.x/staging@1413 20a16fea-79d4-4915-8869-1ea9d5ebf173 --- sites/all/modules/memcache/LICENSE.txt | 601 ++++++++++-------- sites/all/modules/memcache/README.txt | 81 ++- sites/all/modules/memcache/dmemcache.inc | 249 ++++++-- .../modules/memcache/memcache-lock-code.inc | 161 +++++ sites/all/modules/memcache/memcache-lock.inc | 122 +--- sites/all/modules/memcache/memcache.inc | 323 +++++++--- sites/all/modules/memcache/memcache.info | 14 +- sites/all/modules/memcache/memcache.install | 52 ++ sites/all/modules/memcache/memcache.module | 6 + sites/all/modules/memcache/memcache.test | 541 ---------------- .../memcache_admin/memcache_admin.info | 9 +- .../memcache_admin/memcache_admin.install | 13 + .../memcache_admin/memcache_admin.module | 460 +++++++++++--- .../modules/memcache/tests/memcache-lock.test | 71 +++ .../memcache/tests/memcache-session.test | 226 +++++++ .../all/modules/memcache/tests/memcache.test | 572 +++++++++++++++++ .../all/modules/memcache/tests/memcache6.test | 343 ++++++++++ .../modules/memcache/tests/memcache_test.info | 12 + .../memcache/tests/memcache_test.module | 129 ++++ .../{ => unstable}/memcache-session.inc | 7 +- 20 files changed, 2763 insertions(+), 1229 deletions(-) create mode 100644 sites/all/modules/memcache/memcache-lock-code.inc create mode 100644 sites/all/modules/memcache/memcache.install create mode 100644 sites/all/modules/memcache/memcache.module delete mode 100644 sites/all/modules/memcache/memcache.test create mode 100644 sites/all/modules/memcache/memcache_admin/memcache_admin.install create mode 100644 sites/all/modules/memcache/tests/memcache-lock.test create mode 100644 sites/all/modules/memcache/tests/memcache-session.test create mode 100644 sites/all/modules/memcache/tests/memcache.test create mode 100644 sites/all/modules/memcache/tests/memcache6.test create mode 100644 sites/all/modules/memcache/tests/memcache_test.info create mode 100644 sites/all/modules/memcache/tests/memcache_test.module rename sites/all/modules/memcache/{ => unstable}/memcache-session.inc (99%) diff --git a/sites/all/modules/memcache/LICENSE.txt b/sites/all/modules/memcache/LICENSE.txt index 2c095c8d..d159169d 100644 --- a/sites/all/modules/memcache/LICENSE.txt +++ b/sites/all/modules/memcache/LICENSE.txt @@ -1,274 +1,339 @@ -GNU GENERAL PUBLIC LICENSE - - Version 2, June 1991 - -Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave, -Cambridge, MA 02139, 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 Library 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. + 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.) +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 +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 +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. diff --git a/sites/all/modules/memcache/README.txt b/sites/all/modules/memcache/README.txt index 4efd86d3..59298384 100644 --- a/sites/all/modules/memcache/README.txt +++ b/sites/all/modules/memcache/README.txt @@ -4,49 +4,35 @@ - PHP 5.1 or greater - Availability of a memcached daemon: http://memcached.org/ - One of the two PECL memcache packages: - - http://pecl.php.net/package/memcache - - http://pecl.php.net/package/memcached (recommended + - http://pecl.php.net/package/memcache (recommended): + - http://pecl.php.net/package/memcached ## INSTALLATION ## These are the broad steps you need to take in order to use this software. Order is important. -1. Install the memcached binaries on your server. See - -http://www.lullabot.com/articles/how_install_memcache_debian_etch - -2. Install the PECL memcache extension for PHP. This must be version 2.2.1 or - higher or you will experience errors. -3. Put your site into offline mode. -4. Download and install the memcache module. -5. If you have previously been running the memcache module, run update.php. -6. Start at least one instance of memcached on your server. -7. Edit settings.php to configure the servers, clusters and bins that memcache - is supposed to use. -8. Edit settings.php to include cache.inc, memcache.inc. For example: - include_once('./includes/cache.inc'); - include_once('./sites/all/modules/memcache/memcache.inc'); -9. Edit settings.php to make memcache the default caching class: - $conf['cache_default_class'] = 'MemCacheDrupal'; -9. Bring your site back online. + 1. Install the memcached binaries on your server. See for instance: + http://www.lullabot.com/articles/how_install_memcache_debian_etch + 2. Install the PECL memcache extension for PHP. This must be version 2.2.1 or + higher or you will experience errors. + 3. Put your site into offline mode. + 4. Download and install the memcache module. + 5. If you have previously been running the memcache module, run update.php. + 6. Start at least one instance of memcached on your server. + 7. Edit settings.php to configure the servers, clusters and bins that memcache + is supposed to use. + 8. Edit settings.php to make memcache the default cache class, for example: + $conf['cache_backends'][] = 'sites/all/modules/memcache/memcache.inc'; + $conf['cache_default_class'] = 'MemCacheDrupal'; + 9. Make sure the following line also exists, to ensure that the special + cache_form bin is assigned to non-volatile storage: + $conf['cache_class_cache_form'] = 'DrupalDatabaseCache'; +10. Bring your site back online. For instructions on 1 and 2 above, please see the INSTALLATION.txt file that comes with the memcache module download. -http://www.lullabot.com/files/memcache-inc.png - -In Drupal 7+, you no longer should set cache_inc in settings.php. Instead, you -will have to manually include 'cache.inc' and 'memcache.inc', then update $conf -to tell Drupal to default to memcache for caching: - - // the path to the core cache file - include_once('./includes/cache.inc'); - // the path to the memcache cache file - include_once('./sites/all/modules/memcache/memcache.inc'); - // make MemCacheDrupal the default cache class - $conf['cache_default_class'] = 'MemCacheDrupal'; - ## SERVERS ## If you want the simple version, you can start one default memcache instance on @@ -79,7 +65,7 @@ The bin/cluster/server model can be described as follows: - Servers are memcached instances identified by host:port. - Bins are groups of data that get cached together and map 1:1 to the $table - param in cache_set(). Examples from Drupal core are cache_filter, + parameter of cache_set(). Examples from Drupal core are cache_filter, cache_menu. The default is 'cache'. - Clusters are groups of servers that act as a memory pool. @@ -106,11 +92,13 @@ $conf = array( Here is an example configuration that has two clusters, 'default' and 'cluster2'. Five memcached instances are divided up between the two -clusters. 'cache_filter' and 'cache_menu' bins goe to 'cluster2'. All other +clusters. 'cache_filter' and 'cache_menu' bins go to 'cluster2'. All other bins go to 'default'. -include_once('./includes/cache.inc'); -include_once('./sites/all/modules/memcache/memcache.inc'); +$conf['cache_backends'][] = 'sites/all/modules/memcache/memcache.inc'; +$conf['cache_default_class'] = 'MemCacheDrupal'; +// The 'cache_form' bin must be assigned no non-volatile storage. +$conf['cache_class_cache_form'] = 'DrupalDatabaseCache'; $conf = array( 'cache_default_class' = 'MemCacheDrupal', 'memcache_servers' => array('localhost:11211' => 'default', @@ -123,6 +111,7 @@ $conf = array( 'cache_filter' => 'cluster2', 'cache_menu' => 'cluster2'), ); + ## PREFIXING ## If you want to have multiple Drupal installations share memcached instances, @@ -136,13 +125,16 @@ $conf = array( ## SESSIONS ## +NOTE: Session.inc is not yet ported to Drupal 7 and is not recommended for use +in production.. + Here is a sample config that uses memcache for sessions. Note you MUST have a session and a users server set up for memcached sessions to work. -NOTE: Session.inc is not yet ported to Drupal 7. - -include_once('./includes/cache.inc'); -include_once('./sites/all/modules/memcache/memcache.inc'); +$conf['cache_backends'][] = 'sites/all/modules/memcache/memcache.inc'; +$conf['cache_default_class'] = 'MemCacheDrupal'; +// The 'cache_form' bin must be assigned no non-volatile storage. +$conf['cache_class_cache_form'] = 'DrupalDatabaseCache'; $conf = array( 'cache_default_class' = 'MemCacheDrupal', 'session_inc' => './sites/all/modules/memcache/memcache-session.inc', @@ -183,12 +175,11 @@ See http://drupal.org/node/273824 A module offering a UI for memcache is included. It provides stats, a way to clear the cache, and an interface to organize servers/bins/clusters. - ## Memcached PECL Extension Support -We also now support the Memcached PECL extension. If you install this extension, -it will be used by default. This new extension backends to libmemcached and -allows you to use some of the newer advanced features in memcached 1.4. +We also now support the Memcached PECL extension. This new extension backends +to libmemcached and allows you to use some of the newer advanced features in +memcached 1.4. NOTE: It is important to realize that the memcache php.ini options do not impact the memcached extension, this new extension doesn't read in options that way. diff --git a/sites/all/modules/memcache/dmemcache.inc b/sites/all/modules/memcache/dmemcache.inc index df3c85dc..5b0629a4 100644 --- a/sites/all/modules/memcache/dmemcache.inc +++ b/sites/all/modules/memcache/dmemcache.inc @@ -37,7 +37,7 @@ function dmemcache_set($key, $value, $exp = 0, $bin = 'cache', $mc = NULL) { $full_key = dmemcache_key($key, $bin); $_memcache_statistics[] = array('set', $bin, $full_key, ''); if ($mc || ($mc = dmemcache_object($bin))) { - if (class_exists('Memcached')) { + if ($mc instanceof Memcached) { return $mc->set($full_key, $value, $exp); } else { @@ -73,7 +73,7 @@ function dmemcache_add($key, $value, $exp = 0, $bin = 'cache', $mc = NULL, $flag $full_key = dmemcache_key($key, $bin); $_memcache_statistics[] = array('add', $bin, $full_key, ''); if ($mc || ($mc = dmemcache_object($bin))) { - if (class_exists('Memcached')) { + if ($mc instanceof Memcached) { return $mc->add($full_key, $value, $exp); } else { @@ -93,17 +93,25 @@ function dmemcache_add($key, $value, $exp = 0, $bin = 'cache', $mc = NULL, $flag */ function dmemcache_get($key, $bin = 'cache', $mc = NULL) { global $_memcache_statistics; + $result = FALSE; $full_key = dmemcache_key($key, $bin); $statistics = array('get', $bin, $full_key); $success = '0'; - if ($mc || ($mc = dmemcache_object($bin))) { - $result = $mc->get($full_key); - if ($result) { - $success = '1'; + if ($mc || $mc = dmemcache_object($bin)) { + $track_errors = ini_set('track_errors', '1'); + $php_errormsg = ''; + + $result = @$mc->get($full_key); + $statistics[] = (bool) $result; + $_memcache_statistics[] = $statistics; + + if (!empty($php_errormsg)) { + register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_get: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING); + $php_errormsg = ''; } + ini_set('track_errors', $track_errors); } - $statistics[] = $success; - $_memcache_statistics[] = $statistics; + return $result; } @@ -122,22 +130,43 @@ function dmemcache_get_multi($keys, $bin = 'cache', $mc = NULL) { foreach ($keys as $key => $cid) { $full_key = dmemcache_key($cid, $bin); $statistics[$full_key] = array('getMulti', $bin, $full_key); - $full_keys[] = $full_key; + $full_keys[$cid] = $full_key; } $results = array(); if ($mc || ($mc = dmemcache_object($bin))) { - if (class_exists('Memcached')) { + if ($mc instanceof Memcached) { $results = $mc->getMulti($full_keys); } - else { - $results = $mc->get($full_keys); + elseif ($mc instanceof Memcache) { + $track_errors = ini_set('track_errors', '1'); + $php_errormsg = ''; + + $results = @$mc->get($full_keys); + + if (!empty($php_errormsg)) { + register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_get_multi: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING); + $php_errormsg = ''; + } + ini_set('track_errors', $track_errors); } } foreach ($statistics as $key => $values) { $values[] = isset($results[$key]) ? '1': '0'; $_memcache_statistics[] = $values; } - return $results; + + // If $results is FALSE, convert it to an empty array. + if (!$results) { + $results = array(); + } + + // Convert the full keys back to the cid. + $cid_results = array(); + $cid_lookup = array_flip($full_keys); + foreach ($results as $key => $value) { + $cid_results[$cid_lookup[$key]] = $value; + } + return $cid_results; } /** @@ -153,7 +182,7 @@ function dmemcache_delete($key, $bin = 'cache', $mc = NULL) { $full_key = dmemcache_key($key, $bin); $_memcache_statistics[] = array('delete', $bin, $full_key, ''); if ($mc || ($mc = dmemcache_object($bin))) { - return $mc->delete($full_key); + return $mc->delete($full_key, 0); } return FALSE; } @@ -176,40 +205,62 @@ function dmemcache_flush($bin = 'cache', $mc = NULL) { } } -function dmemcache_stats($bin = 'cache', $type = '') { - // resolve requests for 'default' type to '' - if ($type == 'default') { - $type = ''; - } - // resolve requests for 'default' bin to 'cache'. - if ($bin == 'default') { - $bin = 'cache'; - } - if ($mc = dmemcache_object($bin)) { - if (class_exists('Memcached')) { - return $mc->getStats(); - } - // The PHP Memcache extension 3.x version throws an error if the stats - // type is NULL or not in {reset, malloc, slabs, cachedump, items, sizes}. - // If $type is 'default', then no parameter should be passed to the - // Memcache memcache_get_extended_stats() function. - if ($type == 'default' || $type == '') { - if (class_exists('Memcached')) { - return $mc->getStats(); - } - else if (class_exists('Memcache')) { - return $mc->getExtendedStats(); +function dmemcache_stats($stats_bin = 'cache', $stats_type = 'default', $aggregate = FALSE) { + $memcache_bins = variable_get('memcache_bins', array('cache' => 'default')); + // The stats_type can be over-loaded with an integer slab id, if doing a + // cachedump. We know we're doing a cachedump if $slab is non-zero. + $slab = (int)$stats_type; + + foreach ($memcache_bins as $bin => $target) { + if ($stats_bin == $bin) { + if ($mc = dmemcache_object($bin)) { + if ($mc instanceof Memcached) { + $stats[$bin] = $mc->getStats(); + } + // The PHP Memcache extension 3.x version throws an error if the stats + // type is NULL or not in {reset, malloc, slabs, cachedump, items, + // sizes}. If $stats_type is 'default', then no parameter should be + // passed to the Memcache memcache_get_extended_stats() function. + else if ($mc instanceof Memcache) { + if ($stats_type == 'default' || $stats_type == '') { + $stats[$bin] = $mc->getExtendedStats(); + } + // If $slab isn't zero, then we are dumping the contents of a + // specific cache slab. + else if (!empty($slab)) { + $stats[$bin] = $mc->getStats('cachedump', $slab); + } + else { + $stats[$bin] = $mc->getExtendedStats($stats_type); + } + } } } - else { - if (class_exists('Memcached')) { - return $mc->getStats(); - } - else if (class_exists('Memcache')) { - return $mc->getExtendedStats($type); + } + // Optionally calculate a sum-total for all servers in the current bin. + if ($aggregate) { + // Some variables don't logically aggregate. + $no_aggregate = array('pid', 'time', 'version', 'pointer_size', 'accepting_conns', 'listen_disabled_num'); + foreach($stats as $bin => $servers) { + if (is_array($servers)) { + foreach ($servers as $server) { + if (is_array($server)) { + foreach ($server as $key => $value) { + if (!in_array($key, $no_aggregate)) { + if (isset($stats[$bin]['total'][$key])) { + $stats[$bin]['total'][$key] += $value; + } + else { + $stats[$bin]['total'][$key] = $value; + } + } + } + } + } } } } + return $stats; } /** @@ -226,7 +277,33 @@ function dmemcache_stats($bin = 'cache', $type = '') { * @return an Memcache object or FALSE. */ function dmemcache_object($bin = NULL, $flush = FALSE) { - static $memcacheCache = array(), $memcache_servers, $memcache_bins; + static $extension, $memcacheCache = array(), $memcache_servers, $memcache_bins, $memcache_persistent, $failed_connection_cache; + + if (!isset($extension)) { + // If an extension is specified in settings.php, use that when available. + $preferred = variable_get('memcache_extension', NULL); + if (isset($preferred) && class_exists($preferred)) { + $extension = $preferred; + } + // If no extension is set, default to Memcache. + // The Memcached extension has some features that the older extension lacks + // but also an unfixed bug that affects cache clears. + // @see http://pecl.php.net/bugs/bug.php?id=16829 + elseif (class_exists('Memcache')) { + $extension = 'Memcache'; + } + elseif (class_exists('Memcached')) { + $extension = 'Memcached'; + } + + // Indicate whether to connect to memcache using a persistent connection. + // Note: this only affects the Memcache PECL extension, and does not + // affect the Memcached PECL extension. For a detailed explanation see: + // http://drupal.org/node/822316#comment-4427676 + if (!isset($memcache_persistent)) { + $memcache_persistent = variable_get('memcache_persistent', FALSE); + } + } if ($flush) { foreach ($memcacheCache as $cluster) { @@ -257,7 +334,7 @@ function dmemcache_object($bin = NULL, $flush = FALSE) { } else { // Create a new Memcache object. Each cluster gets its own Memcache object. - if (class_exists('Memcached')) { + if ($extension == 'Memcached') { $memcache = new Memcached; $default_opts = array( Memcached::OPT_COMPRESSION => FALSE, @@ -266,12 +343,14 @@ function dmemcache_object($bin = NULL, $flush = FALSE) { foreach ($default_opts as $key => $value) { $memcache->setOption($key, $value); } + // See README.txt for setting custom Memcache options when using the + // memcached PECL extension. $memconf = variable_get('memcache_options', array()); foreach ($memconf as $key => $value) { $memcache->setOption($key, $value); } } - else if (class_exists('Memcache')) { + elseif ($extension == 'Memcache') { $memcache = new Memcache; } else { @@ -283,21 +362,59 @@ function dmemcache_object($bin = NULL, $flush = FALSE) { // Link all the servers to this cluster. foreach ($memcache_servers as $s => $c) { - if ($c == $cluster) { + if ($c == $cluster && !isset($failed_connection_cache[$s])) { list($host, $port) = explode(':', $s); - // This is a server that belongs to this cluster. - if (!class_exists('Memcached') && !$init) { - // If using PECL memcache extension, use ->connect for first server - if ($memcache->connect($host, $port)) { - $init = TRUE; - } + // Support unix sockets in the format 'unix:///path/to/socket'. + if ($host == 'unix') { + // When using unix sockets use the full path for $host. + $host = $s; + // Port is always 0 for unix sockets. + $port = 0; } - else { - if ($memcache->addServer($host, $port) && !$init) { - $init = TRUE; + + // Using the Memcache PECL extension. + if ($memcache instanceof Memcache) { + // When using the PECL memcache extension, we must use ->(p)connect + // for the first connection. + if (!$init) { + $track_errors = ini_set('track_errors', '1'); + $php_errormsg = ''; + + if ($memcache_persistent && @$memcache->pconnect($host, $port)) { + $init = TRUE; + } + elseif (!$memcache_persistent && @$memcache->connect($host, $port)) { + $init = TRUE; + } + + if (!empty($php_errormsg)) { + register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_object: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING); + $php_errormsg = ''; + } + ini_set('track_errors', $track_errors); + } + else { + $memcache->addServer($host, $port, $memcache_persistent); } } + else if ($memcache->addServer($host, $port) && !$init) { + $init = TRUE; + } + + if (!$init) { + // Ensure we use an available t() function. + $t = get_t(); + $error_msg = $t( + 'Failed to connect to memcache server: %server', + array('%server' => $s) + ); + // We can't use watchdog because this happens in a bootstrap phase + // where watchdog is non-functional. Thus use trigger_error() to + // start drupal_error_handler(). + trigger_error($error_msg, E_USER_ERROR); + $failed_connection_cache[$s] = FALSE; + } } } @@ -321,18 +438,14 @@ function dmemcache_object($bin = NULL, $flush = FALSE) { } function dmemcache_key($key, $bin = 'cache') { - static $prefix; - // memcache_key_prefix can be set in settings.php to support site namespaces - // in a multisite environment. - if (empty($prefix)) { - if ($prefix = variable_get('memcache_key_prefix', '')) { - $prefix .= '-'; - } - // When simpletest is running, emulate the simpletest database prefix here - // to avoid the child site setting cache entries in the parent site. - if (isset($GLOBALS['drupal_test_info']['test_run_id'])) { - $prefix .= $GLOBALS['drupal_test_info']['test_run_id']; - } + $prefix = ''; + if ($prefix = variable_get('memcache_key_prefix', '')) { + $prefix .= '-'; + } + // When simpletest is running, emulate the simpletest database prefix here + // to avoid the child site setting cache entries in the parent site. + if (isset($GLOBALS['drupal_test_info']['test_run_id'])) { + $prefix .= $GLOBALS['drupal_test_info']['test_run_id']; } $full_key = urlencode($prefix . $bin . '-' . $key); diff --git a/sites/all/modules/memcache/memcache-lock-code.inc b/sites/all/modules/memcache/memcache-lock-code.inc new file mode 100644 index 00000000..374d02b0 --- /dev/null +++ b/sites/all/modules/memcache/memcache-lock-code.inc @@ -0,0 +1,161 @@ +<?php + +/** + * @file + * A memcache based implementation of a locking mechanism. + * See includes/lock.inc for documenation + * + * ATTENTION: Don't include this file directly - use the memcache-lock.inc to + * have a failover solution. + */ + +/** + * Initialize the locking system. + */ +function lock_initialize() { + global $locks; + + $locks = array(); +} + +/** + * Acquire (or renew) a lock, but do not block if it fails. + * + * @param $name + * The name of the lock. + * @param $timeout + * A number of seconds (int) before the lock expires (minimum of 1). + * @return + * TRUE if the lock was acquired, FALSE if it failed. + */ +function lock_acquire($name, $timeout = 30) { + global $locks; + + // Ensure that the timeout is at least 1 sec. This is a limitation + // imposed by memcached. + $timeout = (int) max($timeout, 1); + + if (dmemcache_add($name, _lock_id(), $timeout, 'semaphore')) { + $locks[$name] = _lock_id(); + } + elseif ($result = dmemcache_get($name, 'semaphore') && isset($locks[$name]) && $locks[$name] == _lock_id()) { + // Only renew the lock if we already set it and it has not expired. + dmemcache_set($name, _lock_id(), $timeout, 'semaphore'); + } + else { + // Failed to acquire the lock. Unset the key from the $locks array even if + // not set, PHP 5+ allows this without error or warning. + unset($locks[$name]); + } + + return isset($locks[$name]); +} + +/** + * Check if lock acquired by a different process may be available. + * + * If an existing lock has expired, it is removed. + * + * @param $name + * The name of the lock. + * @return + * TRUE if there is no lock or it was removed, FALSE otherwise. + */ +function lock_may_be_available($name) { + return !dmemcache_get($name, 'semaphore'); +} + +/** + * Wait for a lock to be available. + * + * This function may be called in a request that fails to acquire a desired + * lock. This will block further execution until the lock is available or the + * specified delay in seconds is reached. This should not be used with locks + * that are acquired very frequently, since the lock is likely to be acquired + * again by a different request while waiting. + * + * @param $name + * The name of the lock. + * @param $delay + * The maximum number of seconds to wait, as an integer. + * @return + * TRUE if the lock holds, FALSE if it is available. + */ +function lock_wait($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 (!dmemcache_get($name, 'semaphore')) { + // No longer need to wait. + return FALSE; + } + } + // The caller must still wait longer to get the lock. + return TRUE; +} + +/** + * Release a lock previously acquired by lock_acquire(). + * + * This will release the named lock if it is still held by the current request. + * + * @param $name + * The name of the lock. + */ +function lock_release($name) { + global $locks; + + dmemcache_delete($name, 'semaphore'); + // We unset unconditionally since caller assumes lock is released anyway. + unset($locks[$name]); +} + +/** + * Generate a unique identifier for locks generated during this request. + */ +function _lock_id() { + static $lock_id; + if (!isset($lock_id)) { + $lock_id = uniqid(mt_rand(), TRUE); + // We only register a shutdown function if a lock is used. + register_shutdown_function('lock_release_all', $lock_id); + } + return $lock_id; +} + +/** + * Release all locks acquired by this request. + */ +function lock_release_all($lock_id) { + global $locks; + foreach ($locks as $name => $id) { + $value = dmemcache_get($name, 'semaphore'); + + if ($value == $id) { + dmemcache_delete($name, 'semaphore'); + } + } +} diff --git a/sites/all/modules/memcache/memcache-lock.inc b/sites/all/modules/memcache/memcache-lock.inc index 5d252840..bc0f9368 100644 --- a/sites/all/modules/memcache/memcache-lock.inc +++ b/sites/all/modules/memcache/memcache-lock.inc @@ -6,117 +6,13 @@ * See includes/lock.inc for documenation */ -// Set up a define to make the code more readable, so we know we're setting a -// value for our lock and not simply passing dmemcache a flag. -define('LOCK_VALUE', TRUE); - - -/** - * Initialize the locking system. - */ -function lock_initialize() { - global $locks; - - $locks = array(); -} - -/** - * Acquire (or renew) a lock, but do not block if it fails. - * - * @param $name - * The name of the lock. - * @param $timeout - * A number of seconds (int) before the lock expires (minimum of 1). - * @return - * TRUE if the lock was acquired, FALSE if it failed. - */ -function lock_acquire($name, $timeout = 30) { - global $locks; - - // Ensure that the timeout is at least 1 sec. This is a limitation - // imposed by memcached. - $timeout = max($timeout, 1); - $now = microtime(TRUE); - $expire = $now + $timeout; - - $result = dmemcache_get($name, 'semaphore'); - if ($result && isset($locks[$name]) && $locks[$name] > $now) { - // Only renew the lock if we already set it and it has not expired. - if (dmemcache_set($name, LOCK_VALUE, $timeout, 'semaphore')) { - $locks[$name] = $expire; - } - } - else if (dmemcache_add($name, LOCK_VALUE, $timeout, 'semaphore')) { - $locks[$name] = $expire; - } - else { - // Failed to acquire the lock. Unset the key from the $locks array even if - // not set, PHP 5+ allows this without error or warning. - unset($locks[$name]); - } - - return isset($locks[$name]); -} - -/** - * Check if lock acquired by a different process may be available. - * - * If an existing lock has expired, it is removed. - * - * @param $name - * The name of the lock. - * @return - * TRUE if there is no lock or it was removed, FALSE otherwise. - */ -function lock_may_be_available($name) { - return !dmemcache_get($name, 'semaphore'); -} - -/** - * Wait for a lock to be available. - * - * This function may be called in a request that fails to acquire a desired - * lock. This will block further execution until the lock is available or the - * specified delay in seconds is reached. This should not be used with locks - * that are acquired very frequently, since the lock is likely to be acquired - * again by a different request during the sleep(). - * - * @param $name - * The name of the lock. - * @param $delay - * The maximum number of seconds to wait, as an integer. - * @return - * TRUE if the lock holds, FALSE if it is available. - */ -function lock_wait($name, $delay = 30) { - $delay = (int) $delay; - while ($delay--) { - // 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. - sleep(1); - if (!dmemcache_get($name, 'semaphore')) { - // No longer need to wait. - return FALSE; - } - } - // The caller must still wait longer to get the lock. - return TRUE; +require_once dirname(__FILE__) . '/dmemcache.inc'; + +// Check if memcached is available - if not include default lock handler. +// @todo get rid of this conditional include as soon as this is done: +// http://drupal.org/node/1225404 +$lock_file = dirname(__FILE__) . '/memcache-lock-code.inc'; +if (!dmemcache_object('semaphore')) { + $lock_file = DRUPAL_ROOT . '/includes/lock.inc'; } - -/** - * Release a lock previously acquired by lock_acquire(). - * - * This will release the named lock if it is still held by the current request. - * - * @param $name - * The name of the lock. - */ -function lock_release($name) { - global $locks; - - dmemcache_delete($name, 'semaphore'); - // We unset unconditionally since caller assumes lock is released anyway. - unset($locks[$name]); -} - +require_once $lock_file; \ No newline at end of file diff --git a/sites/all/modules/memcache/memcache.inc b/sites/all/modules/memcache/memcache.inc index 7718880d..7d438eab 100644 --- a/sites/all/modules/memcache/memcache.inc +++ b/sites/all/modules/memcache/memcache.inc @@ -1,12 +1,14 @@ <?php -require_once 'dmemcache.inc'; +require_once dirname(__FILE__) . '/dmemcache.inc'; /** * Defines the period after which wildcard clears are not considered valid. */ define('MEMCACHE_WILDCARD_INVALIDATE', 86400 * 28); +define('MEMCACHE_CONTENT_CLEAR', 'MEMCACHE_CONTENT_CLEAR'); + /** Implementation of cache.inc with memcache logic included **/ class MemCacheDrupal implements DrupalCacheInterface { @@ -14,12 +16,9 @@ class MemCacheDrupal implements DrupalCacheInterface { $this->memcache = dmemcache_object($bin); $this->bin = $bin; - $this->wildcard_flushes = variable_get('memcache_wildcard_flushes', array()); - $this->invalidate = variable_get('memcache_wildcard_invalidate', MEMCACHE_WILDCARD_INVALIDATE); - $this->cache_lifetime = variable_get('cache_lifetime', 0); - $this->cache_flush = variable_get('cache_flush_' . $this->bin); - $this->flushed = min($this->cache_flush, REQUEST_TIME - $this->cache_lifetime); + $this->reloadVariables(); } + function get($cid) { $cache = dmemcache_get($cid, $this->bin, $this->memcache); return $this->valid($cid, $cache) ? $cache : FALSE; @@ -32,73 +31,89 @@ class MemCacheDrupal implements DrupalCacheInterface { // This object has expired, so don't return it. unset($results[$cid]); } - else { - // Remove items from the referenced $cids array that we are returning, - // per the comment in cache_get_multiple() in includes/cache.inc. - unset($cids[$result->cid]); - } } + // Remove items from the referenced $cids array that we are returning, + // per the comment in cache_get_multiple() in includes/cache.inc. + $cids = array_diff($cids, array_keys($results)); return $results; } protected function valid($cid, $cache) { - if (!isset($cache) || !is_object($cache)) { - return FALSE; - } - - // The wildcard_valid() function has overhead due to a call to - // dmemcache_get_multi() to fetch possible wildcard flushes. Since some - // bins never have wildcard clears with a cid, we can shortcut these checks. - if (!empty($this->wildcard_flushes[$this->bin]) && - max($this->wildcard_flushes[$this->bin]) >= (REQUEST_TIME - $this->invalidate) && - !$this->wildcard_valid($cid, $cache)) { - return FALSE; - } - - // Determine when the current bin was last flushed. - $item_flushed_globally = $cache->created && $this->cache_flush && - $this->cache_lifetime && - ($cache->created <= $this->flushed); - - // Look in the session to determine if this item was flushed for the - // current user (ie, if they posted a comment and are viewing a cached page) - $cache_bins = isset($_SESSION['cache_flush']) ? $_SESSION['cache_flush'] : NULL; - $item_flushed_for_user = !empty($cache_bins) && - isset($cache_bins[$this->bin]) && - ($cache->created < $cache_bins[$this->bin]); - if ($item_flushed_for_user) { - return FALSE; + if ($cache) { + $cache_tables = isset($_SESSION['cache_flush']) ? $_SESSION['cache_flush'] : NULL; + // Items that have expired are invalid. + if (isset($cache->expire) && $cache->expire !== CACHE_PERMANENT && $cache->expire <= $_SERVER['REQUEST_TIME']) { + // If the memcache_stampede_protection variable is set, allow one process + // to rebuild the cache entry while serving expired content to the + // rest. Note that core happily returns expired cache items as valid and + // relies on cron to expire them, but this is mostly reliant on its + // use of CACHE_TEMPORARY which does not map well to memcache. + // @see http://drupal.org/node/534092 + if (variable_get('memcache_stampede_protection', FALSE)) { + // The process that acquires the lock will get a cache miss, all + // others will get a cache hit. + if (lock_acquire("memcache_$cid:$this->bin", variable_get('memcache_stampede_semaphore', 15))) { + $cache = FALSE; + } + } + else { + $cache = FALSE; + } + } + // Items created before the last full wildcard flush against this bin are + // invalid. + elseif ($cache->created <= $this->cache_flush) { + $cache = FALSE; + } + // Items created before the last content flush on this bin i.e. + // cache_clear_all() are invalid. + elseif ($cache->expire != CACHE_PERMANENT && $cache->created + $this->cache_lifetime <= $this->cache_content_flush) { + $cache = FALSE; + } + // Items cached before the cache was last flushed by the current user are + // invalid. + elseif ($cache->expire != CACHE_PERMANENT && is_array($cache_tables) && isset($cache_tables[$this->bin]) && $cache_tables[$this->bin] >= $cache->created) { + // Cache item expired, return FALSE. + $cache = FALSE; + } + // Finally, check for wildcard clears against this cid. + else { + if (!$this->wildcard_valid($cid, $cache)) { + $cache = FALSE; + } + } } - // The item can be expired if: - // - A liftetime is set and the item is older than both the lifetime and - // the global flush. - // - The item has been create before the bin was flushed for this user. - // - The item could simply expire. - // - // For the two global cases we try and grab a lock. If we get the lock, we - // return FALSE instead of the cached object which should cause it to be - // rebuilt. If we do not get the lock, we return the cached object despite - // it's expired The goal here is to avoid cache stampedes. - // By default the cache stampede semaphore is held for 15 seconds. This - // can be adjusted by setting the memcache_stampede_semaphore variable. - $item_expired = isset($cache->expire) && - $cache->expire !== CACHE_PERMANENT && - $cache->expire <= REQUEST_TIME; - if ($item_flushed_globally || $item_expired) { - // To avoid a stampede, return TRUE despite the item being expired if - // a previous process set the stampede semaphore already. However only - // do this if the data is less than 30 minutes stale. - if ((REQUEST_TIME - $cache->expire) >= variable_get('memcache_max_staleness', 1800) || - dmemcache_add($cid . '_semaphore', '', variable_get('memcache_stampede_semaphore', 15), $this->bin)) { - return FALSE; + // On cache misses, attempt to avoid stampedes when the + // memcache_stampede_protection variable is enabled. + if (!$cache) { + if (variable_get('memcache_stampede_protection', FALSE) && !lock_acquire("memcache_$cid:$this->bin", variable_get('memcache_stampede_semaphore', 15))) { + // Prevent any single request from waiting more than three times due to + // stampede protection. By default this is a maximum total wait of 15 + // seconds. This accounts for two possibilities - a cache and lock miss + // more than once for the same item. Or a cache and lock miss for + // different items during the same request. + // @todo: it would be better to base this on time waited rather than + // number of waits, but the lock API does not currently provide this + // information. Currently the limit will kick in for three waits of 25ms + // or three waits of 5000ms. + static $lock_count = 0; + $lock_count++; + if ($lock_count <= variable_get('memcache_stampede_wait_limit', 3)) { + // The memcache_stampede_semaphore variable was used in previous releases + // of memcache, but the max_wait variable was not, so by default divide + // the semaphore value by 3 (5 seconds). + lock_wait("memcache_$cid:$this->bin", variable_get('memcache_stampede_wait_time', 5)); + $cache = $this->get($cid); + } } } - return TRUE; + + return (bool) $cache; } function set($cid, $data, $expire = CACHE_PERMANENT, array $headers = NULL) { - $created = REQUEST_TIME; + $created = time(); // Create new cache object. $cache = new stdClass; @@ -131,6 +146,36 @@ class MemCacheDrupal implements DrupalCacheInterface { } function clear($cid = NULL, $wildcard = FALSE) { + if ($this->memcache === FALSE) { + // No memcache connection. + return; + } + + // It is not possible to detect a cache_clear_all() call other than looking + // at the backtrace unless http://drupal.org/node/81461 is added. + $backtrace = debug_backtrace(); + if ($cid == MEMCACHE_CONTENT_CLEAR || (isset($backtrace[2]) && $backtrace[2]['function'] == 'cache_clear_all' && empty($backtrace[2]['args']))) { + // Update the timestamp of the last global flushing of this bin. When + // retrieving data from this bin, we will compare the cache creation + // time minus the cache_flush time to the cache_lifetime to determine + // whether or not the cached item is still valid. + $this->cache_content_flush = time(); + $this->variable_set('cache_content_flush_' . $this->bin, $this->cache_content_flush); + if (variable_get('cache_lifetime', 0)) { + // We store the time in the current user's session. We then simulate + // that the cache was flushed for this user by not returning cached + // data to this user that was cached before the timestamp. + if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) { + $cache_bins = $_SESSION['cache_flush']; + } + else { + $cache_bins = array(); + } + // Use time() rather than request time here for correctness. + $cache_tables[$this->bin] = $this->cache_content_flush; + $_SESSION['cache_flush'] = $cache_tables; + } + } if (empty($cid) || $wildcard === TRUE) { // system_cron() flushes all cache bins returned by hook_flush_caches() // with cache_clear_all(NULL, $bin); This is for garbage collection with @@ -142,26 +187,29 @@ class MemCacheDrupal implements DrupalCacheInterface { elseif ($cid == '*') { $cid = ''; } - if ($this->cache_lifetime && empty($cid)) { + if (empty($cid)) { // Update the timestamp of the last global flushing of this bin. When // retrieving data from this bin, we will compare the cache creation // time minus the cache_flush time to the cache_lifetime to determine // whether or not the cached item is still valid. - variable_set("cache_flush_$this->bin", REQUEST_TIME); - $this->flushed = REQUEST_TIME; + $this->cache_flush = time(); + $this->variable_set("cache_flush_$this->bin", $this->cache_flush); + $this->flushed = min($this->cache_flush, time() - $this->cache_lifetime); - // We store the time in the current user's session which is saved into - // the sessions table by sess_write(). We then simulate that the cache - // was flushed for this user by not returning cached data to this user - // that was cached before the timestamp. - if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) { - $cache_bins = $_SESSION['cache_flush']; - } - else { - $cache_bins = array(); + if ($this->cache_lifetime) { + // We store the time in the current user's session which is saved into + // the sessions table by sess_write(). We then simulate that the cache + // was flushed for this user by not returning cached data to this user + // that was cached before the timestamp. + if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) { + $cache_bins = $_SESSION['cache_flush']; + } + else { + $cache_bins = array(); + } + $cache_bins[$this->bin] = $this->cache_flush; + $_SESSION['cache_flush'] = $cache_bins; } - $cache_bins[$this->bin] = REQUEST_TIME; - $_SESSION['cache_flush'] = $cache_bins; } else { // Register a wildcard flush for current cid @@ -181,7 +229,7 @@ class MemCacheDrupal implements DrupalCacheInterface { * value against this single-value sum tells us whether or not a new wildcard * flush has affected the cached item. */ - private function wildcard_flushes($cid) { + protected function wildcard_flushes($cid) { return array_sum($this->wildcards($cid)); } @@ -190,12 +238,21 @@ class MemCacheDrupal implements DrupalCacheInterface { * statically so multiple cache requests for the same item on the same page * load doesn't add overhead. */ - private function wildcards($cid, $flush = FALSE) { + protected function wildcards($cid, $flush = FALSE) { static $wildcards = array(); $matching = array(); - if (isset($this->wildcard_flushes[$this->bin]) && - is_array($this->wildcard_flushes[$this->bin])) { + $length = strlen($cid); + + if (isset($this->wildcard_flushes[$this->bin]) && is_array($this->wildcard_flushes[$this->bin])) { + // Wildcard flushes per table are keyed by a substring equal to the + // shortest wildcard clear on the table so far. So if the shortest + // wildcard was "links:foo:", and the cid we're checking for is + // "links:bar:bar", then the key will be "links:bar:". + $keys = array_keys($this->wildcard_flushes[$this->bin]); + $wildcard_length = strlen(reset($keys)); + $wildcard_key = substr($cid, 0, $wildcard_length); + // Determine which lookups we need to perform to determine whether or not // our cid was impacted by a wildcard flush. $lookup = array(); @@ -203,15 +260,16 @@ class MemCacheDrupal implements DrupalCacheInterface { // Find statically cached wildcards, and determine possibly matching // wildcards for this cid based on a history of the lengths of past // valid wildcard flushes in this bin. - foreach ($this->wildcard_flushes[$this->bin] as $length => $timestamp) { - if ($timestamp >= (REQUEST_TIME - $this->invalidate)) { - $key = '.wildcard-' . substr($cid, 0, $length); - $wildcard = dmemcache_key($key, $this->bin); - if (isset($wildcards[$this->bin][$wildcard])) { - $matching[$wildcard] = $wildcards[$this->bin][$wildcard]; - } - else { - $lookup[$wildcard] = $key; + if (isset($this->wildcard_flushes[$this->bin][$wildcard_key])) { + foreach ($this->wildcard_flushes[$this->bin][$wildcard_key] as $flush_length => $timestamp) { + if ($length >= $flush_length && $timestamp >= ($_SERVER['REQUEST_TIME'] - $this->invalidate)) { + $wildcard = '.wildcard-' . substr($cid, 0, $flush_length); + if (isset($wildcards[$this->bin][$wildcard])) { + $matching[$wildcard] = $wildcards[$this->bin][$wildcard]; + } + else { + $lookup[$wildcard] = $wildcard; + } } } } @@ -234,31 +292,55 @@ class MemCacheDrupal implements DrupalCacheInterface { // Also store failed lookups in our static cache, so we don't have to // do repeat lookups on single page loads. - foreach ($lookup as $wildcard => $key) { - $wildcards[$this->bin][$wildcard] = 0; + foreach ($lookup as $key => $key) { + $wildcards[$this->bin][$key] = 0; } } } + if ($flush) { + $key_length = $length; + if (isset($this->wildcard_flushes[$this->bin])) { + $keys = array_keys($this->wildcard_flushes[$this->bin]); + $key_length = strlen(reset($keys)); + } + $key = substr($cid, 0, $key_length); // Avoid too many calls to variable_set() by only recording a flush for // a fraction of the wildcard invalidation variable, per cid length. // Defaults to 28 / 4, or one week. - $length = strlen($cid); - if (!isset($this->wildcard_flushes[$this->bin][$length]) || - ($_SERVER['REQUEST_TIME'] - $this->wildcard_flushes[$this->bin][$length] > $this->invalidate / 4)) { - $this->wildcard_flushes = variable_get('memcache_wildcard_flushes', array()); - $this->wildcard_flushes[$this->bin][$length] = $_SERVER['REQUEST_TIME']; + if (!isset($this->wildcard_flushes[$this->bin][$key][$length]) || ($_SERVER['REQUEST_TIME'] - $this->wildcard_flushes[$this->bin][$key][$length] > $this->invalidate / 4)) { + + // If there are more than 50 different wildcard keys for this bin + // shorten the key by one, this should reduce variability by + // an order of magnitude and ensure we don't use too much memory. + if (isset($this->wildcard_flushes[$this->bin]) && count($this->wildcard_flushes[$this->bin]) > 50) { + $key = substr($cid, 0, $key_length - 1); + $length = strlen($key); + } + + // If this is the shortest key length so far, we need to remove all + // other wildcards lengths recorded so far for this bin and start + // again. This is equivalent to a full cache flush for this table, but + // it ensures the minimum possible number of wildcards are requested + // along with cache consistency. + if ($length < $key_length) { + $this->wildcard_flushes[$this->bin] = array(); + $this->variable_set("cache_flush_$this->bin", time()); + $this->cache_flush = time(); + } + $key = substr($cid, 0, $key_length); + $this->wildcard_flushes[$this->bin][$key][$length] = $_SERVER['REQUEST_TIME']; + variable_set('memcache_wildcard_flushes', $this->wildcard_flushes); } - $wildcard = dmemcache_key('.wildcard-' . $cid, $this->bin); - if (isset($wildcards[$this->bin][$wildcard]) && $wildcards[$this->bin][$wildcard] != 0) { - $this->memcache->increment($wildcard); - $wildcards[$this->bin][$wildcard]++; + $key = '.wildcard-' . $cid; + if (isset($wildcards[$this->bin][$key])) { + $wildcards[$this->bin][$key]++; } else { - $wildcards[$this->bin][$wildcard] = 1; - dmemcache_set('.wildcard-' . $cid, '1', 0, $this->bin); + $wildcards[$this->bin][$key] = 1; } + dmemcache_set($key, $wildcards[$this->bin][$key], 0, $this->bin); } return $matching; } @@ -266,7 +348,7 @@ class MemCacheDrupal implements DrupalCacheInterface { /** * Check if a wildcard flush has invalidated the current cached copy. */ - private function wildcard_valid($cid, $cache) { + protected function wildcard_valid($cid, $cache) { // Previously cached content won't have ->flushes defined. We could // force flush, but instead leave this up to the site admin. $flushes = isset($cache->flushes) ? (int)$cache->flushes : 0; @@ -280,5 +362,40 @@ class MemCacheDrupal implements DrupalCacheInterface { // We do not know so err on the safe side? return FALSE; } -} + /** + * Helper function to reload variables. + * + * This is used by the tests to verify that the cache object used the correct + * settings. + */ + function reloadVariables() { + $this->wildcard_flushes = variable_get('memcache_wildcard_flushes', array()); + $this->invalidate = variable_get('memcache_wildcard_invalidate', MEMCACHE_WILDCARD_INVALIDATE); + $this->cache_lifetime = variable_get('cache_lifetime', 0); + $this->cache_flush = variable_get('cache_flush_' . $this->bin); + $this->cache_content_flush = variable_get('cache_content_flush_' . $this->bin, 0); + $this->flushed = min($this->cache_flush, REQUEST_TIME - $this->cache_lifetime); + } + + /** + * Re-implementation of variable_set() that writes through instead of clearing. + */ + function variable_set($name, $value) { + global $conf; + + db_merge('variable') + ->key(array('name' => $name)) + ->fields(array('value' => serialize($value))) + ->execute(); + // If the variables are cached, get a fresh copy, update with the new value + // and set it again. + if ($cached = cache_get('variables', 'cache_bootstrap')) { + $variables = $cached->data; + $variables[$name] = $value; + cache_set('variables', $variables, 'cache_bootstrap'); + } + // If the variables aren't cached, there's no need to do anything. + $conf[$name] = $value; + } +} diff --git a/sites/all/modules/memcache/memcache.info b/sites/all/modules/memcache/memcache.info index b04fbcbe..4831c2a6 100644 --- a/sites/all/modules/memcache/memcache.info +++ b/sites/all/modules/memcache/memcache.info @@ -1,14 +1,14 @@ name = Memcache description = High performance integration with memcache. -package = Caching +package = Performance and scalability core = 7.x -files[] = memcache.inc -files[] = memcache.module -files[] = memcache.test +files[] = tests/memcache.test +files[] = tests/memcache-session.test +files[] = tests/memcache-lock.test -; Information added by drupal.org packaging script on 2011-05-27 -version = "7.x-1.0-beta4" +; Information added by drupal.org packaging script on 2012-01-07 +version = "7.x-1.0-rc3" core = "7.x" project = "memcache" -datestamp = "1306500116" +datestamp = "1325917859" diff --git a/sites/all/modules/memcache/memcache.install b/sites/all/modules/memcache/memcache.install new file mode 100644 index 00000000..347c3396 --- /dev/null +++ b/sites/all/modules/memcache/memcache.install @@ -0,0 +1,52 @@ +<?php + +/** + * Implements hook_requirements(). + */ +function memcache_requirements($phase) { + $requirements = array(); + $t = get_t(); + $memcache = extension_loaded('memcache'); + $memcached = extension_loaded('memcached'); + if ($phase == 'install' || $phase == 'runtime') { + if (!$memcache && !$memcached) { + $requirements['memcache_extension']['severity'] = REQUIREMENT_ERROR; + $requirements['memcache_extension']['title'] = $t('Extensions not available'); + $requirements['memcache_extension']['value'] = $t('Either the <a href="http://php.net/manual/en/book.memcache.php">memcache</a> or <a href="http://php.net/manual/en/book.memcached.php">memcached</a> extensions must be installed in order to use memcache integration.'); + } + } + if ($phase == 'runtime') { + if ($memcache) { + // @todo: consider adding minimum version requirement for extensions. + $requirements['memcache_extension_version']['severity'] = REQUIREMENT_OK; + $requirements['memcache_extension_version']['title'] = $t('Memcache version'); + $requirements['memcache_extension_version']['value'] = phpversion('memcache'); + } + if ($memcached) { + $requirements['memcached_extension_version']['severity'] = REQUIREMENT_OK; + $requirements['memcached_extension_version']['title'] = $t('Memcached version'); + $requirements['memcached_extension_version']['value'] = phpversion('memcached'); + } + // Confirm that dmemcache.inc has been included. + $requirements['memcache_inc']['title'] = $t('Memcache integration'); + if (function_exists('dmemcache_set')) { + $requirements['memcache_inc']['severity'] = REQUIREMENT_OK; + $requirements['memcache_inc']['title'] = $t('Memcache integration'); + $requirements['memcache_inc']['value'] = $t('Memcache integration functions are loaded'); + } + else { + $requirements['memcache_inc']['severity'] = REQUIREMENT_WARNING; + $requirements['memcache_inc']['title'] = $t('Memcache integration'); + $requirements['memcache_inc']['value'] = $t('Memcache integration is not currently loaded.'); + $requirements['memcache_inc']['description'] = $t('Check README.txt and ensure that memcache.inc is configured correctly in settings.php'); + } + } + return $requirements; +} + +/** + * Remove the memcache_widlcard_flushes variable since its structure has changed. + */ +function memcache_update_7000() { + variable_del('memcache_wildcard_flushes'); +} diff --git a/sites/all/modules/memcache/memcache.module b/sites/all/modules/memcache/memcache.module new file mode 100644 index 00000000..92f76a31 --- /dev/null +++ b/sites/all/modules/memcache/memcache.module @@ -0,0 +1,6 @@ +<?php +/** + * Provides very limited functionality such as hook_requirements(). + * memcache.inc must be configured in settings.php, and memcache.module is not + * necessary to use memcache as a cache backend. + */ diff --git a/sites/all/modules/memcache/memcache.test b/sites/all/modules/memcache/memcache.test deleted file mode 100644 index 89abd6b7..00000000 --- a/sites/all/modules/memcache/memcache.test +++ /dev/null @@ -1,541 +0,0 @@ -<?php - -class MemcacheTestCase extends DrupalWebTestCase { - protected $default_bin = 'cache'; - protected $default_cid = 'test_temporary'; - protected $default_value = 'MemcacheTest'; - - function setUp() { - include_once drupal_get_path('module', 'memcache') . '/memcache.inc'; - variable_set('cache_default_class', 'MemCacheDrupal'); - parent::setUp(); - } - - /** - * Check whether or not a cache entry exists. - * - * @param $cid - * The cache id. - * @param $var - * The variable the cache should contain. - * @param $bin - * The bin the cache item was stored in. - * @return - * TRUE on pass, FALSE on fail. - */ - protected function checkCacheExists($cid, $var, $bin = NULL) { - if ($bin == NULL) { - $bin = $this->default_bin; - } - - $cache = cache_get($cid, $bin); - - return isset($cache->data) && $cache->data == $var; - } - - /** - * Assert or a cache entry exists. - * - * @param $message - * Message to display. - * @param $var - * The variable the cache should contain. - * @param $cid - * The cache id. - * @param $bin - * The bin the cache item was stored in. - */ - protected function assertCacheExists($message, $var = NULL, $cid = NULL, $bin = NULL) { - if ($bin == NULL) { - $bin = $this->default_bin; - } - if ($cid == NULL) { - $cid = $this->default_cid; - } - if ($var == NULL) { - $var = $this->default_value; - } - - $this->assertTrue($this->checkCacheExists($cid, $var, $bin), $message); - } - - /** - * Assert or a cache entry has been removed. - * - * @param $message - * Message to display. - * @param $cid - * The cache id. - * @param $bin - * The bin the cache item was stored in. - */ - function assertCacheRemoved($message, $cid = NULL, $bin = NULL) { - if ($bin == NULL) { - $bin = $this->default_bin; - } - if ($cid == NULL) { - $cid = $this->default_cid; - } - - $cache = cache_get($cid, $bin); - $this->assertFalse($cache, $message); - } - - /** - * Perform the general wipe. - * @param $bin - * The bin to perform the wipe on. - */ - protected function generalWipe($bin = NULL) { - if ($bin == NULL) { - $bin = $this->default_bin; - } - - cache_clear_all(NULL, $bin); - } - - /** - * Setup the lifetime settings for caching. - * - * @param $time - * The time in seconds the cache should minimal live. - */ - protected function setupLifetime($time) { - variable_set('cache_lifetime', $time); - variable_set('cache_flush', 0); - } -} - -class MemCacheSavingCase extends MemcacheTestCase { - public static function getInfo() { - return array( - 'name' => 'Memcache saving test', - 'description' => 'Check our variables are saved and restored the right way.', - 'group' => 'Memcache' - ); - } - - /** - * Test the saving and restoring of a string. - */ - function testString() { - $this->checkVariable($this->randomName(100)); - } - - /** - * Test the saving and restoring of an integer. - */ - function testInteger() { - $this->checkVariable(100); - } - - /** - * Test the saving and restoring of a double. - */ - function testDouble() { - $this->checkVariable(1.29); - } - - /** - * Test the saving and restoring of an array. - */ - function testArray() { - $this->checkVariable(array('drupal1', 'drupal2' => 'drupal3', 'drupal4' => array('drupal5', 'drupal6'))); - } - - /** - * Test the saving and restoring of an object. - */ - function testObject() { - $test_object = new stdClass(); - $test_object->test1 = $this->randomName(100); - $test_object->test2 = 100; - $test_object->test3 = array('drupal1', 'drupal2' => 'drupal3', 'drupal4' => array('drupal5', 'drupal6')); - - cache_set('test_object', $test_object, 'cache'); - $cache = cache_get('test_object', 'cache'); - $this->assertTrue(isset($cache->data) && $cache->data == $test_object, t('Object is saved and restored properly.')); - } - - /* - * Check or a variable is stored and restored properly. - **/ - function checkVariable($var) { - cache_set('test_var', $var, 'cache'); - $cache = cache_get('test_var', 'cache'); - $this->assertTrue(isset($cache->data) && $cache->data === $var, t('@type is saved and restored properly.', array('@type' => ucfirst(gettype($var))))); - } -} - -/** - * Test cache_get_multiple(). - */ -class MemCacheGetMultipleUnitTest extends MemcacheTestCase { - - public static function getInfo() { - return array( - 'name' => 'Fetching multiple cache items', - 'description' => 'Confirm that multiple records are fetched correctly.', - 'group' => 'Memcache', - ); - } - - function setUp() { - $this->default_bin = 'cache_page'; - parent::setUp(); - } - - /** - * Test cache_get_multiple(). - */ - function testCacheMultiple() { - $item1 = $this->randomName(10); - $item2 = $this->randomName(10); - cache_set('item1', $item1, $this->default_bin); - cache_set('item2', $item2, $this->default_bin); - $this->assertTrue($this->checkCacheExists('item1', $item1), t('Item 1 is cached.')); - $this->assertTrue($this->checkCacheExists('item2', $item2), t('Item 2 is cached.')); - - // Fetch both records from the database with cache_get_multiple(). - $item_ids = array('item1', 'item2'); - $items = cache_get_multiple($item_ids, $this->default_bin); - $this->assertEqual($items['item1']->data, $item1, t('Item was returned from cache successfully.')); - $this->assertEqual($items['item2']->data, $item2, t('Item was returned from cache successfully.')); - - // Remove one item from the cache. - cache_clear_all('item2', $this->default_bin); - - // Confirm that only one item is returned by cache_get_multiple(). - $item_ids = array('item1', 'item2'); - $items = cache_get_multiple($item_ids, $this->default_bin); - $this->assertEqual($items['item1']->data, $item1, t('Item was returned from cache successfully.')); - $this->assertFalse(isset($items['item2']), t('Item was not returned from the cache.')); - $this->assertTrue(count($items) == 1, t('Only valid cache entries returned.')); - } -} - -/** - * Test cache clearing methods. - */ -class MemCacheClearCase extends MemcacheTestCase { - public static function getInfo() { - return array( - 'name' => 'Cache clear test', - 'description' => 'Check our clearing is done the proper way.', - 'group' => 'Memcache' - ); - } - - function setUp() { - $this->default_bin = 'cache_page'; - $this->default_value = $this->randomName(10); - - parent::setUp(); - } - - /** - * Test clearing using a cid. - */ - function testClearCid() { - cache_set('test_cid_clear', $this->default_value, $this->default_bin); - - $this->assertCacheExists(t('Cache was set for clearing cid.'), $this->default_value, 'test_cid_clear'); - cache_clear_all('test_cid_clear', $this->default_bin); - - $this->assertCacheRemoved(t('Cache was removed after clearing cid.'), 'test_cid_clear'); - - cache_set('test_cid_clear1', $this->default_value, $this->default_bin); - cache_set('test_cid_clear2', $this->default_value, $this->default_bin); - $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) - && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches were created for checking cid "*" with wildcard false.')); - cache_clear_all('*', $this->default_bin); - $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) - && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches still exists after clearing cid "*" with wildcard false.')); - } - - /** - * Test clearing using wildcard. - */ - function testClearWildcard() { - cache_set('test_cid_clear1', $this->default_value, $this->default_bin); - cache_set('test_cid_clear2', $this->default_value, $this->default_bin); - $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) - && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches were created for checking cid "*" with wildcard true.')); - cache_clear_all('*', $this->default_bin, TRUE); - $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) - || $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches removed after clearing cid "*" with wildcard true.')); - - cache_set('test_cid_clear1', $this->default_value, $this->default_bin); - cache_set('test_cid_clear2', $this->default_value, $this->default_bin); - $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) - && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches were created for checking cid substring with wildcard true.')); - cache_clear_all('test_', $this->default_bin, TRUE); - $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) - || $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches removed after clearing cid substring with wildcard true.')); - } - - /** - * Test clearing using an array. - */ - function testClearArray() { - // Create three cache entries. - cache_set('test_cid_clear1', $this->default_value, $this->default_bin); - cache_set('test_cid_clear2', $this->default_value, $this->default_bin); - cache_set('test_cid_clear3', $this->default_value, $this->default_bin); - $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) - && $this->checkCacheExists('test_cid_clear2', $this->default_value) - && $this->checkCacheExists('test_cid_clear3', $this->default_value), - t('Three cache entries were created.')); - - // Clear two entries using an array. - cache_clear_all(array('test_cid_clear1', 'test_cid_clear2'), $this->default_bin); - $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) - || $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two cache entries removed after clearing with an array.')); - - $this->assertTrue($this->checkCacheExists('test_cid_clear3', $this->default_value), - t('Entry was not cleared from the cache')); - - // Set the cache clear threshold to 2 to confirm that the full bin is cleared - // when the threshold is exceeded. - variable_set('cache_clear_threshold', 2); - cache_set('test_cid_clear1', $this->default_value, $this->default_bin); - cache_set('test_cid_clear2', $this->default_value, $this->default_bin); - $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) - && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two cache entries were created.')); - cache_clear_all(array('test_cid_clear1', 'test_cid_clear2', 'test_cid_clear3'), $this->default_bin); - $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) - || $this->checkCacheExists('test_cid_clear2', $this->default_value) - || $this->checkCacheExists('test_cid_clear3', $this->default_value), - t('All cache entries removed when the array exceeded the cache clear threshold.')); - } -} - -/** - * @file - * Provides SimpleTests for core session handling functionality. - */ -class MemcacheSessionTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Session tests', - 'description' => 'Memcache session handling tests.', - 'group' => 'Memcache' - ); - } - - function setUp() { - parent::setUp('session_test'); - } - - /** - * Tests for drupal_save_session() and drupal_session_regenerate(). - */ - function testSessionSaveRegenerate() { - $this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE (inside of testing framework) when initially called with no arguments.'), t('Session')); - $this->assertFalse(drupal_save_session(FALSE), t('drupal_save_session() correctly returns FALSE when called with FALSE.'), t('Session')); - $this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE when saving has been disabled.'), t('Session')); - $this->assertTrue(drupal_save_session(TRUE), t('drupal_save_session() correctly returns TRUE when called with TRUE.'), t('Session')); - $this->assertTrue(drupal_save_session(), t('drupal_save_session() correctly returns TRUE when saving has been enabled.'), t('Session')); - - // Test session hardening code from SA-2008-044. - $user = $this->drupalCreateUser(array('access content')); - - // Enable sessions. - $this->sessionReset($user->uid); - - // Make sure the session cookie is set as HttpOnly. - $this->drupalLogin($user); - $this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), t('Session cookie is set as HttpOnly.')); - $this->drupalLogout(); - - // Verify that the session is regenerated if a module calls exit - // in hook_user_login(). - user_save($user, array('name' => 'session_test_user')); - $user->name = 'session_test_user'; - $this->drupalGet('session-test/id'); - $matches = array(); - preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); - $this->assertTrue(!empty($matches[1]) , t('Found session ID before logging in.')); - $original_session = $matches[1]; - - // We cannot use $this->drupalLogin($user); because we exit in - // session_test_user_login() which breaks a normal assertion. - $edit = array( - 'name' => $user->name, - 'pass' => $user->pass_raw - ); - $this->drupalPost('user', $edit, t('Log in')); - $this->drupalGet('user'); - $pass = $this->assertText($user->name, t('Found name: %name', array('%name' => $user->name)), t('User login')); - $this->_logged_in = $pass; - - $this->drupalGet('session-test/id'); - $matches = array(); - preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); - $this->assertTrue(!empty($matches[1]) , t('Found session ID after logging in.')); - $this->assertTrue($matches[1] != $original_session, t('Session ID changed after login.')); - } - - /** - * Test data persistence via the session_test module callbacks. Also tests - * drupal_session_count() since session data is already generated here. - */ - function testDataPersistence() { - $user = $this->drupalCreateUser(array('access content')); - // Enable sessions. - $this->sessionReset($user->uid); - - $this->drupalLogin($user); - - $value_1 = $this->randomName(); - $this->drupalGet('session-test/set/' . $value_1); - $this->assertText($value_1, t('The session value was stored.'), t('Session')); - $this->drupalGet('session-test/get'); - $this->assertText($value_1, t('Session correctly returned the stored data for an authenticated user.'), t('Session')); - - // Attempt to write over val_1. If drupal_save_session(FALSE) is working. - // properly, val_1 will still be set. - $value_2 = $this->randomName(); - $this->drupalGet('session-test/no-set/' . $value_2); - $this->assertText($value_2, t('The session value was correctly passed to session-test/no-set.'), t('Session')); - $this->drupalGet('session-test/get'); - $this->assertText($value_1, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session')); - - // Switch browser cookie to anonymous user, then back to user 1. - $this->sessionReset(); - $this->sessionReset($user->uid); - $this->assertText($value_1, t('Session data persists through browser close.'), t('Session')); - - // Logout the user and make sure the stored value no longer persists. - $this->drupalLogout(); - - $this->sessionReset(); - $this->drupalGet('session-test/get'); - $this->assertNoText($value_1, t("After logout, previous user's session data is not available."), t('Session')); - - // Now try to store some data as an anonymous user. - $value_3 = $this->randomName(); - $this->drupalGet('session-test/set/' . $value_3); - $this->assertText($value_3, t('Session data stored for anonymous user.'), t('Session')); - $this->drupalGet('session-test/get'); - $this->assertText($value_3, t('Session correctly returned the stored data for an anonymous user.'), t('Session')); - - // Try to store data when drupal_save_session(FALSE). - $value_4 = $this->randomName(); - $this->drupalGet('session-test/no-set/' . $value_4); - $this->assertText($value_4, t('The session value was correctly passed to session-test/no-set.'), t('Session')); - $this->drupalGet('session-test/get'); - $this->assertText($value_3, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session')); - - // Login, the data should persist. - $this->drupalLogin($user); - $this->sessionReset($user->uid); - $this->drupalGet('session-test/get'); - $this->assertNoText($value_1, t('Session has persisted for an authenticated user after logging out and then back in.'), t('Session')); - - // Change session and create another user. - $user2 = $this->drupalCreateUser(array('access content')); - $this->sessionReset($user2->uid); - $this->drupalLogin($user2); - } - - /** - * Test that empty anonymous sessions are destroyed. - */ - function testEmptyAnonymousSession() { - // Verify that no session is automatically created for anonymous user. - $this->drupalGet(''); - $this->assertSessionCookie(FALSE); - $this->assertSessionEmpty(TRUE); - - // The same behavior is expected when caching is enabled. - variable_set('cache', CACHE_NORMAL); - $this->drupalGet(''); - $this->assertSessionCookie(FALSE); - $this->assertSessionEmpty(TRUE); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.')); - - // Start a new session by setting a message. - $this->drupalGet('session-test/set-message'); - $this->assertSessionCookie(TRUE); - $this->assertTrue($this->drupalGetHeader('Set-Cookie'), t('New session was started.')); - - // Display the message, during the same request the session is destroyed - // and the session cookie is unset. - $this->drupalGet(''); - $this->assertSessionCookie(FALSE); - $this->assertSessionEmpty(FALSE); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.')); - $this->assertText(t('This is a dummy message.'), t('Message was displayed.')); - $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.')); - - // Verify that session was destroyed. - $this->drupalGet(''); - $this->assertSessionCookie(FALSE); - $this->assertSessionEmpty(TRUE); - $this->assertNoText(t('This is a dummy message.'), t('Message was not cached.')); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); - $this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.')); - - // Verify that no session is created if drupal_save_session(FALSE) is called. - $this->drupalGet('session-test/set-message-but-dont-save'); - $this->assertSessionCookie(FALSE); - $this->assertSessionEmpty(TRUE); - - // Verify that no message is displayed. - $this->drupalGet(''); - $this->assertSessionCookie(FALSE); - $this->assertSessionEmpty(TRUE); - $this->assertNoText(t('This is a dummy message.'), t('The message was not saved.')); - } - - /** - * Reset the cookie file so that it refers to the specified user. - * - * @param $uid User id to set as the active session. - */ - function sessionReset($uid = 0) { - // Close the internal browser. - $this->curlClose(); - $this->loggedInUser = FALSE; - - // Change cookie file for user. - $this->cookieFile = file_directory_path('temporary') . '/cookie.' . $uid . '.txt'; - $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile; - $this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE; - $this->drupalGet('session-test/get'); - $this->assertResponse(200, t('Session test module is correctly enabled.'), t('Session')); - } - - /** - * Assert whether the SimpleTest browser sent a session cookie. - */ - function assertSessionCookie($sent) { - if ($sent) { - $this->assertNotNull($this->session_id, t('Session cookie was sent.')); - } - else { - $this->assertNull($this->session_id, t('Session cookie was not sent.')); - } - } - - /** - * Assert whether $_SESSION is empty at the beginning of the request. - */ - function assertSessionEmpty($empty) { - if ($empty) { - $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '1', t('Session was empty.')); - } - else { - $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', t('Session was not empty.')); - } - } -} diff --git a/sites/all/modules/memcache/memcache_admin/memcache_admin.info b/sites/all/modules/memcache/memcache_admin/memcache_admin.info index b9d8fcb6..7d887553 100644 --- a/sites/all/modules/memcache/memcache_admin/memcache_admin.info +++ b/sites/all/modules/memcache/memcache_admin/memcache_admin.info @@ -1,12 +1,11 @@ name = Memcache Admin description = Adds a User Interface to monitor the Memcache for this site. -package = Caching core = 7.x -files[] = memcache_admin.module +package = Performance and scalability -; Information added by drupal.org packaging script on 2011-05-27 -version = "7.x-1.0-beta4" +; Information added by drupal.org packaging script on 2012-01-07 +version = "7.x-1.0-rc3" core = "7.x" project = "memcache" -datestamp = "1306500116" +datestamp = "1325917859" diff --git a/sites/all/modules/memcache/memcache_admin/memcache_admin.install b/sites/all/modules/memcache/memcache_admin/memcache_admin.install new file mode 100644 index 00000000..0622b206 --- /dev/null +++ b/sites/all/modules/memcache/memcache_admin/memcache_admin.install @@ -0,0 +1,13 @@ +<?php + +/** + * @file update functions for memcache_admin. + */ + +/** + * Flush caches and rebuild menu to allow new stats pages to appear. + */ +function memcache_admin_update_7001() { + drupal_flush_all_caches(); + menu_rebuild(); +} diff --git a/sites/all/modules/memcache/memcache_admin/memcache_admin.module b/sites/all/modules/memcache/memcache_admin/memcache_admin.module index ee84db5c..9760668e 100644 --- a/sites/all/modules/memcache/memcache_admin/memcache_admin.module +++ b/sites/all/modules/memcache/memcache_admin/memcache_admin.module @@ -7,7 +7,7 @@ */ function memcache_admin_init() { global $user; - if (($user->uid == 0) || strstr($_SERVER['PHP_SELF'], 'update.php') || strstr($_GET['q'], 'autocomplete')) { + if (($user->uid == 0) || strstr($_SERVER['PHP_SELF'], 'update.php') || (isset($_GET['q']) && (in_array($_GET['q'], array('upload/js', 'admin/content/node-settings/rebuild')) || substr($_GET['q'], 0, strlen('system/files')) == 'system/files' || substr($_GET['q'], 0, strlen('batch')) == 'batch' || strstr($_GET['q'], 'autocomplete')))) { // update.php relies on standard error handler } else { @@ -19,18 +19,22 @@ function memcache_admin_init() { } /** - * Implementation of hook_perm(). + * Implements hook_perm(). */ function memcache_admin_permission() { return array( 'access memcache statistics' => array( 'title' => t('Access memcache statistics'), ), + 'access slab cachedump' => array( + 'title' => t('Access cachedump of memcache slab'), + 'restrict access' => TRUE, + ), ); } /** - * Implementation of hook_menu(). + * Implements hook_menu(). */ function memcache_admin_menu() { $items['admin/config/system/memcache'] = array( @@ -41,15 +45,15 @@ function memcache_admin_menu() { 'access arguments' => array('administer site configuration'), ); $items['admin/reports/memcache'] = array( - 'title' => 'Memcache status', - 'description' => "View the statistics for this site's memcache.", + 'title' => 'Memcache statistics', + 'description' => "View statistics for all configured memcache servers.", 'page callback' => 'memcache_admin_stats', 'access arguments' => array('access memcache statistics'), 'weight' => 1, ); - $memache_servers = variable_get('memcache_servers', array()); + $memcache_servers = variable_get('memcache_servers', array('127.0.0.1:11211' => 'default')); $clusters = array(); - foreach($memache_servers as $server => $cluster) { + foreach ($memcache_servers as $server => $cluster) { $clusters[$cluster]['servers'][] = $server; $clusters[$cluster]['bin'] = _memcache_admin_get_bin_for_cluster($cluster); } @@ -70,21 +74,25 @@ function memcache_admin_menu() { 'page callback' => 'memcache_admin_stats', 'page arguments' => array($cluster), 'access arguments' => array('access memcache statistics'), - 'weight' => $count, + 'weight' => $count++, ); - $count++; - - $sub_count = 0; - foreach (array('default', 'malloc', 'maps', 'slabs', 'items', 'sizes') as $type) { - $items['admin/reports/memcache/'. $cluster .'/'. $type] = array( - 'type' => $type == 'default' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK, - 'page callback' => 'memcache_admin_stats', - 'page arguments' => array($cluster, $type), - 'title' => $type, + foreach ($cluster_info['servers'] as $server) { + $items["admin/reports/memcache/$cluster/$server"] = array( + 'title' => check_plain($server), + 'type' => MENU_CALLBACK, + 'page callback' => 'memcache_admin_stats_raw', + 'page arguments' => array($cluster, $server), 'access arguments' => array('access memcache statistics'), - 'weight' => $sub_count, ); - $sub_count++; + foreach (memcache_admin_stats_types($cluster) as $type) { + $items["admin/reports/memcache/$cluster/$server/$type"] = array( + 'type' => MENU_CALLBACK, + 'page callback' => 'memcache_admin_stats_raw', + 'page arguments' => array($cluster, $server, $type), + 'title' => $type, + 'access arguments' => array('access memcache statistics'), + ); + } } } } @@ -104,102 +112,392 @@ function memcache_admin_admin_settings() { return system_settings_form($form); } +function _memcache_admin_default_bin($bin) { + if ($bin == 'default') { + return 'cache'; + } + return $bin; +} + +function _memcache_admin_stats_connections($stats) { + return t('!current open of !total total', array('!current' => number_format($stats['curr_connections']), '!total' => number_format($stats['total_connections']))); +} + +/** + * Statistics report: calculate # of set cmds and total cmds. + */ +function _memcache_admin_stats_sets($stats) { + if (($stats['cmd_set'] + $stats['cmd_get']) == 0) { + $sets = 0; + } + else { + $sets = $stats['cmd_set'] / ($stats['cmd_set'] + $stats['cmd_get']) * 100; + } + if (empty($stats['uptime'])) { + $average = 0; + } + else { + $average = $sets / $stats['uptime']; + } + return t('!average/s; !set sets (!sets%) of !total commands', array('!average' => number_format($average, 2), '!sets' => number_format($sets, 2), '!set' => number_format($stats['cmd_set']), '!total' => number_format($stats['cmd_set'] + $stats['cmd_get']))); +} + +/** + * Statistics report: calculate # of get cmds, broken down by hits and misses. + */ +function _memcache_admin_stats_gets($stats) { + if (($stats['cmd_set'] + $stats['cmd_get']) == 0) { + $gets = 0; + } + else { + $gets = $stats['cmd_get'] / ($stats['cmd_set'] + $stats['cmd_get']) * 100; + } + if (empty($stats['uptime'])) { + $average = 0; + } + else { + $average = $stats['cmd_get'] / $stats['uptime']; + } + return t('!average/s; !total gets (!gets%); !hit hits (!percent_hit%) !miss misses (!percent_miss%)', array('!average' => number_format($average, 2), '!gets' => number_format($gets, 2), '!hit' => number_format($stats['get_hits']), '!percent_hit' => ($stats['cmd_get'] > 0 ? number_format($stats['get_hits'] / $stats['cmd_get'] * 100, 2) : '0.00'), '!miss' => number_format($stats['get_misses']), '!percent_miss' => ($stats['cmd_get'] > 0 ? number_format($stats['get_misses'] / $stats['cmd_get'] * 100, 2) : '0.00'), '!total' => number_format($stats['cmd_get']))); +} + +/** + * Statistics report: calculate # of increments and decrements. + */ +function _memcache_admin_stats_counters($stats) { + return t('!incr increments, !decr decrements', array('!incr' => number_format($stats['incr_hits'] + $stats['incr_misses']), '!decr' => number_format($stats['decr_hits'] + $stats['decr_misses']))); +} + /** - * Memcahe Stats page + * Statistics report: calculate bytes transferred. + */ +function _memcache_admin_stats_transfer($stats) { + if ($stats['bytes_written'] == 0) { + $written = 0; + } + else { + $written = $stats['bytes_read'] / $stats['bytes_written'] * 100; + } + return t('!to:!from (!written% to cache)', array('!to' => format_size((int)$stats['bytes_read']), '!from' => format_size((int)$stats['bytes_written']), '!written' => number_format($written, 2))); +} + +/** + * Statistics report: calculate per-connection averages. + */ +function _memcache_admin_stats_average($stats) { + if ($stats['total_connections'] == 0) { + $get = 0; + $set = 0; + $read = 0; + $write = 0; + } + else { + $get = $stats['cmd_get'] / $stats['total_connections']; + $set = $stats['cmd_set'] / $stats['total_connections']; + $read = $stats['bytes_written'] / $stats['total_connections']; + $write = $stats['bytes_read'] / $stats['total_connections']; + } + return t('!read in !get gets; !write in !set sets', array('!get' => number_format($get, 2), '!set' => number_format($set, 2), '!read' => format_size(number_format($read, 2)), '!write' => format_size(number_format($write, 2)))); +} + +/** + * Statistics report: calculate available memory. + */ +function _memcache_admin_stats_memory($stats) { + if ($stats['limit_maxbytes'] == 0) { + $percent = 0; + } + else { + $percent = 100 - $stats['bytes'] / $stats['limit_maxbytes'] * 100; + } + return t('!available (!percent%) of !total', array('!available' => format_size($stats['limit_maxbytes'] - $stats['bytes']), '!percent' => number_format($percent, 2), '!total' => format_size($stats['limit_maxbytes']))); +} + +/** + * Helper function, reverse map the memcache_bins variable. + */ +function memcache_admin_bin_mapping($bin = 'cache') { + $bins = array_flip(variable_get('memcache_bins', array('cache' => 'default'))); + if (isset($bins[$bin])) { + return $bins[$bin]; + } + else { + // The default bin is 'cache'. + return _memcache_admin_default_bin($bin); + } +} + +/** + * Memcache Stats page * - * @param string $cluster - which cluster to view? - * @param string $type - which type of stat, eg: default, malloc, maps, cachedump, slabs, items or sizes * @return string */ -function memcache_admin_stats($cluster = 'default', $type = 'default') { - $bin = _memcache_admin_get_bin_for_cluster($cluster); - - if ($bin) { - $stats = dmemcache_stats($bin, $type); - - if (is_array($stats) && count($stats)) { - $output = ''; - - foreach ($stats as $server => $values) { - if (is_array($values)) { - // Do some custome value tweaks for specific stat page types. - switch ($type) { - case 'default' : - $values['uptime'] = format_interval($values['uptime']); - $values['time'] = format_date($values['time']); - $values['bytes'] = format_size($values['bytes']); - $values['bytes_read'] = format_size($values['bytes_read']); - $values['bytes_written'] = format_size($values['bytes_written']); - $values['limit_maxbytes'] = format_size($values['limit_maxbytes']); - - //Custom Entries - $values['hit_percentage'] = ($values['cmd_get'] > 0) - ? number_format(100.0 * $values['get_hits'] / $values['cmd_get'], 2) . '%' - : '0'; - - $mem_used = intval($values['bytes']) / (intval($values['limit_maxbytes']) * 1024); - $values['mem_used'] = number_format(100.0 * $mem_used, 2) . '%'; - break; - } +function memcache_admin_stats($bin = 'cache') { + $bin = memcache_admin_bin_mapping($bin); - $output .= theme('memcache_admin_stats_table', array('server' => $server, 'stats' => $values)); - } - else { - drupal_set_message(t('Unable to get statistic from server %server', array('%server' => $server))); - } - } - } + $stats = dmemcache_stats($bin, 'default', TRUE); + + $memcache_servers = variable_get('memcache_servers', array('127.0.0.1:11211' => 'default')); + if (is_array($stats[$bin]) && count($stats[$bin])) { + $stats = $stats[$bin]; + + $mc = dmemcache_object($bin); + if ($mc instanceof Memcached) { + $version = t('Memcached v!version', array('!version' => phpversion('Memcached'))); + } + elseif ($mc instanceof Memcache) { + $version = t('Memcache v!version', array('!version' => phpversion('Memcache'))); + } else { - $output = ''; - drupal_set_message(t('No available statistics for this bin.')); + $version = t('Unknown'); + drupal_set_message(t('Failed to detect the memcache PECL extension.'), 'error'); + } + // Building per-server stats for the current memcache bin. + $servers = array(); + foreach ($memcache_servers as $server => $b) { + $b = memcache_admin_bin_mapping($b); + if ($b == $bin) { + $servers[] = $server; + + if (empty($stats[$server]['uptime'])) { + drupal_set_message(t('Failed to connect to server at %server.', array('%server' => $server)), 'error'); + } + $data['server_overview'][$server] = t('v!version running !uptime', array('!version' => check_plain($stats[$server]['version']), '!uptime' => format_interval($stats[$server]['uptime']))); + $data['server_pecl'][$server] = t('n/a'); + $data['server_time'][$server] = format_date($stats[$server]['time']); + $data['server_connections'][$server] = _memcache_admin_stats_connections($stats[$server]); + $data['cache_sets'][$server] = _memcache_admin_stats_sets($stats[$server]); + $data['cache_gets'][$server] = _memcache_admin_stats_gets($stats[$server]); + $data['cache_counters'][$server] = _memcache_admin_stats_counters($stats[$server]); + $data['cache_transfer'][$server] = _memcache_admin_stats_transfer($stats[$server]); + $data['cache_average'][$server] = _memcache_admin_stats_average($stats[$server]); + $data['memory_available'][$server] = _memcache_admin_stats_memory($stats[$server]); + $data['memory_evictions'][$server] = number_format($stats[$server]['evictions']); + } } + // Building a report as a custom formatted array of arrays that gets + // properly displayed by theme_memcache_admin_stats_table. + $report = array( + 'Server overview' => array( + array_merge( + array('header' => t('Uptime')), + array('total' => t('n/a')), + $data['server_overview']), + array_merge( + array('header' => t('PECL extension')), + array('total' => $version), + $data['server_pecl']), + array_merge( + array('header' => t('Server time')), + array('total' => t('n/a')), + $data['server_time']), + array_merge( + array('header' => t('Connections')), + array('total' => _memcache_admin_stats_connections($stats['total'])), + $data['server_connections']), + ), + 'Cache statistics' => array( + array_merge( + array('header' => t('Sets')), + array('total' => _memcache_admin_stats_sets($stats['total'])), + $data['cache_sets']), + array_merge( + array('header' => t('Gets')), + array('total' => _memcache_admin_stats_gets($stats['total'])), + $data['cache_gets']), + array_merge( + array('header' => t('Counters')), + array('total' => _memcache_admin_stats_counters($stats['total'])), + $data['cache_counters']), + array_merge( + array('header' => t('Transferred')), + array('total' => _memcache_admin_stats_transfer($stats['total'])), + $data['cache_transfer']), + array_merge( + array('header' => t('Per-connection average')), + array('total' => _memcache_admin_stats_average($stats['total'])), + $data['cache_average']), + ), + 'Memory overview' => array( + array_merge( + array('header' => t('Available memory')), + array('total' => _memcache_admin_stats_memory($stats['total'])), + $data['memory_available']), + array_merge( + array('header' => t('Evictions')), + array('total' => $stats['total']['evictions']), + $data['memory_evictions']), + ), + ); + $output = theme('memcache_admin_stats_table', array('bin' => $bin, 'servers' => $servers, 'report' => $report)); + } + else { + $output = ''; + drupal_set_message(t('There are no statistics being reported for this bin.'), 'error'); } return $output; } +function memcache_admin_stats_raw($bin, $server, $type = 'default') { + $cluster = memcache_admin_bin_mapping($bin); + $slab = (int)arg(7); + if (arg(6) == 'cachedump' && !empty($slab) && user_access('access slab cachedump')) { + $stats = dmemcache_stats($cluster, arg(7), FALSE); + } + else { + $stats = dmemcache_stats($cluster, $type, FALSE); + } + $breadcrumbs = array(l(t('Home'), NULL), l(t('Administer'), 'admin'), l(t('Reports'), 'admin/reports'), l(t('Memcache'), 'admin/reports/memcache'), l(t($bin), "admin/reports/memcache/$bin")); + if ($type == 'slabs' && arg(6) == 'cachedump' && user_access('access slab cachedump')) { + $breadcrumbs[] = l($server, "admin/reports/memcache/$bin/$server"); + $breadcrumbs[] = l('slabs', "admin/reports/memcache/$bin/$server/$type"); + } + drupal_set_breadcrumb($breadcrumbs); + if (isset($stats[$cluster][$server]) && is_array($stats[$cluster][$server]) && count($stats[$cluster][$server])) { + $output = theme('memcache_admin_stats_raw_table', array('cluster' => $cluster, 'server' => $server, 'stats' => $stats[$cluster][$server], 'type' => $type)); + } + elseif ($type == 'slabs' && is_array($stats[$cluster]) && count($stats[$cluster])) { + $output = theme('memcache_admin_stats_raw_table', array('cluster' => $cluster, 'server' => $server, 'stats' => $stats[$cluster], 'type' => $type)); + } + else { + $output = theme('memcache_admin_stats_raw_table', array('cluster' => $cluster, 'server' => $server, 'stats' => array(), 'type' => $type)); + drupal_set_message(t('No @type statistics for this bin.', array('@type' => $type))); + } + return $output; +} + /** - * Implementation of hook_theme(). + * Implements hook_theme(). */ function memcache_admin_theme() { return array( 'memcache_admin_stats_table' => array( - 'variables' => array('server' => NULL, 'stats' => NULL), + 'variables' => array('bin' => NULL, 'servers' => NULL, 'report' => NULL), + ), + 'memcache_admin_stats_raw_table' => array( + 'variables' => array('bin' => NULL, 'server' => NULL, 'stats' => NULL, 'type' => NULL), ) ); } /** * Theme function for rendering the output from memcache_admin_stats - * - * @param string $server - Server name:port for caption for the table - * @param array $stats - array of key/value string pairs for the table results - * @return string */ function theme_memcache_admin_stats_table($variables) { + $bin = $variables['bin']; + $servers = $variables['servers']; + $stats = $variables['report']; + + $output = ''; + $links = array(); + $memcache_bins = variable_get('memcache_bins', array('cache' => 'default')); + foreach ($servers as $server) { + $link_bin = $memcache_bins[$bin]; + $links[] = l($server, check_plain("admin/reports/memcache/$link_bin/$server")); + } + $headers = array_merge(array('', t('Totals')), $links); + foreach ($stats as $table => $data) { + $rows = array(); + foreach ($data as $row) { + if (isset($row[2]) && is_array($row[2])) { + $row[2] = implode(', ', $row[2]); + } + else { + $row[2] = ''; + } + $rows[] = $row; + } + $output .= theme('table', array('header' => $headers, 'rows' => $rows)); + } + return $output; +} + +function memcache_admin_stats_types($bin) { + module_load_include('inc', 'memcache', 'dmemcache'); + if ($mc = dmemcache_object($bin)) { + if ($mc instanceof Memcache) { + // TODO: Determine which versions of the PECL memcache extension have + // these other stats types: 'malloc', 'maps', optionally detect this + // version and expose them. These stats are "subject to change without + // warning" unfortunately. + return array('default', 'slabs', 'items', 'sizes'); + } + else { + // The Memcached PECL extension only offers the default statistics. + return array('default'); + } + } + else { + return array(); + } +} + +function theme_memcache_admin_stats_raw_table($variables) { + $cluster = $variables['cluster']; $server = $variables['server']; $stats = $variables['stats']; + $current_type = isset($variables['type']) ? $variables['type'] : 'default'; - $rows = array(); + $memcache_bins = variable_get('memcache_bins', array()); + $bin = isset($memcache_bins[$cluster]) ? $memcache_bins[$cluster] : 'default'; + // Provide navigation for the various memcache stats types + if (count(memcache_admin_stats_types($bin)) > 1) { + foreach (memcache_admin_stats_types($bin) as $type) { + if ($current_type == $type) { + $links[] = '<strong>' . l(t($type), "admin/reports/memcache/$bin/$server/". ($type == 'default' ? '' : $type)) .'</strong>'; + } + else { + $links[] = l(t($type), "admin/reports/memcache/$bin/$server/". ($type == 'default' ? '' : $type)); + } + } + } + $output = !empty($links) ? implode($links, ' | ') : ''; + $headers = array(t('Property'), t('Value')); + $rows = array(); + // Items are returned as an array within an array within an array. We step + // in one level to properly display the contained statistics. + if ($current_type == 'items' && isset($stats['items'])) { + $stats = $stats['items']; + } foreach ($stats as $key => $value) { + // Add navigation for getting a cachedump of individual slabs + if (($current_type == 'slabs' || $current_type == 'items') && is_int($key) && user_access('access slab cachedump')) { + $key = l($key, "admin/reports/memcache/$bin/$server/slabs/cachedump/$key"); + } if (is_array($value)) { $rs = array(); foreach ($value as $k => $v) { - $rs[] = array($k, $v); - } - $rows[] = array($key, theme('table', array('header' => array(), 'rows' => $rs))); + // Format timestamp when viewing cachedump of individual slabs. + if ($current_type == 'slabs' && user_access('access slab cachedump') && arg(6) == 'cachedump' && $k == 0) { + $k = t('Size'); + $v = format_size($v); + } + else if ($current_type == 'slabs' && user_access('access slab cachedump') && arg(6) == 'cachedump' && $k == 1) { + $k = t('Expire'); + $full_stats = dmemcache_stats($cluster, 'default'); + $infinite = $full_stats[$cluster][$server]['time'] - $full_stats[$cluster][$server]['uptime']; + if ($v == $infinite) { + $v = t('infinite'); + } + else { + $v = t('in @time', array('@time' => format_interval($v - time()))); + } + } + $rs[] = array(check_plain($k), check_plain($v)); + } + $rows[] = array($key, theme('table', array('rows' => $rs))); } else { - $rows[] = array($key, $value); + $rows[] = array(check_plain($key), check_plain($value)); } } - - return theme('table', array('header' => array(t('Property'), t('Value')), 'rows' => $rows, 'caption' => $server)); + $output .= theme('table', array('header' => $headers, 'rows' => $rows)); + return $output; } - /** * Retrieve the cluster for any given bin * @@ -210,8 +508,8 @@ function _memcache_admin_get_bin_for_cluster($cluster) { static $cluster_map = array(); if (!isset($cluster_map[$cluster])) { - $memache_bins = variable_get('memcache_bins', array()); - if ($mapping = array_search($cluster, $memache_bins)) { + $memcache_bins = variable_get('memcache_bins', array()); + if ($mapping = array_search($cluster, $memcache_bins)) { $cluster_map[$cluster] = $mapping; } else { @@ -229,6 +527,14 @@ function _memcache_admin_get_bin_for_cluster($cluster) { function memcache_admin_shutdown() { global $_memcache_statistics; + // Don't call theme() during shutdown if the registry has been rebuilt (such + // as when enabling/disabling modules on admin/build/modules) as things break. + // Instead, simply exit without displaying admin statistics for this page + // load. See http://drupal.org/node/616282 for discussion. + if (!function_exists('theme_get_registry') || !theme_get_registry()) { + return; + } + // Try not to break non-HTML pages. if (function_exists('drupal_get_http_header')) { $header = drupal_get_http_header('content-type'); diff --git a/sites/all/modules/memcache/tests/memcache-lock.test b/sites/all/modules/memcache/tests/memcache-lock.test new file mode 100644 index 00000000..4bfff7b1 --- /dev/null +++ b/sites/all/modules/memcache/tests/memcache-lock.test @@ -0,0 +1,71 @@ +<?php + +/** + * Tests for the lock system. + */ +class MemcacheLockFunctionalTest extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Locking framework tests', + 'description' => 'Confirm locking works between two separate requests.', + 'group' => 'Memcache', + ); + } + + function setUp() { + parent::setUp('memcache_test'); + } + + /** + * Confirm that we can acquire and release locks in two parallel requests. + */ + function testLockAcquire() { + // Nasty hack. There are two processes involved here - the process + // calling the test itself, that calls lock_acquire() directly. And the + // 'child' process that is requested over http by drupalGet(). + // In dmemcache.inc, we look for simpletest installs, to force a + // simpletest prefix as part of the memcache key - this avoids the + // actual Drupal site install being corrupted by entries from the tested + // site. However, the child install apparently does not set the simpletest + // global, so when making requests to that site, we pass the simpletest ID + // as part of the URL, and mess around with the globals on that side. + $test_run_id = $GLOBALS['drupal_test_info']['test_run_id']; + $lock_acquired = 'TRUE: Lock successfully acquired in memcache_test_lock_acquire()'; + $lock_not_acquired = 'FALSE: Lock not acquired in memcache_test_lock_acquire()'; + $this->assertTrue(lock_acquire('memcache_test_lock_acquire'), t('Lock acquired by this request.'), t('Lock')); + $this->assertTrue(lock_acquire('memcache_test_lock_acquire'), t('Lock extended by this request.'), t('Lock')); + lock_release('memcache_test_lock_acquire'); + + // Cause another request to acquire the lock. + $this->drupalGet('memcache-test/lock-acquire' . "/$test_run_id"); + $this->assertText($lock_acquired, t('Lock acquired by the other request.'), t('Lock')); + // The other request has finished, thus it should have released its lock. + $this->assertTrue(lock_acquire('memcache_test_lock_acquire'), t('Lock acquired by this request.'), t('Lock')); + // This request holds the lock, so the other request cannot acquire it. + $this->drupalGet('memcache-test/lock-acquire' . "/$test_run_id"); + $this->assertText($lock_not_acquired, t('Lock not acquired by the other request.'), t('Lock')); + lock_release('memcache_test_lock_acquire'); + + // Try a very short timeout and lock breaking. + $this->assertTrue(lock_acquire('memcache_test_lock_acquire', 1), t('Lock acquired by this request.'), t('Lock')); + sleep(2); + // The other request should break our lock. + $this->drupalGet('memcache-test/lock-acquire' . "/$test_run_id"); + $this->assertText($lock_acquired, t('Lock acquired by the other request, breaking our lock.'), t('Lock')); + // We cannot renew it, since the other thread took it. + // @todo: this assertion currently fails - the lock_acquire() call returns + // true. For now, commented out the assertion, uncomment when attempting to + // fix. + //$this->assertFalse(lock_acquire('memcache_test_lock_acquire'), t('Lock cannot be extended by this request.'), t('Lock')); + + // Check the shut-down function. + $lock_acquired_exit = 'TRUE: Lock successfully acquired in memcache_test_lock_exit()'; + $lock_not_acquired_exit = 'FALSE: Lock not acquired in memcache_test_lock_exit()'; + $this->drupalGet('memcache-test/lock-exit' . "/$test_run_id"); + $this->assertText($lock_acquired_exit, t('Lock acquired by the other request before exit.'), t('Lock')); + $this->assertTrue(lock_acquire('memcache_test_lock_exit'), t('Lock acquired by this request after the other request exits.'), t('Lock')); + } + +} diff --git a/sites/all/modules/memcache/tests/memcache-session.test b/sites/all/modules/memcache/tests/memcache-session.test new file mode 100644 index 00000000..26265a22 --- /dev/null +++ b/sites/all/modules/memcache/tests/memcache-session.test @@ -0,0 +1,226 @@ +<?php + +/** + * @file + * Provides SimpleTests for core session handling functionality. + */ + +class MemcacheSessionTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Session tests', + 'description' => 'Memcache session handling tests.', + 'group' => 'Memcache' + ); + } + + function setUp() { + parent::setUp('session_test'); + } + + /** + * Tests for drupal_save_session() and drupal_session_regenerate(). + */ + function testSessionSaveRegenerate() { + $this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE (inside of testing framework) when initially called with no arguments.'), t('Session')); + $this->assertFalse(drupal_save_session(FALSE), t('drupal_save_session() correctly returns FALSE when called with FALSE.'), t('Session')); + $this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE when saving has been disabled.'), t('Session')); + $this->assertTrue(drupal_save_session(TRUE), t('drupal_save_session() correctly returns TRUE when called with TRUE.'), t('Session')); + $this->assertTrue(drupal_save_session(), t('drupal_save_session() correctly returns TRUE when saving has been enabled.'), t('Session')); + + // Test session hardening code from SA-2008-044. + $user = $this->drupalCreateUser(array('access content')); + + // Enable sessions. + $this->sessionReset($user->uid); + + // Make sure the session cookie is set as HttpOnly. + $this->drupalLogin($user); + $this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), t('Session cookie is set as HttpOnly.')); + $this->drupalLogout(); + + // Verify that the session is regenerated if a module calls exit + // in hook_user_login(). + user_save($user, array('name' => 'session_test_user')); + $user->name = 'session_test_user'; + $this->drupalGet('session-test/id'); + $matches = array(); + preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); + $this->assertTrue(!empty($matches[1]) , t('Found session ID before logging in.')); + $original_session = $matches[1]; + + // We cannot use $this->drupalLogin($user); because we exit in + // session_test_user_login() which breaks a normal assertion. + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw + ); + $this->drupalPost('user', $edit, t('Log in')); + $this->drupalGet('user'); + $pass = $this->assertText($user->name, t('Found name: %name', array('%name' => $user->name)), t('User login')); + $this->_logged_in = $pass; + + $this->drupalGet('session-test/id'); + $matches = array(); + preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); + $this->assertTrue(!empty($matches[1]) , t('Found session ID after logging in.')); + $this->assertTrue($matches[1] != $original_session, t('Session ID changed after login.')); + } + + /** + * Test data persistence via the session_test module callbacks. Also tests + * drupal_session_count() since session data is already generated here. + */ + function testDataPersistence() { + $user = $this->drupalCreateUser(array('access content')); + // Enable sessions. + $this->sessionReset($user->uid); + + $this->drupalLogin($user); + + $value_1 = $this->randomName(); + $this->drupalGet('session-test/set/' . $value_1); + $this->assertText($value_1, t('The session value was stored.'), t('Session')); + $this->drupalGet('session-test/get'); + $this->assertText($value_1, t('Session correctly returned the stored data for an authenticated user.'), t('Session')); + + // Attempt to write over val_1. If drupal_save_session(FALSE) is working. + // properly, val_1 will still be set. + $value_2 = $this->randomName(); + $this->drupalGet('session-test/no-set/' . $value_2); + $this->assertText($value_2, t('The session value was correctly passed to session-test/no-set.'), t('Session')); + $this->drupalGet('session-test/get'); + $this->assertText($value_1, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session')); + + // Switch browser cookie to anonymous user, then back to user 1. + $this->sessionReset(); + $this->sessionReset($user->uid); + $this->assertText($value_1, t('Session data persists through browser close.'), t('Session')); + + // Logout the user and make sure the stored value no longer persists. + $this->drupalLogout(); + + $this->sessionReset(); + $this->drupalGet('session-test/get'); + $this->assertNoText($value_1, t("After logout, previous user's session data is not available."), t('Session')); + + // Now try to store some data as an anonymous user. + $value_3 = $this->randomName(); + $this->drupalGet('session-test/set/' . $value_3); + $this->assertText($value_3, t('Session data stored for anonymous user.'), t('Session')); + $this->drupalGet('session-test/get'); + $this->assertText($value_3, t('Session correctly returned the stored data for an anonymous user.'), t('Session')); + + // Try to store data when drupal_save_session(FALSE). + $value_4 = $this->randomName(); + $this->drupalGet('session-test/no-set/' . $value_4); + $this->assertText($value_4, t('The session value was correctly passed to session-test/no-set.'), t('Session')); + $this->drupalGet('session-test/get'); + $this->assertText($value_3, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session')); + + // Login, the data should persist. + $this->drupalLogin($user); + $this->sessionReset($user->uid); + $this->drupalGet('session-test/get'); + $this->assertNoText($value_1, t('Session has persisted for an authenticated user after logging out and then back in.'), t('Session')); + + // Change session and create another user. + $user2 = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user2->uid); + $this->drupalLogin($user2); + } + + /** + * Test that empty anonymous sessions are destroyed. + */ + function testEmptyAnonymousSession() { + // Verify that no session is automatically created for anonymous user. + $this->drupalGet(''); + $this->assertSessionCookie(FALSE); + $this->assertSessionEmpty(TRUE); + + // The same behavior is expected when caching is enabled. + variable_set('cache', TRUE); + $this->drupalGet(''); + $this->assertSessionCookie(FALSE); + $this->assertSessionEmpty(TRUE); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.')); + + // Start a new session by setting a message. + $this->drupalGet('session-test/set-message'); + $this->assertSessionCookie(TRUE); + $this->assertTrue($this->drupalGetHeader('Set-Cookie'), t('New session was started.')); + + // Display the message, during the same request the session is destroyed + // and the session cookie is unset. + $this->drupalGet(''); + $this->assertSessionCookie(FALSE); + $this->assertSessionEmpty(FALSE); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.')); + $this->assertText(t('This is a dummy message.'), t('Message was displayed.')); + $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.')); + + // Verify that session was destroyed. + $this->drupalGet(''); + $this->assertSessionCookie(FALSE); + $this->assertSessionEmpty(TRUE); + $this->assertNoText(t('This is a dummy message.'), t('Message was not cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); + $this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.')); + + // Verify that no session is created if drupal_save_session(FALSE) is called. + $this->drupalGet('session-test/set-message-but-dont-save'); + $this->assertSessionCookie(FALSE); + $this->assertSessionEmpty(TRUE); + + // Verify that no message is displayed. + $this->drupalGet(''); + $this->assertSessionCookie(FALSE); + $this->assertSessionEmpty(TRUE); + $this->assertNoText(t('This is a dummy message.'), t('The message was not saved.')); + } + + /** + * Reset the cookie file so that it refers to the specified user. + * + * @param $uid User id to set as the active session. + */ + function sessionReset($uid = 0) { + // Close the internal browser. + $this->curlClose(); + $this->loggedInUser = FALSE; + + // Change cookie file for user. + $this->cookieFile = variable_get('file_public_path', conf_path() . '/files') . '/cookie.' . $uid . '.txt'; + $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile; + $this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE; + $this->drupalGet('session-test/get'); + $this->assertResponse(200, t('Session test module is correctly enabled.'), t('Session')); + } + + /** + * Assert whether the SimpleTest browser sent a session cookie. + */ + function assertSessionCookie($sent) { + if ($sent) { + $this->assertNotNull($this->session_id, t('Session cookie was sent.')); + } + else { + $this->assertNull($this->session_id, t('Session cookie was not sent.')); + } + } + + /** + * Assert whether $_SESSION is empty at the beginning of the request. + */ + function assertSessionEmpty($empty) { + if ($empty) { + $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '1', t('Session was empty.')); + } + else { + $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', t('Session was not empty.')); + } + } +} diff --git a/sites/all/modules/memcache/tests/memcache.test b/sites/all/modules/memcache/tests/memcache.test new file mode 100644 index 00000000..6cce3658 --- /dev/null +++ b/sites/all/modules/memcache/tests/memcache.test @@ -0,0 +1,572 @@ +<?php + +class MemcacheTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + protected $default_bin = 'cache_memcache'; + protected $default_cid = 'test_temporary'; + protected $default_value = 'MemcacheTest'; + + function setUp() { + parent::setUp(func_get_args()); + + variable_set("cache_flush_$this->default_bin", 0); + variable_set('cache_class_cache_memcache', 'MemcacheDrupal'); + + $this->resetVariables(); + } + + /** + * Check whether or not a cache entry exists. + * + * @param $cid + * The cache id. + * @param $var + * The variable the cache should contain. + * @param $bin + * The bin the cache item was stored in. + * @return + * TRUE on pass, FALSE on fail. + */ + protected function checkCacheExists($cid, $var, $bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + + $cache = cache_get($cid, $bin); + + return isset($cache->data) && $cache->data == $var; + } + + /** + * Assert or a cache entry exists. + * + * @param $message + * Message to display. + * @param $var + * The variable the cache should contain. + * @param $cid + * The cache id. + * @param $bin + * The bin the cache item was stored in. + */ + protected function assertCacheExists($message, $var = NULL, $cid = NULL, $bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + if ($cid == NULL) { + $cid = $this->default_cid; + } + if ($var == NULL) { + $var = $this->default_value; + } + + $this->assertTrue($this->checkCacheExists($cid, $var, $bin), $message); + } + + /** + * Assert or a cache entry has been removed. + * + * @param $message + * Message to display. + * @param $cid + * The cache id. + * @param $bin + * The bin the cache item was stored in. + */ + function assertCacheRemoved($message, $cid = NULL, $bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + if ($cid == NULL) { + $cid = $this->default_cid; + } + + $cache = cache_get($cid, $bin); + $this->assertFalse($cache, $message); + } + + /** + * Perform the general wipe. + * @param $bin + * The bin to perform the wipe on. + */ + protected function generalWipe($bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + + cache_clear_all(NULL, $bin); + } + + function resetVariables() { + _cache_get_object($this->default_bin)->reloadVariables(); + } +} + +class MemCacheSavingCase extends MemcacheTestCase { + public static function getInfo() { + return array( + 'name' => 'Memcache saving test', + 'description' => 'Check our variables are saved and restored the right way.', + 'group' => 'Memcache' + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Test the saving and restoring of a string. + */ + function testString() { + $this->checkVariable($this->randomName(100)); + } + + /** + * Test the saving and restoring of an integer. + */ + function testInteger() { + $this->checkVariable(100); + } + + /** + * Test the saving and restoring of a double. + */ + function testDouble() { + $this->checkVariable(1.29); + } + + /** + * Test the saving and restoring of an array. + */ + function testArray() { + $this->checkVariable(array('drupal1', 'drupal2' => 'drupal3', 'drupal4' => array('drupal5', 'drupal6'))); + } + + /** + * Test the saving and restoring of an object. + */ + function testObject() { + $test_object = new stdClass(); + $test_object->test1 = $this->randomName(100); + $test_object->test2 = 100; + $test_object->test3 = array('drupal1', 'drupal2' => 'drupal3', 'drupal4' => array('drupal5', 'drupal6')); + + cache_set('test_object', $test_object, 'cache'); + $cache = cache_get('test_object', 'cache'); + $this->assertTrue(isset($cache->data) && $cache->data == $test_object, t('Object is saved and restored properly.')); + } + + /* + * Check or a variable is stored and restored properly. + **/ + function checkVariable($var) { + cache_set('test_var', $var, 'cache'); + $cache = cache_get('test_var', 'cache'); + $this->assertTrue(isset($cache->data) && $cache->data === $var, t('@type is saved and restored properly.', array('@type' => ucfirst(gettype($var))))); + } +} + +/** + * Test cache_get_multiple(). + */ +class MemCacheGetMultipleUnitTest extends MemcacheTestCase { + + public static function getInfo() { + return array( + 'name' => 'Fetching multiple cache items', + 'description' => 'Confirm that multiple records are fetched correctly.', + 'group' => 'Memcache', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Test cache_get_multiple(). + */ + function testCacheMultiple() { + $item1 = $this->randomName(10); + $item2 = $this->randomName(10); + cache_set('test:item1', $item1, $this->default_bin); + cache_set('test:item2', $item2, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test:item1', $item1), t('Item 1 is cached.')); + $this->assertTrue($this->checkCacheExists('test:item2', $item2), t('Item 2 is cached.')); + + // Fetch both records from the database with cache_get_multiple(). + $item_ids = array('test:item1', 'test:item2'); + $items = cache_get_multiple($item_ids, $this->default_bin); + $this->assertEqual($items['test:item1']->data, $item1, t('Item was returned from cache successfully.')); + $this->assertEqual($items['test:item2']->data, $item2, t('Item was returned from cache successfully.')); + + $this->assertTrue(empty($item_ids), t('Ids of returned items have been removed.')); + + // Remove one item from the cache. + cache_clear_all('test:item2', $this->default_bin); + + // Confirm that only one item is returned by cache_get_multiple(). + $item_ids = array('test:item1', 'test:item2'); + $items = cache_get_multiple($item_ids, $this->default_bin); + $this->assertEqual($items['test:item1']->data, $item1, t('Item was returned from cache successfully.')); + $this->assertFalse(isset($items['test:item2']), t('Item was not returned from the cache.')); + $this->assertTrue(count($items) == 1, t('Only valid cache entries returned.')); + $this->assertTrue(count($item_ids) == 1, t('Invalid cache ids still present.')); + + } +} + +/** + * Test cache clearing methods. + */ +class MemCacheClearCase extends MemcacheTestCase { + public static function getInfo() { + return array( + 'name' => 'Cache clear test', + 'description' => 'Check our clearing is done the proper way.', + 'group' => 'Memcache' + ); + } + + function setUp() { + + parent::setUp('memcache_test'); + $this->default_value = $this->randomName(10); + } + + + /** + * Test clearing the cache with a cid, no cache lifetime. + */ + function testClearCidNoLifetime() { + $this->clearCidTest(); + } + + /** + * Test clearing the cache with a cid, with cache lifetime. + */ + function testClearCidLifetime() { + variable_set('cache_lifetime', 6000); + $this->clearCidTest(); + } + + /** + * Test clearing using wildcard prefixes, no cache lifetime. + */ + function testClearWildcardNoLifetime() { + $this->clearWildcardPrefixTest(); + } + + /** + * Test clearing using wildcard prefix, with cache lifetime. + */ + function testClearWildcardLifetime() { + variable_set('cache_lifetime', 6000); + $this->clearWildcardPrefixTest(); + } + + /** + * Test full bin flushes with no cache lifetime. + */ + function testClearWildcardFull() { + cache_set('test_cid_clear1', $this->default_value, $this->default_bin); + cache_set('test_cid_clear2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches were created for checking cid "*" with wildcard true.')); + cache_clear_all('*', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) + || $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches removed after clearing cid "*" with wildcard true.')); + } + + /** + * Test full bin flushes with cache lifetime. + */ + function testClearCacheLifetime() { + variable_set('cache_lifetime', 600); + $this->resetVariables(); + + // Set a cache item with an expiry. + cache_set('test_cid', $this->default_value, $this->default_bin, time() + 3600); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was created successfully.'); + + // Set a permanent cache item. + cache_set('test_cid_2', $this->default_value, $this->default_bin); + + // Clear the page and block caches. + cache_clear_all(MEMCACHE_CONTENT_CLEAR, $this->default_bin); + // Since the cache was cleared within the current session, cache_get() + // should return false. + $this->assertFalse($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was cleared successfully.'); + + // However permament items should stay in place. + $this->assertTrue($this->checkCacheExists('test_cid_2', $this->default_value), 'Cache item was not cleared'); + + // If $_SESSION['cache_flush'] is not set, then the expired item should be returned. + unset($_SESSION['cache_flush']); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item is still returned due to minimum cache lifetime.'); + + // Set a much shorter cache lifetime. + variable_set('cache_content_flush_' . $this->default_bin, 0); + variable_set('cache_lifetime', 1); + cache_set('test_cid_1', $this->default_value, $this->default_bin, time() + 6000); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was created successfully.'); + sleep(2); + cache_clear_all(MEMCACHE_CONTENT_CLEAR, $this->default_bin); + $this->assertFalse($this->checkCacheExists('test_cid', $this->default_value), 'Cache item is not returned once minimum cache lifetime has expired.'); + + // Reset the cache clear variables. + variable_set('cache_content_flush_' . $this->default_bin, 0); + variable_set('cache_lifetime', 6000); + $this->resetVariables(); + sleep(1); + + // Confirm that cache_lifetime does not take effect for full bin flushes. + cache_set('test_cid', $this->default_value, $this->default_bin, time() + 6000); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was created successfully.'); + cache_set('test_cid_2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_2', $this->default_value), 'Cache item was created successfully.'); + + // Now flush the bin. + cache_clear_all('*', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was cleared successfully.'); + $this->assertFalse($this->checkCacheExists('test_cid_2', $this->default_value), 'Cache item was cleared successfully.'); + } + + /** + * Test different wildcards to verify the wildcard optimizations. + */ + function testWildCardOptimizations() { + + // Set and clear a cache with a short cid/wildcard. + cache_set('foo:1', $this->default_value, $this->default_bin); + + $this->assertCacheExists(t('Foo cache was set.'), $this->default_value, 'foo:1'); + + cache_clear_all('foo', $this->default_bin, TRUE); + $this->assertCacheRemoved(t('Foo cache was invalidated.'), 'foo:1'); + + // Set additional longer caches. + cache_set('foobar', $this->default_value, $this->default_bin); + cache_set('foofoo', $this->default_value, $this->default_bin); + + $this->assertCacheExists(t('Foobar cache set.'), $this->default_value, 'foobar'); + $this->assertCacheExists(t('Foofoo cache set.'), $this->default_value, 'foofoo'); + + // Clear one of them with a wildcard and make sure the other one is still + // valid. + cache_clear_all('foobar', $this->default_bin, TRUE); + $this->assertCacheRemoved(t('Foobar cache invalidated.'), 'foobar'); + $this->assertCacheExists(t('Foofoo cache still valid.'), $this->default_value, 'foofoo'); + + // Set and clear a cache with a different, equally short cid/wildcard. + cache_set('bar:1', $this->default_value, $this->default_bin); + $this->assertCacheExists(t('Bar cache was set.'), $this->default_value, 'bar:1'); + + cache_clear_all('bar', $this->default_bin, TRUE); + $this->assertCacheRemoved(t('Bar cache invalidated.'), 'bar:1'); + $this->assertCacheExists(t('Foofoo cache still valid.'), $this->default_value, 'foofoo'); + + // Clear cache with an even shorter wildcard. This results in a full bin + // bin clear, all entries are marked invalid. + cache_set('bar:2', $this->default_value, $this->default_bin); + cache_clear_all('ba', $this->default_bin, TRUE); + $this->assertCacheRemoved(t('Bar:1 cache invalidated.'), 'bar:1'); + $this->assertCacheRemoved(t('Bar:2 cache invalidated.'), 'bar:2'); + $this->assertCacheRemoved(t('Foofoo cache invalidated.'), 'foofoo'); + } + + + /** + * Test clearing using a cid. + */ + function clearCidTest() { + cache_set('test_cid_clear', $this->default_value, $this->default_bin); + + $this->assertCacheExists(t('Cache was set for clearing cid.'), $this->default_value, 'test_cid_clear'); + cache_clear_all('test_cid_clear', $this->default_bin); + + $this->assertCacheRemoved(t('Cache was removed after clearing cid.'), 'test_cid_clear'); + + cache_set('test_cid_clear1', $this->default_value, $this->default_bin); + cache_set('test_cid_clear2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches were created for checking cid "*" with wildcard false.')); + cache_clear_all('*', $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches still exists after clearing cid "*" with wildcard false.')); + } + + /** + * Test cache clears using wildcard prefixes. + */ + function clearWildcardPrefixTest() { + $this->resetVariables(); + cache_set('test_cid_clear:1', $this->default_value, $this->default_bin); + cache_set('test_cid_clear:2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear:1', $this->default_value) + && $this->checkCacheExists('test_cid_clear:2', $this->default_value), + t('Two caches were created for checking cid substring with wildcard true.')); + cache_clear_all('test_cid_clear:', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear:1', $this->default_value) + || $this->checkCacheExists('test_cid_clear:2', $this->default_value), + t('Two caches removed after clearing cid substring with wildcard true.')); + // Test for the case where a wildcard object disappears, for example a + // partial memcache restart or eviction. + cache_set('test_cid_clear:1', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear:1', $this->default_value), 'The cache was created successfully.'); + cache_clear_all('test_', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear:1', $this->default_value), 'The cache was cleared successfully.'); + // Delete the wildcard manually to simulate an eviction. + $wildcard = '.wildcard-' . 'test_cid_clear:'; + dmemcache_delete($wildcard, $this->default_bin); + // Reset the memcache_wildcards() static cache. + // @todo: this is a class object in D7. + //memcache_wildcards(FALSE, FALSE, FALSE, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear:1', $this->default_value), 'The cache was cleared successfully.'); + } + + /** + * Test wildcard flushing on separate pages to ensure no static cache is used. + */ + function testClearWildcardOnSeparatePages() { + + $random_wildcard = $this->randomName(2) . ':' . $this->randomName(3); + $random_key = $random_wildcard . ':' . $this->randomName(4) . ':' . $this->randomName(2); + $random_value = $this->randomName(); + + $this->drupalGetAJAX('memcache-test/clear-cache'); + + $data = $this->drupalGetAJAX('memcache-test/set/' . $random_key . '/' . $random_value); + + $this->assertTrue(is_array($data), 'Cache has data.'); + $this->assertEqual($random_key, $data['cid'], 'Cache keys match.'); + $this->assertEqual($random_value, $data['data'], 'Cache values match.'); + + $data = $this->drupalGetAJAX('memcache-test/get/' . $random_key); + $this->assertEqual($random_key, $data['cid'], 'Cache keys match.'); + $this->assertEqual($random_value, $data['data'], 'Cache values match.'); + + $this->drupalGet('memcache-test/wildcard-clear/' . $random_wildcard); + + $data = $this->drupalGetAJAX('memcache-test/get/' . $random_key); + $this->assertFalse($data, 'Cache was properly flushed.'); + + $data = $this->drupalGetAJAX('memcache-test/set/' . $random_key . '/' . $random_value); + + $this->assertTrue(is_array($data), 'Cache has data.'); + $this->assertEqual($random_key, $data['cid'], 'Cache keys match.'); + $this->assertEqual($random_value, $data['data'], 'Cache values match.'); + + $data = $this->drupalGetAJAX('memcache-test/get/' . $random_key); + $this->assertEqual($random_key, $data['cid'], 'Cache keys match.'); + $this->assertEqual($random_value, $data['data'], 'Cache values match.'); + + $this->drupalGet('memcache-test/wildcard-clear/' . $random_wildcard); + + $data = $this->drupalGetAJAX('memcache-test/get/' . $random_key); + $this->assertFalse($data, 'Cache was properly flushed.'); + } + +} + +/** + * Test some real world cache scenarios with default modules. + * + * Please make sure you've set the proper memcache settings in the settings.php. + * Looks like I've not chance to change the cache settings to what's needed by + * this test. + */ +class MemCacheRealWorldCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Real world cache tests', + 'description' => 'Test some real world cache scenarios.', + 'group' => 'Memcache' + ); + } + + function setUp() { + parent::setUp('menu'); + } + + /** + * Test if the menu module caching acts as expected. + * + * The menu module clears the affected menu if an menu item is changed using + * wildcards. + */ + function testMenu() { + // Create and login user. + $account = $this->drupalCreateUser(array('access administration pages', 'administer blocks', 'administer menu', 'create article content')); + $this->drupalLogin($account); + + // Add Menu Link to test with + $item = $this->addMenuLink(); + $original_title = $item['link_title']; + + // Check if menu link is displayed. + $this->drupalGet(''); + $this->assertText($original_title, 'Menu item displayed in frontend'); + + // Change menu item multiple times and check if the change is reflected. + for($i=0; $i < 3; $i++) { + // Edit menu link. + $edit = array(); + $edit['link_title'] = $this->randomName(16);; + $this->drupalPost("admin/structure/menu/item/{$item['mlid']}/edit", $edit, t('Save')); + if (!$this->assertResponse(200)) { + // One fail is enough. + break; + } + // Verify edited menu link. + if (!$this->drupalGet('admin/structure/menu/manage/' . $item['menu_name'])) { + // One fail is enough. + break; + } + $this->assertText($edit['link_title'], 'Menu link was edited'); + $this->drupalGet(''); + if (!$this->assertText($edit['link_title'], 'Change is reflected in frontend')) { + // One fail is enough. + break; + } + } + } + + /** + * Adds a menu link. + * + * @see MenuTestCase::addMenuLink() + */ + function addMenuLink($plid = 0, $link = '<front>', $menu_name = 'main-menu') { + // View add menu link page. + $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); + $this->assertResponse(200); + + $title = '!OriginalLink_' . $this->randomName(16); + $edit = array( + 'link_path' => $link, + 'link_title' => $title, + 'description' => '', + 'enabled' => TRUE, // Use this to disable the menu and test. + 'expanded' => TRUE, // Setting this to true should test whether it works when we do the std_user tests. + 'parent' => $menu_name . ':' . $plid, + 'weight' => '0', + ); + + // Add menu link. + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertResponse(200); + // Unlike most other modules, there is no confirmation message displayed. + $this->assertText($title, 'Menu link was added'); + + $item = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => $title))->fetchAssoc(); + return $item; + } +} diff --git a/sites/all/modules/memcache/tests/memcache6.test b/sites/all/modules/memcache/tests/memcache6.test new file mode 100644 index 00000000..197b6e9b --- /dev/null +++ b/sites/all/modules/memcache/tests/memcache6.test @@ -0,0 +1,343 @@ +<?php + +class MemcacheTestCase extends DrupalWebTestCase { + protected $default_bin = 'cache'; + protected $default_cid = 'test_temporary'; + protected $default_value = 'MemcacheTest'; + + function setUp() { + parent::setUp(); + } + + /** + * Check whether or not a cache entry exists. + * + * @param $cid + * The cache id. + * @param $var + * The variable the cache should contain. + * @param $bin + * The bin the cache item was stored in. + * @return + * TRUE on pass, FALSE on fail. + */ + protected function checkCacheExists($cid, $var, $bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + + $cache = cache_get($cid, $bin); + + return isset($cache->data) && $cache->data == $var; + } + + /** + * Assert or a cache entry exists. + * + * @param $message + * Message to display. + * @param $var + * The variable the cache should contain. + * @param $cid + * The cache id. + * @param $bin + * The bin the cache item was stored in. + */ + protected function assertCacheExists($message, $var = NULL, $cid = NULL, $bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + if ($cid == NULL) { + $cid = $this->default_cid; + } + if ($var == NULL) { + $var = $this->default_value; + } + + $this->assertTrue($this->checkCacheExists($cid, $var, $bin), $message); + } + + /** + * Assert or a cache entry has been removed. + * + * @param $message + * Message to display. + * @param $cid + * The cache id. + * @param $bin + * The bin the cache item was stored in. + */ + function assertCacheRemoved($message, $cid = NULL, $bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + if ($cid == NULL) { + $cid = $this->default_cid; + } + + $cache = cache_get($cid, $bin); + $this->assertFalse($cache, $message); + } + + /** + * Perform the general wipe. + * @param $bin + * The bin to perform the wipe on. + */ + protected function generalWipe($bin = NULL) { + if ($bin == NULL) { + $bin = $this->default_bin; + } + + cache_clear_all(NULL, $bin); + } + + /** + * Setup the lifetime settings for caching. + * + * @param $time + * The time in seconds the cache should minimal live. + */ + protected function setupLifetime($time) { + variable_set('cache_lifetime', $time); + variable_set('cache_flush', 0); + } +} + +class MemCacheSavingCase extends MemcacheTestCase { + public static function getInfo() { + return array( + 'name' => 'Memcache saving test', + 'description' => 'Check our variables are saved and restored the right way.', + 'group' => 'Memcache' + ); + } + + function setUp() { + parent::setUp(); + variable_set("cache_flush_cache", 0); + } + + /** + * Test the saving and restoring of a string. + */ + function testString() { + $this->checkVariable($this->randomName(100)); + } + + /** + * Test the saving and restoring of an integer. + */ + function testInteger() { + $this->checkVariable(100); + } + + /** + * Test the saving and restoring of a double. + */ + function testDouble() { + $this->checkVariable(1.29); + } + + /** + * Test the saving and restoring of an array. + */ + function testArray() { + $this->checkVariable(array('drupal1', 'drupal2' => 'drupal3', 'drupal4' => array('drupal5', 'drupal6'))); + } + + /** + * Test the saving and restoring of an object. + */ + function testObject() { + $test_object = new stdClass(); + $test_object->test1 = $this->randomName(100); + $test_object->test2 = 100; + $test_object->test3 = array('drupal1', 'drupal2' => 'drupal3', 'drupal4' => array('drupal5', 'drupal6')); + + cache_set('test_object', $test_object, 'cache'); + $cache = cache_get('test_object', 'cache'); + $this->assertTrue(isset($cache->data) && $cache->data == $test_object, t('Object is saved and restored properly.')); + } + + /* + * Check or a variable is stored and restored properly. + **/ + function checkVariable($var) { + cache_set('test_var', $var, 'cache'); + $cache = cache_get('test_var', 'cache'); + $this->assertTrue(isset($cache->data) && $cache->data === $var, t('@type is saved and restored properly.', array('@type' => ucfirst(gettype($var))))); + } +} + +/** + * Test cache clearing methods. + */ +class MemCacheClearCase extends MemcacheTestCase { + public static function getInfo() { + return array( + 'name' => 'Cache clear test', + 'description' => 'Check our clearing is done the proper way.', + 'group' => 'Memcache' + ); + } + + function setUp() { + $this->default_bin = 'cache_page'; + $this->default_value = $this->randomName(10); + + parent::setUp(); + } + + + /** + * Test clearing the cache with a cid, no cache lifetime. + */ + function testClearCidNoLifetime() { + $this->clearCidTest(); + } + + /** + * Test clearing the cache with a cid, with cache lifetime. + */ + function testClearCidLifetime() { + variable_set('cache_lifetime', 6000); + $this->clearCidTest(); + } + + /** + * Test clearing using wildcard prefixes, no cache lifetime. + */ + function testClearWildcardNoLifetime() { + $this->clearWildcardPrefixTest(); + } + + /** + * Test clearing using wildcard prefix, with cache lifetime. + */ + function testClearWildcardLifetime() { + variable_set('cache_lifetime', 6000); + $this->clearWildcardPrefixTest(); + } + + /** + * Test full bin flushes with no cache lifetime. + */ + function testClearWildcardFull() { + variable_set("cache_flush_$this->default_bin", 0); + cache_set('test_cid_clear1', $this->default_value, $this->default_bin); + cache_set('test_cid_clear2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches were created for checking cid "*" with wildcard true.')); + cache_clear_all('*', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) + || $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches removed after clearing cid "*" with wildcard true.')); + } + + /** + * Test full bin flushes with cache lifetime. + */ + function testClearCacheLifetime() { + variable_set("cache_flush_$this->default_bin", 0); + variable_set('cache_lifetime', 600); + + // Set a cache item with an expiry. + cache_set('test_cid', $this->default_value, $this->default_bin, time() + 3600); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was created successfully.'); + + // Set a permanent cache item. + cache_set('test_cid_2', $this->default_value, $this->default_bin); + + // Clear the page and block caches. + cache_clear_all(); + // Since the cache was cleared within the current session, cache_get() + // should return false. + $this->assertFalse($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was cleared successfully.'); + + // However permament items should stay in place. + $this->assertTrue($this->checkCacheExists('test_cid_2', $this->default_value), 'Cache item was not cleared'); + + // If $_SESSION['cache_flush'] is not set, then the expired item should be returned. + unset($_SESSION['cache_flush']); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item is still returned due to minimum cache lifetime.'); + + // Set a much shorter cache lifetime. + variable_set('cache_content_flush_' . $this->default_bin, 0); + variable_set('cache_lifetime', 1); + cache_set('test_cid_1', $this->default_value, $this->default_bin, time() + 6000); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was created successfully.'); + sleep(2); + cache_clear_all(); + $this->assertFalse($this->checkCacheExists('test_cid', $this->default_value), 'Cache item is not returned once minimum cache lifetime has expired.'); + + // Reset the cache clear variables. + variable_set('cache_content_flush_' . $this->default_bin, 0); + variable_set('cache_lifetime', 6000); + sleep(1); + + // Confirm that cache_lifetime does not take effect for full bin flushes. + cache_set('test_cid', $this->default_value, $this->default_bin, time() + 6000); + $this->assertTrue($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was created successfully.'); + cache_set('test_cid_2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_2', $this->default_value), 'Cache item was created successfully.'); + + // Now flush the bin. + cache_clear_all('*', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid', $this->default_value), 'Cache item was cleared successfully.'); + $this->assertFalse($this->checkCacheExists('test_cid_2', $this->default_value), 'Cache item was cleared successfully.'); + } + + + /** + * Test clearing using a cid. + */ + function clearCidTest() { + variable_set("cache_flush_$this->default_bin", 0); + cache_set('test_cid_clear', $this->default_value, $this->default_bin); + + $this->assertCacheExists(t('Cache was set for clearing cid.'), $this->default_value, 'test_cid_clear'); + cache_clear_all('test_cid_clear', $this->default_bin); + + $this->assertCacheRemoved(t('Cache was removed after clearing cid.'), 'test_cid_clear'); + + cache_set('test_cid_clear1', $this->default_value, $this->default_bin); + cache_set('test_cid_clear2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches were created for checking cid "*" with wildcard false.')); + cache_clear_all('*', $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches still exists after clearing cid "*" with wildcard false.')); + } + + /** + * Test cache clears using wildcard prefixes. + */ + function clearWildcardPrefixTest() { + variable_set("cache_flush_$this->default_bin", 0); + cache_set('test_cid_clear1', $this->default_value, $this->default_bin); + cache_set('test_cid_clear2', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches were created for checking cid substring with wildcard true.')); + cache_clear_all('test_', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) + || $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches removed after clearing cid substring with wildcard true.')); + // Test for the case where a wildcard object disappears, for example a + // partial memcache restart or eviction. + cache_set('test_cid_clear1', $this->default_value, $this->default_bin); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value), 'The cache was created successfully.'); + cache_clear_all('test_', $this->default_bin, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value), 'The cache was cleared successfully.'); + // Delete the wildcard manually to simulate an eviction. + $wildcard = '.wildcard-' . 'test_'; + dmemcache_delete($wildcard, $this->default_bin); + // Reset the memcache_wildcards() static cache. + memcache_wildcards(FALSE, FALSE, FALSE, TRUE); + $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value), 'The cache was cleared successfully.'); + } +} diff --git a/sites/all/modules/memcache/tests/memcache_test.info b/sites/all/modules/memcache/tests/memcache_test.info new file mode 100644 index 00000000..5669c98d --- /dev/null +++ b/sites/all/modules/memcache/tests/memcache_test.info @@ -0,0 +1,12 @@ +name = Memcache test +description = Support module for memcache testing. +package = Testing +core = 7.x +hidden = TRUE + +; Information added by drupal.org packaging script on 2012-01-07 +version = "7.x-1.0-rc3" +core = "7.x" +project = "memcache" +datestamp = "1325917859" + diff --git a/sites/all/modules/memcache/tests/memcache_test.module b/sites/all/modules/memcache/tests/memcache_test.module new file mode 100644 index 00000000..f0f3956b --- /dev/null +++ b/sites/all/modules/memcache/tests/memcache_test.module @@ -0,0 +1,129 @@ +<?php + +/** + * Implements hook_menu(). + */ +function memcache_test_menu() { + $items['memcache-test/lock-acquire'] = array( + 'title' => 'Lock acquire', + 'page callback' => 'memcache_test_lock_acquire', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['memcache-test/lock-exit'] = array( + 'title' => 'Lock acquire then exit', + 'page callback' => 'memcache_test_lock_exit', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['memcache-test/set/%/%'] = array( + 'title' => 'Set a value with a key', + 'page callback' => 'memcache_test_set', + 'page arguments' => array(2, 3), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['memcache-test/get/%'] = array( + 'title' => 'Get a value from the cache', + 'page callback' => 'memcache_test_get', + 'page arguments' => array(2), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['memcache-test/wildcard-clear/%'] = array( + 'title' => 'Clear the cache with a wildcard', + 'page callback' => 'memcache_test_wildcard_flush', + 'page arguments' => array(2), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['memcache-test/clear/%'] = array( + 'title' => 'Clear the cache with a wildcard', + 'page callback' => 'memcache_test_clear', + 'page arguments' => array(2), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['memcache-test/clear-cache'] = array( + 'title' => 'Clear the cache with a wildcard', + 'page callback' => 'memcache_test_clear_cache', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Try to acquire a named lock and report the outcome. + */ +function memcache_test_lock_acquire() { + if (lock_acquire('memcache_test_lock_acquire')) { + lock_release('memcache_test_lock_acquire'); + return 'TRUE: Lock successfully acquired in memcache_test_lock_acquire()'; + } + else { + return 'FALSE: Lock not acquired in memcache_test_lock_acquire()'; + } +} + +/** + * Try to acquire a specific lock, and then exit. + */ +function memcache_test_lock_exit() { + if (lock_acquire('memcache_test_lock_exit', 900)) { + echo 'TRUE: Lock successfully acquired in memcache_test_lock_exit()'; + // The shut-down function should release the lock. + exit(); + } + else { + return 'FALSE: Lock not acquired in memcache_test_lock_exit()'; + } +} + +/** + * Set a value into the cache. + */ +function memcache_test_set($key, $value) { + $cache = cache_set($key, $value, 'memcache'); + drupal_json_output(cache_get($key, 'memcache')); +} + +/** + * Set a value into the cache. + */ +function memcache_test_get($key) { + $GLOBALS['in_test'] = TRUE; + drupal_json_output(cache_get($key, 'memcache')); +} + +/** + * Clear cache using a wildcard. + */ +function memcache_test_wildcard_flush($key) { + cache_clear_all($key, 'memcache', TRUE); + drupal_json_output($key); +} + +/** + * Clear cache using a specific key. + */ +function memcache_test_clear($key) { + cache_clear_all($key, 'memcache'); + + drupal_json_output($key); +} + +/** +* Clear complete cache. +*/ +function memcache_test_clear_cache() { + cache_clear_all(NULL, 'memcache'); + drupal_json_output(); +} diff --git a/sites/all/modules/memcache/memcache-session.inc b/sites/all/modules/memcache/unstable/memcache-session.inc similarity index 99% rename from sites/all/modules/memcache/memcache-session.inc rename to sites/all/modules/memcache/unstable/memcache-session.inc index a0137aee..ee56e57c 100644 --- a/sites/all/modules/memcache/memcache-session.inc +++ b/sites/all/modules/memcache/unstable/memcache-session.inc @@ -1,14 +1,17 @@ <?php -require_once dirname(__FILE__) . '/dmemcache.inc'; - /** * @file * User session handling functions. * + * Note session support is not fully ported to Drupal 7 and is not recommended + * for production sites. + * * An alternative to includes/session.inc. */ +require_once dirname(__FILE__) . '/dmemcache.inc'; + /** * Implement hook_user() using a required module's namespace since memcache is * not a module and thus can't implement hooks directly. -- GitLab