Commit | Line | Data |
---|---|---|
3768d7a0 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
bc77d7c0 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
3768d7a0 | 5 | | | |
bc77d7c0 TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
3768d7a0 TO |
9 | +--------------------------------------------------------------------+ |
10 | */ | |
11 | ||
12 | /** | |
13 | * | |
14 | * @package CRM | |
ca5cec67 | 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
3768d7a0 TO |
16 | */ |
17 | ||
18 | /** | |
19 | * Class CRM_Utils_Cache_Tiered | |
20 | * | |
21 | * `Tiered` implements a hierarchy of fast and slow caches. For example, you | |
22 | * might have a configuration in which: | |
23 | * | |
24 | * - A local/in-memory array caches info for up to 1 minute (60s). | |
25 | * - A Redis cache retains info for up to 10 minutes (600s). | |
26 | * - A SQL cache retains info for up to 1 hour (3600s). | |
27 | * | |
28 | * Cached data will be written to all three tiers. When reading, you'll hit the | |
29 | * fastest available tier. | |
30 | * | |
31 | * The example would be created with: | |
32 | * | |
33 | * $cache = new CRM_Utils_Cache_Tiered([ | |
34 | * new CRM_Utils_Cache_ArrayCache(...), | |
35 | * new CRM_Utils_Cache_Redis(...), | |
36 | * new CRM_Utils_Cache_SqlGroup(...), | |
37 | * ], [60, 600, 3600]); | |
38 | * | |
39 | * Note: | |
40 | * - Correctly implementing PSR-16 leads to a small amount of CPU+mem overhead. | |
41 | * If you need an extremely high number of re-reads within a thread and can live | |
42 | * with only two tiers, try CRM_Utils_Cache_ArrayDecorator or | |
43 | * CRM_Utils_Cache_FastArrayDecorator instead. | |
44 | * - With the exception of unit-testing, you should not access the underlying | |
45 | * tiers directly. The data-format may be different than your expectation. | |
46 | */ | |
47 | class CRM_Utils_Cache_Tiered implements CRM_Utils_Cache_Interface { | |
48 | ||
6714d8d2 SL |
49 | // TODO Consider native implementation. |
50 | use CRM_Utils_Cache_NaiveMultipleTrait; | |
3768d7a0 TO |
51 | |
52 | /** | |
53 | * @var array | |
54 | * Array(int $tierNum => int $seconds). | |
55 | */ | |
56 | protected $maxTimeouts; | |
57 | ||
58 | /** | |
59 | * @var array | |
60 | * List of cache instances, with fastest/closest first. | |
61 | * Array(int $tierNum => CRM_Utils_Cache_Interface). | |
62 | */ | |
63 | protected $tiers; | |
64 | ||
65 | /** | |
66 | * CRM_Utils_Cache_Tiered constructor. | |
67 | * @param array $tiers | |
68 | * List of cache instances, with fastest/closest first. | |
69 | * Must be indexed numerically (0, 1, 2...). | |
70 | * @param array $maxTimeouts | |
71 | * A list of maximum timeouts for each cache-tier. | |
72 | * There must be at least one value in this array. | |
73 | * If timeouts are omitted for slower tiers, they are filled in with the last value. | |
74 | * @throws CRM_Core_Exception | |
75 | */ | |
76 | public function __construct($tiers, $maxTimeouts = [86400]) { | |
77 | $this->tiers = $tiers; | |
78 | $this->maxTimeouts = []; | |
79 | ||
80 | foreach ($tiers as $k => $tier) { | |
81 | $this->maxTimeouts[$k] = isset($maxTimeouts[$k]) | |
82 | ? $maxTimeouts[$k] | |
83 | : $this->maxTimeouts[$k - 1]; | |
84 | } | |
85 | ||
86 | for ($far = 1; $far < count($tiers); $far++) { | |
87 | $near = $far - 1; | |
88 | if ($this->maxTimeouts[$near] > $this->maxTimeouts[$far]) { | |
89 | throw new \CRM_Core_Exception("Invalid configuration: Near cache #{$near} has longer timeout than far cache #{$far}"); | |
90 | } | |
91 | } | |
92 | } | |
93 | ||
94 | public function set($key, $value, $ttl = NULL) { | |
95 | if ($ttl !== NULL & !is_int($ttl) && !($ttl instanceof DateInterval)) { | |
96 | throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache TTL"); | |
97 | } | |
98 | foreach ($this->tiers as $tierNum => $tier) { | |
99 | /** @var CRM_Utils_Cache_Interface $tier */ | |
100 | $effTtl = $this->getEffectiveTtl($tierNum, $ttl); | |
101 | $expiresAt = CRM_Utils_Date::convertCacheTtlToExpires($effTtl, $this->maxTimeouts[$tierNum]); | |
102 | if (!$tier->set($key, [0 => $expiresAt, 1 => $value], $effTtl)) { | |
103 | return FALSE; | |
104 | } | |
105 | } | |
106 | return TRUE; | |
107 | } | |
108 | ||
109 | public function get($key, $default = NULL) { | |
110 | $nack = CRM_Utils_Cache::nack(); | |
111 | foreach ($this->tiers as $readTierNum => $tier) { | |
112 | /** @var CRM_Utils_Cache_Interface $tier */ | |
113 | $wrapped = $tier->get($key, $nack); | |
114 | if ($wrapped !== $nack && $wrapped[0] >= CRM_Utils_Time::getTimeRaw()) { | |
115 | list ($parentExpires, $value) = $wrapped; | |
116 | // (Re)populate the faster caches; and then return the value we found. | |
117 | for ($i = 0; $i < $readTierNum; $i++) { | |
118 | $now = CRM_Utils_Time::getTimeRaw(); | |
119 | $effExpires = min($parentExpires, $now + $this->maxTimeouts[$i]); | |
120 | $this->tiers[$i]->set($key, [0 => $effExpires, 1 => $value], $effExpires - $now); | |
121 | } | |
122 | return $value; | |
123 | } | |
124 | } | |
125 | return $default; | |
126 | } | |
127 | ||
128 | public function delete($key) { | |
129 | foreach ($this->tiers as $tier) { | |
130 | /** @var CRM_Utils_Cache_Interface $tier */ | |
131 | $tier->delete($key); | |
132 | } | |
133 | return TRUE; | |
134 | } | |
135 | ||
136 | public function flush() { | |
137 | return $this->clear(); | |
138 | } | |
139 | ||
140 | public function clear() { | |
141 | foreach ($this->tiers as $tier) { | |
142 | /** @var CRM_Utils_Cache_Interface $tier */ | |
143 | if (!$tier->clear()) { | |
144 | return FALSE; | |
145 | } | |
146 | } | |
147 | return TRUE; | |
148 | } | |
149 | ||
150 | public function has($key) { | |
151 | $nack = CRM_Utils_Cache::nack(); | |
152 | foreach ($this->tiers as $tier) { | |
153 | /** @var CRM_Utils_Cache_Interface $tier */ | |
154 | $wrapped = $tier->get($key, $nack); | |
155 | if ($wrapped !== $nack && $wrapped[0] > CRM_Utils_Time::getTimeRaw()) { | |
156 | return TRUE; | |
157 | } | |
158 | } | |
159 | return FALSE; | |
160 | } | |
161 | ||
162 | protected function getEffectiveTtl($tierNum, $ttl) { | |
163 | if ($ttl === NULL) { | |
164 | return $this->maxTimeouts[$tierNum]; | |
165 | } | |
166 | else { | |
167 | return min($this->maxTimeouts[$tierNum], $ttl); | |
168 | } | |
169 | } | |
170 | ||
171 | } |