• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

predis / predis / 15590381613

11 Jun 2025 04:28PM UTC coverage: 92.729% (-0.009%) from 92.738%
15590381613

push

github

web-flow
add XACK command (#1555)

* add XACK command

* add XACK to ClientContextInterface

* modify CHANGELOG.md

---------

Co-authored-by: aleksanders <alexander.s@seranking.com>
Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com>

4 of 4 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

7308 of 7881 relevant lines covered (92.73%)

111.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.48
/src/Cluster/SlotMap.php
1
<?php
2

3
/*
4
 * This file is part of the Predis package.
5
 *
6
 * (c) 2009-2020 Daniele Alessandri
7
 * (c) 2021-2025 Till Krüss
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace Predis\Cluster;
14

15
use ArrayAccess;
16
use ArrayIterator;
17
use Countable;
18
use IteratorAggregate;
19
use OutOfBoundsException;
20
use Predis\Connection\NodeConnectionInterface;
21
use ReturnTypeWillChange;
22
use Traversable;
23

24
/**
25
 * Compact slot map for redis-cluster.
26
 */
27
class SlotMap implements ArrayAccess, IteratorAggregate, Countable
28
{
29
    /**
30
     * Slot ranges list.
31
     *
32
     * @var SlotRange[]
33
     */
34
    private $slotRanges = [];
35

36
    /**
37
     * Checks if the given slot is valid.
38
     *
39
     * @param int $slot Slot index.
40
     *
41
     * @return bool
42
     */
43
    public static function isValid($slot)
55✔
44
    {
45
        return $slot >= 0 && $slot <= SlotRange::MAX_SLOTS;
55✔
46
    }
47

48
    /**
49
     * Checks if the given slot range is valid.
50
     *
51
     * @param int $first Initial slot of the range.
52
     * @param int $last  Last slot of the range.
53
     *
54
     * @return bool
55
     */
56
    public static function isValidRange($first, $last)
44✔
57
    {
58
        return SlotRange::isValidRange($first, $last);
44✔
59
    }
60

61
    /**
62
     * Resets the slot map.
63
     */
64
    public function reset()
68✔
65
    {
66
        $this->slotRanges = [];
68✔
67
    }
68

69
    /**
70
     * Checks if the slot map is empty.
71
     *
72
     * @return bool
73
     */
74
    public function isEmpty()
52✔
75
    {
76
        return empty($this->slotRanges);
52✔
77
    }
78

79
    /**
80
     * Returns the current slot map as a dictionary of $slot => $node.
81
     *
82
     * The order of the slots in the dictionary is not guaranteed.
83
     *
84
     * @return array
85
     */
86
    public function toArray()
10✔
87
    {
88
        return array_reduce(
10✔
89
            $this->slotRanges,
10✔
90
            function ($carry, $slotRange) {
10✔
91
                return $carry + $slotRange->toArray();
9✔
92
            },
10✔
93
            []
10✔
94
        );
10✔
95
    }
96

97
    /**
98
     * Returns the list of unique nodes in the slot map.
99
     *
100
     * @return array
101
     */
102
    public function getNodes()
4✔
103
    {
104
        return array_unique(array_map(
4✔
105
            function ($slotRange) {
4✔
106
                return $slotRange->getConnection();
3✔
107
            },
4✔
108
            $this->slotRanges
4✔
109
        ));
4✔
110
    }
111

112
    /**
113
     * Returns the list of slot ranges.
114
     *
115
     * @return SlotRange[]
116
     */
117
    public function getSlotRanges()
×
118
    {
119
        return $this->slotRanges;
×
120
    }
121

122
    /**
123
     * Assigns the specified slot range to a node.
124
     *
125
     * @param int                            $first      Initial slot of the range.
126
     * @param int                            $last       Last slot of the range.
127
     * @param NodeConnectionInterface|string $connection ID or connection instance.
128
     *
129
     * @throws OutOfBoundsException
130
     */
131
    public function setSlots($first, $last, $connection)
40✔
132
    {
133
        if (!static::isValidRange($first, $last)) {
40✔
134
            throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`");
1✔
135
        }
136

137
        $targetSlotRange = new SlotRange($first, $last, (string) $connection);
39✔
138

139
        // Get gaps of slot ranges list.
140
        $gaps = $this->getGaps($this->slotRanges);
39✔
141

142
        $results = $this->slotRanges;
39✔
143

144
        foreach ($gaps as $gap) {
39✔
145
            if (!$gap->hasIntersectionWith($targetSlotRange)) {
39✔
146
                continue;
5✔
147
            }
148

149
            // Get intersection of the gap and target slot range.
150
            $results[] = new SlotRange(
39✔
151
                max($gap->getStart(), $targetSlotRange->getStart()),
39✔
152
                min($gap->getEnd(), $targetSlotRange->getEnd()),
39✔
153
                $targetSlotRange->getConnection()
39✔
154
            );
39✔
155
        }
156

157
        $this->sortSlotRanges($results);
39✔
158

159
        $results = $this->compactSlotRanges($results);
39✔
160

161
        $this->slotRanges = $results;
39✔
162
    }
163

164
    /**
165
     * Returns the specified slot range.
166
     *
167
     * @param int $first Initial slot of the range.
168
     * @param int $last  Last slot of the range.
169
     *
170
     * @return array<int, string>
171
     */
172
    public function getSlots($first, $last)
4✔
173
    {
174
        if (!static::isValidRange($first, $last)) {
4✔
175
            throw new OutOfBoundsException("Invalid slot range $first-$last");
1✔
176
        }
177

178
        $placeHolder = new NullSlotRange($first, $last);
3✔
179

180
        $intersections = [];
3✔
181
        foreach ($this->slotRanges as $slotRange) {
3✔
182
            if (!$placeHolder->hasIntersectionWith($slotRange)) {
2✔
183
                continue;
1✔
184
            }
185

186
            $intersections[] = new SlotRange(
1✔
187
                max($placeHolder->getStart(), $slotRange->getStart()),
1✔
188
                min($placeHolder->getEnd(), $slotRange->getEnd()),
1✔
189
                $slotRange->getConnection()
1✔
190
            );
1✔
191
        }
192

193
        return array_reduce(
3✔
194
            $intersections,
3✔
195
            function ($carry, $slotRange) {
3✔
196
                return $carry + $slotRange->toArray();
1✔
197
            },
3✔
198
            []
3✔
199
        );
3✔
200
    }
201

202
    /**
203
     * Checks if the specified slot is assigned.
204
     *
205
     * @param int $slot Slot index.
206
     *
207
     * @return bool
208
     */
209
    #[ReturnTypeWillChange]
5✔
210
    public function offsetExists($slot)
211
    {
212
        return $this->findRangeBySlot($slot) !== false;
5✔
213
    }
214

215
    /**
216
     * Returns the node assigned to the specified slot.
217
     *
218
     * @param int $slot Slot index.
219
     *
220
     * @return string|null
221
     */
222
    #[ReturnTypeWillChange]
52✔
223
    public function offsetGet($slot)
224
    {
225
        $found = $this->findRangeBySlot($slot);
52✔
226

227
        return $found ? $found->getConnection() : null;
52✔
228
    }
229

230
    /**
231
     * Assigns the specified slot to a node.
232
     *
233
     * @param int                            $slot       Slot index.
234
     * @param NodeConnectionInterface|string $connection ID or connection instance.
235
     *
236
     * @return void
237
     */
238
    #[ReturnTypeWillChange]
9✔
239
    public function offsetSet($slot, $connection)
240
    {
241
        if (!static::isValid($slot)) {
9✔
242
            throw new OutOfBoundsException("Invalid slot $slot for `$connection`");
1✔
243
        }
244

245
        $this->offsetUnset($slot);
8✔
246
        $this->setSlots($slot, $slot, $connection);
8✔
247
    }
248

249
    /**
250
     * Returns the node assigned to the specified slot.
251
     *
252
     * @param int $slot Slot index.
253
     *
254
     * @return void
255
     */
256
    #[ReturnTypeWillChange]
10✔
257
    public function offsetUnset($slot)
258
    {
259
        if (!static::isValid($slot)) {
10✔
260
            throw new OutOfBoundsException("Invalid slot $slot");
×
261
        }
262

263
        $results = [];
10✔
264
        foreach ($this->slotRanges as $slotRange) {
10✔
265
            if (!$slotRange->hasSlot($slot)) {
6✔
266
                $results[] = $slotRange;
5✔
267
            }
268

269
            if (static::isValidRange($slotRange->getStart(), $slot - 1)) {
6✔
270
                $results[] = new SlotRange($slotRange->getStart(), $slot - 1, $slotRange->getConnection());
5✔
271
            }
272

273
            if (static::isValidRange($slot + 1, $slotRange->getEnd())) {
6✔
274
                $results[] = new SlotRange($slot + 1, $slotRange->getEnd(), $slotRange->getConnection());
5✔
275
            }
276
        }
277

278
        $this->slotRanges = $results;
10✔
279
    }
280

281
    /**
282
     * Returns the current number of assigned slots.
283
     *
284
     * @return int
285
     */
286
    #[ReturnTypeWillChange]
7✔
287
    public function count()
288
    {
289
        return array_sum(array_map(
7✔
290
            function ($slotRange) {
7✔
291
                return $slotRange->count();
3✔
292
            },
7✔
293
            $this->slotRanges
7✔
294
        ));
7✔
295
    }
296

297
    /**
298
     * Returns an iterator over the slot map.
299
     *
300
     * @return Traversable<int, string>
301
     */
302
    #[ReturnTypeWillChange]
1✔
303
    public function getIterator()
304
    {
305
        return new ArrayIterator($this->toArray());
1✔
306
    }
307

308
    /**
309
     * Find the slot range which contains the specific slot index.
310
     *
311
     * @param int $slot Slot index.
312
     *
313
     * @return SlotRange|false The slot range object or false if not found.
314
     */
315
    protected function findRangeBySlot(int $slot)
57✔
316
    {
317
        foreach ($this->slotRanges as $slotRange) {
57✔
318
            if ($slotRange->hasSlot($slot)) {
13✔
319
                return $slotRange;
8✔
320
            }
321
        }
322

323
        return false;
52✔
324
    }
325

326
    /**
327
     * Get gaps between sorted slot ranges with NullSlotRange object.
328
     *
329
     * @param SlotRange[] $slotRanges
330
     *
331
     * @return SlotRange[]
332
     */
333
    protected function getGaps(array $slotRanges)
39✔
334
    {
335
        if (empty($slotRanges)) {
39✔
336
            return [
39✔
337
                new NullSlotRange(0, SlotRange::MAX_SLOTS),
39✔
338
            ];
39✔
339
        }
340
        $gaps = [];
23✔
341
        $count = count($slotRanges);
23✔
342
        $i = 0;
23✔
343
        foreach ($slotRanges as $key => $slotRange) {
23✔
344
            $start = $slotRange->getStart();
23✔
345
            $end = $slotRange->getEnd();
23✔
346
            if (static::isValidRange($i, $start - 1)) {
23✔
347
                $gaps[] = new NullSlotRange($i, $start - 1);
6✔
348
            }
349

350
            $i = $end + 1;
23✔
351

352
            if ($key === $count - 1) {
23✔
353
                if (static::isValidRange($i, SlotRange::MAX_SLOTS)) {
23✔
354
                    $gaps[] = new NullSlotRange($i, SlotRange::MAX_SLOTS);
23✔
355
                }
356
            }
357
        }
358

359
        return $gaps;
23✔
360
    }
361

362
    /**
363
     * Sort slot ranges by start index.
364
     *
365
     * @param SlotRange[] $slotRanges
366
     *
367
     * @return void
368
     */
369
    protected function sortSlotRanges(array &$slotRanges)
39✔
370
    {
371
        usort(
39✔
372
            $slotRanges,
39✔
373
            function (SlotRange $a, SlotRange $b) {
39✔
374
                if ($a->getStart() == $b->getStart()) {
22✔
375
                    return 0;
4✔
376
                }
377

378
                return $a->getStart() < $b->getStart() ? -1 : 1;
22✔
379
            }
39✔
380
        );
39✔
381
    }
382

383
    /**
384
     * Compact adjacent slot ranges with the same connection.
385
     *
386
     * @param SlotRange[] $slotRanges
387
     *
388
     * @return SlotRange[]
389
     */
390
    protected function compactSlotRanges(array $slotRanges)
39✔
391
    {
392
        if (empty($slotRanges)) {
39✔
393
            return [];
×
394
        }
395

396
        $compacted = [];
39✔
397
        $count = count($slotRanges);
39✔
398
        $i = 0;
39✔
399
        $carry = $slotRanges[0];
39✔
400
        while ($i < $count) {
39✔
401
            $next = $slotRanges[$i + 1] ?? null;
39✔
402
            if (
403
                !is_null($next)
39✔
404
                && ($carry->getEnd() + 1) === $next->getStart()
39✔
405
                && $carry->getConnection() === $next->getConnection()
39✔
406
            ) {
UNCOV
407
                $carry = new SlotRange($carry->getStart(), $next->getEnd(), $carry->getConnection());
×
408
            } else {
409
                $compacted[] = $carry;
39✔
410
                $carry = $next;
39✔
411
            }
412
            $i++;
39✔
413
        }
414

415
        return array_values($compacted);
39✔
416
    }
417
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc