Merge pull request #19766 from WeMoveEU/faster-select2-groups
[civicrm-core.git] / CRM / Core / PrevNextCache / Redis.php
CommitLineData
751f3d98
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
751f3d98 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 |
751f3d98
TO
9 +--------------------------------------------------------------------+
10 */
11
12/**
13 * Class CRM_Core_PrevNextCache_Memory
14 *
15 * Store the previous/next cache in a Redis set.
16 *
17 * Each logical prev-next cache corresponds to three distinct items in Redis:
18 * - "{prefix}/{qfKey}/list" - Sorted set of `entity_id`, with all entities
19 * - "{prefix}/{qfkey}/sel" - Sorted set of `entity_id`, with only entities marked by user
20 * - "{prefix}/{qfkey}/data" - Hash mapping from `entity_id` to `data`
21 *
22 * @link https://github.com/phpredis/phpredis
23 */
24class CRM_Core_PrevNextCache_Redis implements CRM_Core_PrevNextCache_Interface {
25
26 const TTL = 21600;
27
28 /**
29 * @var Redis
30 */
31 protected $redis;
32
33 /**
34 * @var string
35 */
36 protected $prefix;
37
38 /**
39 * CRM_Core_PrevNextCache_Redis constructor.
40 * @param array $settings
41 */
42 public function __construct($settings) {
43 $this->redis = CRM_Utils_Cache_Redis::connect($settings);
2e1f50d6 44 $this->prefix = $settings['prefix'] ?? '';
751f3d98
TO
45 $this->prefix .= \CRM_Utils_Cache::DELIMITER . 'prevnext' . \CRM_Utils_Cache::DELIMITER;
46 }
47
2c8b42d5 48 public function fillWithSql($cacheKey, $sql, $sqlParams = []) {
464b6086 49 $dao = CRM_Core_DAO::executeQuery($sql, $sqlParams, FALSE);
751f3d98
TO
50
51 list($allKey, $dataKey, , $maxScore) = $this->initCacheKey($cacheKey);
52
53 while ($dao->fetch()) {
54 list (, $entity_id, $data) = array_values($dao->toArray());
55 $maxScore++;
56 $this->redis->zAdd($allKey, $maxScore, $entity_id);
57 $this->redis->hSet($dataKey, $entity_id, $data);
58 }
59
751f3d98
TO
60 return TRUE;
61 }
62
63 public function fillWithArray($cacheKey, $rows) {
64 list($allKey, $dataKey, , $maxScore) = $this->initCacheKey($cacheKey);
65
66 foreach ($rows as $row) {
67 $maxScore++;
68 $this->redis->zAdd($allKey, $maxScore, $row['entity_id1']);
69 $this->redis->hSet($dataKey, $row['entity_id1'], $row['data']);
70 }
71
72 return TRUE;
73 }
74
75 public function fetch($cacheKey, $offset, $rowCount) {
76 $allKey = $this->key($cacheKey, 'all');
77 return $this->redis->zRange($allKey, $offset, $offset + $rowCount - 1);
78 }
79
80 public function markSelection($cacheKey, $action, $ids = NULL) {
81 $allKey = $this->key($cacheKey, 'all');
82 $selKey = $this->key($cacheKey, 'sel');
83
84 if ($action === 'select') {
85 foreach ((array) $ids as $id) {
86 $score = $this->redis->zScore($allKey, $id);
87 $this->redis->zAdd($selKey, $score, $id);
88 }
89 }
90 elseif ($action === 'unselect' && $ids === NULL) {
91 $this->redis->delete($selKey);
92 $this->redis->setTimeout($selKey, self::TTL);
93 }
94 elseif ($action === 'unselect' && $ids !== NULL) {
95 foreach ((array) $ids as $id) {
96 $this->redis->zDelete($selKey, $id);
97 }
98 }
99 }
100
101 public function getSelection($cacheKey, $action = 'get') {
102 $allKey = $this->key($cacheKey, 'all');
103 $selKey = $this->key($cacheKey, 'sel');
104
105 if ($action === 'get') {
106 $result = [];
107 foreach ($this->redis->zRange($selKey, 0, -1) as $entity_id) {
108 $result[$entity_id] = 1;
109 }
110 return [$cacheKey => $result];
111 }
112 elseif ($action === 'getall') {
113 $result = [];
114 foreach ($this->redis->zRange($allKey, 0, -1) as $entity_id) {
115 $result[$entity_id] = 1;
116 }
117 return [$cacheKey => $result];
118 }
119 else {
120 throw new \CRM_Core_Exception("Unrecognized action: $action");
121 }
122 }
123
124 public function getPositions($cacheKey, $id1) {
125 $allKey = $this->key($cacheKey, 'all');
126 $dataKey = $this->key($cacheKey, 'data');
127
128 $rank = $this->redis->zRank($allKey, $id1);
129 if (!is_int($rank) || $rank < 0) {
130 return ['foundEntry' => 0];
131 }
132
133 $pos = ['foundEntry' => 1];
134
135 if ($rank > 0) {
136 $pos['prev'] = [];
137 foreach ($this->redis->zRange($allKey, $rank - 1, $rank - 1) as $value) {
138 $pos['prev']['id1'] = $value;
139 }
140 $pos['prev']['data'] = $this->redis->hGet($dataKey, $pos['prev']['id1']);
141 }
142
143 $count = $this->getCount($cacheKey);
144 if ($count > $rank + 1) {
145 $pos['next'] = [];
146 foreach ($this->redis->zRange($allKey, $rank + 1, $rank + 1) as $value) {
147 $pos['next']['id1'] = $value;
148 }
149 $pos['next']['data'] = $this->redis->hGet($dataKey, $pos['next']['id1']);
150 }
151
152 return $pos;
153 }
154
155 public function deleteItem($id = NULL, $cacheKey = NULL) {
156 if ($id === NULL && $cacheKey !== NULL) {
157 // Delete by cacheKey.
158 $allKey = $this->key($cacheKey, 'all');
159 $selKey = $this->key($cacheKey, 'sel');
160 $dataKey = $this->key($cacheKey, 'data');
161 $this->redis->delete($allKey, $selKey, $dataKey);
162 }
163 elseif ($id === NULL && $cacheKey === NULL) {
164 // Delete everything.
165 $keys = $this->redis->keys($this->prefix . '*');
166 $this->redis->del($keys);
167 }
168 elseif ($id !== NULL && $cacheKey !== NULL) {
169 // Delete a specific contact, within a specific cache.
170 $this->redis->zDelete($this->key($cacheKey, 'all'), $id);
171 $this->redis->zDelete($this->key($cacheKey, 'sel'), $id);
172 $this->redis->hDel($this->key($cacheKey, 'data'), $id);
173 }
174 elseif ($id !== NULL && $cacheKey === NULL) {
175 // Delete a specific contact, across all prevnext caches.
176 $allKeys = $this->redis->keys($this->key('*', 'all'));
177 foreach ($allKeys as $allKey) {
178 $parts = explode(\CRM_Utils_Cache::DELIMITER, $allKey);
179 array_pop($parts);
180 $tmpCacheKey = array_pop($parts);
181 $this->deleteItem($id, $tmpCacheKey);
182 }
183 }
184 else {
185 throw new CRM_Core_Exception("Not implemented: Redis::deleteItem");
186 }
187 }
188
189 public function getCount($cacheKey) {
190 $allKey = $this->key($cacheKey, 'all');
191 return $this->redis->zSize($allKey);
192 }
193
194 /**
195 * Construct the full path to a cache item.
196 *
197 * @param string $cacheKey
198 * Identifier for this saved search.
199 * Ex: 'abcd1234abcd1234'.
200 * @param string $item
201 * Ex: 'list', 'rel', 'data'.
202 * @return string
203 * Ex: 'dmaster/prevnext/abcd1234abcd1234/list'
204 */
205 private function key($cacheKey, $item) {
206 return $this->prefix . $cacheKey . \CRM_Utils_Cache::DELIMITER . $item;
207 }
208
209 /**
210 * Initialize any data-structures or timeouts for the cache-key.
211 *
212 * This is non-destructive -- if data already exists, it's preserved.
213 *
214 * @return array
215 * 0 => string $allItemsCacheKey,
216 * 1 => string $dataItemsCacheKey,
217 * 2 => string $selectedItemsCacheKey,
218 * 3 => int $maxExistingScore
219 */
220 private function initCacheKey($cacheKey) {
221 $allKey = $this->key($cacheKey, 'all');
222 $selKey = $this->key($cacheKey, 'sel');
223 $dataKey = $this->key($cacheKey, 'data');
224
225 $this->redis->setTimeout($allKey, self::TTL);
226 $this->redis->setTimeout($dataKey, self::TTL);
227 $this->redis->setTimeout($selKey, self::TTL);
228
229 $maxScore = 0;
230 foreach ($this->redis->zRange($allKey, -1, -1, TRUE) as $lastElem => $lastScore) {
231 $maxScore = $lastScore;
232 }
be2fb01f 233 return [$allKey, $dataKey, $selKey, $maxScore];
751f3d98
TO
234 }
235
435fc552
SL
236 /**
237 * @inheritDoc
238 */
239 public function cleanup() {
240 // Redis already handles cleaning up stale keys.
241 return;
242 }
243
751f3d98 244}