Commit | Line | Data |
---|---|---|
3768d7a0 TO |
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 | ||
6714d8d2 SL |
65 | // TODO Consider native implementation. |
66 | use CRM_Utils_Cache_NaiveMultipleTrait; | |
3768d7a0 TO |
67 | |
68 | /** | |
69 | * @var array | |
70 | * Array(int $tierNum => int $seconds). | |
71 | */ | |
72 | protected $maxTimeouts; | |
73 | ||
74 | /** | |
75 | * @var array | |
76 | * List of cache instances, with fastest/closest first. | |
77 | * Array(int $tierNum => CRM_Utils_Cache_Interface). | |
78 | */ | |
79 | protected $tiers; | |
80 | ||
81 | /** | |
82 | * CRM_Utils_Cache_Tiered constructor. | |
83 | * @param array $tiers | |
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 | |
91 | */ | |
92 | public function __construct($tiers, $maxTimeouts = [86400]) { | |
93 | $this->tiers = $tiers; | |
94 | $this->maxTimeouts = []; | |
95 | ||
96 | foreach ($tiers as $k => $tier) { | |
97 | $this->maxTimeouts[$k] = isset($maxTimeouts[$k]) | |
98 | ? $maxTimeouts[$k] | |
99 | : $this->maxTimeouts[$k - 1]; | |
100 | } | |
101 | ||
102 | for ($far = 1; $far < count($tiers); $far++) { | |
103 | $near = $far - 1; | |
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}"); | |
106 | } | |
107 | } | |
108 | } | |
109 | ||
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"); | |
113 | } | |
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)) { | |
119 | return FALSE; | |
120 | } | |
121 | } | |
122 | return TRUE; | |
123 | } | |
124 | ||
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); | |
137 | } | |
138 | return $value; | |
139 | } | |
140 | } | |
141 | return $default; | |
142 | } | |
143 | ||
144 | public function delete($key) { | |
145 | foreach ($this->tiers as $tier) { | |
146 | /** @var CRM_Utils_Cache_Interface $tier */ | |
147 | $tier->delete($key); | |
148 | } | |
149 | return TRUE; | |
150 | } | |
151 | ||
152 | public function flush() { | |
153 | return $this->clear(); | |
154 | } | |
155 | ||
156 | public function clear() { | |
157 | foreach ($this->tiers as $tier) { | |
158 | /** @var CRM_Utils_Cache_Interface $tier */ | |
159 | if (!$tier->clear()) { | |
160 | return FALSE; | |
161 | } | |
162 | } | |
163 | return TRUE; | |
164 | } | |
165 | ||
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()) { | |
172 | return TRUE; | |
173 | } | |
174 | } | |
175 | return FALSE; | |
176 | } | |
177 | ||
178 | protected function getEffectiveTtl($tierNum, $ttl) { | |
179 | if ($ttl === NULL) { | |
180 | return $this->maxTimeouts[$tierNum]; | |
181 | } | |
182 | else { | |
183 | return min($this->maxTimeouts[$tierNum], $ttl); | |
184 | } | |
185 | } | |
186 | ||
187 | } |