Merge pull request #13507 from twomice/lab686_membership_stats_columns
[civicrm-core.git] / CRM / Utils / Cache / Tiered.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
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. |
13 | |
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. |
18 | |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33
34 /**
35 * Class CRM_Utils_Cache_Tiered
36 *
37 * `Tiered` implements a hierarchy of fast and slow caches. For example, you
38 * might have a configuration in which:
39 *
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).
43 *
44 * Cached data will be written to all three tiers. When reading, you'll hit the
45 * fastest available tier.
46 *
47 * The example would be created with:
48 *
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]);
54 *
55 * Note:
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.
62 */
63 class CRM_Utils_Cache_Tiered implements CRM_Utils_Cache_Interface {
64
65 use CRM_Utils_Cache_NaiveMultipleTrait; // TODO Consider native implementation.
66
67 /**
68 * @var array
69 * Array(int $tierNum => int $seconds).
70 */
71 protected $maxTimeouts;
72
73 /**
74 * @var array
75 * List of cache instances, with fastest/closest first.
76 * Array(int $tierNum => CRM_Utils_Cache_Interface).
77 */
78 protected $tiers;
79
80 /**
81 * CRM_Utils_Cache_Tiered constructor.
82 * @param array $tiers
83 * List of cache instances, with fastest/closest first.
84 * Must be indexed numerically (0, 1, 2...).
85 * @param array $maxTimeouts
86 * A list of maximum timeouts for each cache-tier.
87 * There must be at least one value in this array.
88 * If timeouts are omitted for slower tiers, they are filled in with the last value.
89 * @throws CRM_Core_Exception
90 */
91 public function __construct($tiers, $maxTimeouts = [86400]) {
92 $this->tiers = $tiers;
93 $this->maxTimeouts = [];
94
95 foreach ($tiers as $k => $tier) {
96 $this->maxTimeouts[$k] = isset($maxTimeouts[$k])
97 ? $maxTimeouts[$k]
98 : $this->maxTimeouts[$k - 1];
99 }
100
101 for ($far = 1; $far < count($tiers); $far++) {
102 $near = $far - 1;
103 if ($this->maxTimeouts[$near] > $this->maxTimeouts[$far]) {
104 throw new \CRM_Core_Exception("Invalid configuration: Near cache #{$near} has longer timeout than far cache #{$far}");
105 }
106 }
107 }
108
109 public function set($key, $value, $ttl = NULL) {
110 if ($ttl !== NULL & !is_int($ttl) && !($ttl instanceof DateInterval)) {
111 throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache TTL");
112 }
113 foreach ($this->tiers as $tierNum => $tier) {
114 /** @var CRM_Utils_Cache_Interface $tier */
115 $effTtl = $this->getEffectiveTtl($tierNum, $ttl);
116 $expiresAt = CRM_Utils_Date::convertCacheTtlToExpires($effTtl, $this->maxTimeouts[$tierNum]);
117 if (!$tier->set($key, [0 => $expiresAt, 1 => $value], $effTtl)) {
118 return FALSE;
119 }
120 }
121 return TRUE;
122 }
123
124 public function get($key, $default = NULL) {
125 $nack = CRM_Utils_Cache::nack();
126 foreach ($this->tiers as $readTierNum => $tier) {
127 /** @var CRM_Utils_Cache_Interface $tier */
128 $wrapped = $tier->get($key, $nack);
129 if ($wrapped !== $nack && $wrapped[0] >= CRM_Utils_Time::getTimeRaw()) {
130 list ($parentExpires, $value) = $wrapped;
131 // (Re)populate the faster caches; and then return the value we found.
132 for ($i = 0; $i < $readTierNum; $i++) {
133 $now = CRM_Utils_Time::getTimeRaw();
134 $effExpires = min($parentExpires, $now + $this->maxTimeouts[$i]);
135 $this->tiers[$i]->set($key, [0 => $effExpires, 1 => $value], $effExpires - $now);
136 }
137 return $value;
138 }
139 }
140 return $default;
141 }
142
143 public function delete($key) {
144 foreach ($this->tiers as $tier) {
145 /** @var CRM_Utils_Cache_Interface $tier */
146 $tier->delete($key);
147 }
148 return TRUE;
149 }
150
151 public function flush() {
152 return $this->clear();
153 }
154
155 public function clear() {
156 foreach ($this->tiers as $tier) {
157 /** @var CRM_Utils_Cache_Interface $tier */
158 if (!$tier->clear()) {
159 return FALSE;
160 }
161 }
162 return TRUE;
163 }
164
165 public function has($key) {
166 $nack = CRM_Utils_Cache::nack();
167 foreach ($this->tiers as $tier) {
168 /** @var CRM_Utils_Cache_Interface $tier */
169 $wrapped = $tier->get($key, $nack);
170 if ($wrapped !== $nack && $wrapped[0] > CRM_Utils_Time::getTimeRaw()) {
171 return TRUE;
172 }
173 }
174 return FALSE;
175 }
176
177 protected function getEffectiveTtl($tierNum, $ttl) {
178 if ($ttl === NULL) {
179 return $this->maxTimeouts[$tierNum];
180 }
181 else {
182 return min($this->maxTimeouts[$tierNum], $ttl);
183 }
184 }
185
186 }