3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * Class CRM_Utils_Cache_Tiered
21 * `Tiered` implements a hierarchy of fast and slow caches. For example, you
22 * might have a configuration in which:
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).
28 * Cached data will be written to all three tiers. When reading, you'll hit the
29 * fastest available tier.
31 * The example would be created with:
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]);
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.
47 class CRM_Utils_Cache_Tiered
implements CRM_Utils_Cache_Interface
{
49 // TODO Consider native implementation.
50 use CRM_Utils_Cache_NaiveMultipleTrait
;
54 * Array(int $tierNum => int $seconds).
56 protected $maxTimeouts;
60 * List of cache instances, with fastest/closest first.
61 * Array(int $tierNum => CRM_Utils_Cache_Interface).
66 * CRM_Utils_Cache_Tiered constructor.
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
76 public function __construct($tiers, $maxTimeouts = [86400]) {
77 $this->tiers
= $tiers;
78 $this->maxTimeouts
= [];
80 foreach ($tiers as $k => $tier) {
81 $this->maxTimeouts
[$k] = isset($maxTimeouts[$k])
83 : $this->maxTimeouts
[$k - 1];
86 for ($far = 1; $far < count($tiers); $far++
) {
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}");
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");
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)) {
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);
128 public function delete($key) {
129 foreach ($this->tiers
as $tier) {
130 /** @var CRM_Utils_Cache_Interface $tier */
136 public function flush() {
137 return $this->clear();
140 public function clear() {
141 foreach ($this->tiers
as $tier) {
142 /** @var CRM_Utils_Cache_Interface $tier */
143 if (!$tier->clear()) {
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()) {
162 protected function getEffectiveTtl($tierNum, $ttl) {
164 return $this->maxTimeouts
[$tierNum];
167 return min($this->maxTimeouts
[$tierNum], $ttl);