3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2019
35 * Class CRM_Utils_Cache_Tiered
37 * `Tiered` implements a hierarchy of fast and slow caches. For example, you
38 * might have a configuration in which:
40 * - A local/in-memory array caches info for up to 1 minute (60s).
41 * - A Redis cache retains info for up to 10 minutes (600s).
42 * - A SQL cache retains info for up to 1 hour (3600s).
44 * Cached data will be written to all three tiers. When reading, you'll hit the
45 * fastest available tier.
47 * The example would be created with:
49 * $cache = new CRM_Utils_Cache_Tiered([
50 * new CRM_Utils_Cache_ArrayCache(...),
51 * new CRM_Utils_Cache_Redis(...),
52 * new CRM_Utils_Cache_SqlGroup(...),
53 * ], [60, 600, 3600]);
56 * - Correctly implementing PSR-16 leads to a small amount of CPU+mem overhead.
57 * If you need an extremely high number of re-reads within a thread and can live
58 * with only two tiers, try CRM_Utils_Cache_ArrayDecorator or
59 * CRM_Utils_Cache_FastArrayDecorator instead.
60 * - With the exception of unit-testing, you should not access the underlying
61 * tiers directly. The data-format may be different than your expectation.
63 class CRM_Utils_Cache_Tiered
implements CRM_Utils_Cache_Interface
{
65 // TODO Consider native implementation.
66 use CRM_Utils_Cache_NaiveMultipleTrait
;
70 * Array(int $tierNum => int $seconds).
72 protected $maxTimeouts;
76 * List of cache instances, with fastest/closest first.
77 * Array(int $tierNum => CRM_Utils_Cache_Interface).
82 * CRM_Utils_Cache_Tiered constructor.
84 * List of cache instances, with fastest/closest first.
85 * Must be indexed numerically (0, 1, 2...).
86 * @param array $maxTimeouts
87 * A list of maximum timeouts for each cache-tier.
88 * There must be at least one value in this array.
89 * If timeouts are omitted for slower tiers, they are filled in with the last value.
90 * @throws CRM_Core_Exception
92 public function __construct($tiers, $maxTimeouts = [86400]) {
93 $this->tiers
= $tiers;
94 $this->maxTimeouts
= [];
96 foreach ($tiers as $k => $tier) {
97 $this->maxTimeouts
[$k] = isset($maxTimeouts[$k])
99 : $this->maxTimeouts
[$k - 1];
102 for ($far = 1; $far < count($tiers); $far++
) {
104 if ($this->maxTimeouts
[$near] > $this->maxTimeouts
[$far]) {
105 throw new \
CRM_Core_Exception("Invalid configuration: Near cache #{$near} has longer timeout than far cache #{$far}");
110 public function set($key, $value, $ttl = NULL) {
111 if ($ttl !== NULL & !is_int($ttl) && !($ttl instanceof DateInterval
)) {
112 throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache TTL");
114 foreach ($this->tiers
as $tierNum => $tier) {
115 /** @var CRM_Utils_Cache_Interface $tier */
116 $effTtl = $this->getEffectiveTtl($tierNum, $ttl);
117 $expiresAt = CRM_Utils_Date
::convertCacheTtlToExpires($effTtl, $this->maxTimeouts
[$tierNum]);
118 if (!$tier->set($key, [0 => $expiresAt, 1 => $value], $effTtl)) {
125 public function get($key, $default = NULL) {
126 $nack = CRM_Utils_Cache
::nack();
127 foreach ($this->tiers
as $readTierNum => $tier) {
128 /** @var CRM_Utils_Cache_Interface $tier */
129 $wrapped = $tier->get($key, $nack);
130 if ($wrapped !== $nack && $wrapped[0] >= CRM_Utils_Time
::getTimeRaw()) {
131 list ($parentExpires, $value) = $wrapped;
132 // (Re)populate the faster caches; and then return the value we found.
133 for ($i = 0; $i < $readTierNum; $i++
) {
134 $now = CRM_Utils_Time
::getTimeRaw();
135 $effExpires = min($parentExpires, $now +
$this->maxTimeouts
[$i]);
136 $this->tiers
[$i]->set($key, [0 => $effExpires, 1 => $value], $effExpires - $now);
144 public function delete($key) {
145 foreach ($this->tiers
as $tier) {
146 /** @var CRM_Utils_Cache_Interface $tier */
152 public function flush() {
153 return $this->clear();
156 public function clear() {
157 foreach ($this->tiers
as $tier) {
158 /** @var CRM_Utils_Cache_Interface $tier */
159 if (!$tier->clear()) {
166 public function has($key) {
167 $nack = CRM_Utils_Cache
::nack();
168 foreach ($this->tiers
as $tier) {
169 /** @var CRM_Utils_Cache_Interface $tier */
170 $wrapped = $tier->get($key, $nack);
171 if ($wrapped !== $nack && $wrapped[0] > CRM_Utils_Time
::getTimeRaw()) {
178 protected function getEffectiveTtl($tierNum, $ttl) {
180 return $this->maxTimeouts
[$tierNum];
183 return min($this->maxTimeouts
[$tierNum], $ttl);