Merge pull request #15875 from civicrm/5.20
[civicrm-core.git] / CRM / Utils / Cache / Tiered.php
CommitLineData
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 */
47class 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}