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 | ||
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 | } |