[NFC] various code cleanup on CRM_Contact_BAO_Query
[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 // TODO Consider native implementation.
66 use CRM_Utils_Cache_NaiveMultipleTrait;
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 }