Queue - When UserJob.queue_id works down to zero tasks, update status and fire hook
[civicrm-core.git] / CRM / Core / PrevNextCache / Redis.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
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 */
24 class 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);
44 $this->prefix = $settings['prefix'] ?? '';
45 $this->prefix .= \CRM_Utils_Cache::DELIMITER . 'prevnext' . \CRM_Utils_Cache::DELIMITER;
46 }
47
48 public function fillWithSql($cacheKey, $sql, $sqlParams = []) {
49 $dao = CRM_Core_DAO::executeQuery($sql, $sqlParams, FALSE);
50
51 [$allKey, $dataKey, , $maxScore] = $this->initCacheKey($cacheKey);
52
53 while ($dao->fetch()) {
54 [, $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
60 return TRUE;
61 }
62
63 public function fillWithArray($cacheKey, $rows) {
64 [$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->del($selKey);
92 $this->redis->expire($selKey, self::TTL);
93 }
94 elseif ($action === 'unselect' && $ids !== NULL) {
95 foreach ((array) $ids as $id) {
96 $this->redis->zRem($selKey, $id);
97 }
98 }
99 }
100
101 /**
102 * @throws \CRM_Core_Exception
103 */
104 public function getSelection($cacheKey, $action = 'get') {
105 $allKey = $this->key($cacheKey, 'all');
106 $selKey = $this->key($cacheKey, 'sel');
107
108 if ($action === 'get') {
109 $result = [];
110 foreach ($this->redis->zRange($selKey, 0, -1) as $entity_id) {
111 $result[$entity_id] = 1;
112 }
113 return [$cacheKey => $result];
114 }
115 elseif ($action === 'getall') {
116 $result = [];
117 foreach ($this->redis->zRange($allKey, 0, -1) as $entity_id) {
118 $result[$entity_id] = 1;
119 }
120 return [$cacheKey => $result];
121 }
122 else {
123 throw new \CRM_Core_Exception("Unrecognized action: $action");
124 }
125 }
126
127 public function getPositions($cacheKey, $id1) {
128 $allKey = $this->key($cacheKey, 'all');
129 $dataKey = $this->key($cacheKey, 'data');
130
131 $rank = $this->redis->zRank($allKey, $id1);
132 if (!is_int($rank) || $rank < 0) {
133 return ['foundEntry' => 0];
134 }
135
136 $pos = ['foundEntry' => 1];
137
138 if ($rank > 0) {
139 $pos['prev'] = [];
140 foreach ($this->redis->zRange($allKey, $rank - 1, $rank - 1) as $value) {
141 $pos['prev']['id1'] = $value;
142 }
143 $pos['prev']['data'] = $this->redis->hGet($dataKey, $pos['prev']['id1']);
144 }
145
146 $count = $this->getCount($cacheKey);
147 if ($count > $rank + 1) {
148 $pos['next'] = [];
149 foreach ($this->redis->zRange($allKey, $rank + 1, $rank + 1) as $value) {
150 $pos['next']['id1'] = $value;
151 }
152 $pos['next']['data'] = $this->redis->hGet($dataKey, $pos['next']['id1']);
153 }
154
155 return $pos;
156 }
157
158 /**
159 * @throws \CRM_Core_Exception
160 */
161 public function deleteItem($id = NULL, $cacheKey = NULL) {
162 if ($id === NULL && $cacheKey !== NULL) {
163 // Delete by cacheKey.
164 $allKey = $this->key($cacheKey, 'all');
165 $selKey = $this->key($cacheKey, 'sel');
166 $dataKey = $this->key($cacheKey, 'data');
167 $this->redis->del($allKey, $selKey, $dataKey);
168 }
169 elseif ($id === NULL && $cacheKey === NULL) {
170 // Delete everything.
171 $keys = $this->redis->keys($this->prefix . '*');
172 $this->redis->del($keys);
173 }
174 elseif ($id !== NULL && $cacheKey !== NULL) {
175 // Delete a specific contact, within a specific cache.
176 $this->redis->zRem($this->key($cacheKey, 'all'), $id);
177 $this->redis->zRem($this->key($cacheKey, 'sel'), $id);
178 $this->redis->hDel($this->key($cacheKey, 'data'), $id);
179 }
180 elseif ($id !== NULL && $cacheKey === NULL) {
181 // Delete a specific contact, across all prevnext caches.
182 $allKeys = $this->redis->keys($this->key('*', 'all'));
183 foreach ($allKeys as $allKey) {
184 $parts = explode(\CRM_Utils_Cache::DELIMITER, $allKey);
185 array_pop($parts);
186 $tmpCacheKey = array_pop($parts);
187 $this->deleteItem($id, $tmpCacheKey);
188 }
189 }
190 else {
191 throw new CRM_Core_Exception('Not implemented: Redis::deleteItem');
192 }
193 }
194
195 public function getCount($cacheKey) {
196 $allKey = $this->key($cacheKey, 'all');
197 return $this->redis->zCard($allKey);
198 }
199
200 /**
201 * Construct the full path to a cache item.
202 *
203 * @param string $cacheKey
204 * Identifier for this saved search.
205 * Ex: 'abcd1234abcd1234'.
206 * @param string $item
207 * Ex: 'list', 'rel', 'data'.
208 * @return string
209 * Ex: 'dmaster/prevnext/abcd1234abcd1234/list'
210 */
211 private function key($cacheKey, $item) {
212 return $this->prefix . $cacheKey . \CRM_Utils_Cache::DELIMITER . $item;
213 }
214
215 /**
216 * Initialize any data-structures or timeouts for the cache-key.
217 *
218 * This is non-destructive -- if data already exists, it's preserved.
219 *
220 * @return array
221 * 0 => string $allItemsCacheKey,
222 * 1 => string $dataItemsCacheKey,
223 * 2 => string $selectedItemsCacheKey,
224 * 3 => int $maxExistingScore
225 */
226 private function initCacheKey($cacheKey) {
227 $allKey = $this->key($cacheKey, 'all');
228 $selKey = $this->key($cacheKey, 'sel');
229 $dataKey = $this->key($cacheKey, 'data');
230
231 $this->redis->expire($allKey, self::TTL);
232 $this->redis->expire($dataKey, self::TTL);
233 $this->redis->expire($selKey, self::TTL);
234
235 $maxScore = 0;
236 foreach ($this->redis->zRange($allKey, -1, -1, TRUE) as $lastElem => $lastScore) {
237 $maxScore = $lastScore;
238 }
239 return [$allKey, $dataKey, $selKey, $maxScore];
240 }
241
242 /**
243 * @inheritDoc
244 */
245 public function cleanup() {
246 // Redis already handles cleaning up stale keys.
247 return;
248 }
249
250 }