Skip to content

Commit b23c255

Browse files
committed
Redis fixes & Predis cluster support
1 parent 01283c4 commit b23c255

File tree

6 files changed

+351
-16
lines changed

6 files changed

+351
-16
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
<?php
2+
/**
3+
* This file is part of the phpCacheAdmin.
4+
* Copyright (c) Róbert Kelčák (https://kelcak.com/)
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace RobiNN\Pca\Dashboards\Redis\Compatibility\Cluster;
10+
11+
use Exception;
12+
use InvalidArgumentException;
13+
use Predis\Client as PredisClient;
14+
use Predis\Collection\Iterator\Keyspace;
15+
use RobiNN\Pca\Dashboards\DashboardException;
16+
use RobiNN\Pca\Dashboards\Redis\Compatibility\RedisCompatibilityInterface;
17+
use RobiNN\Pca\Dashboards\Redis\Compatibility\RedisJson;
18+
use RobiNN\Pca\Dashboards\Redis\Compatibility\RedisModules;
19+
20+
class PredisCluster extends PredisClient implements RedisCompatibilityInterface {
21+
use RedisJson;
22+
use RedisModules;
23+
24+
/**
25+
* @var array<int, PredisClient>
26+
*/
27+
private array $nodes;
28+
29+
/**
30+
* @var array<int|string, string>
31+
*/
32+
public array $data_types = [
33+
'none' => 'other',
34+
'string' => 'string',
35+
'set' => 'set',
36+
'list' => 'list',
37+
'zset' => 'zset',
38+
'hash' => 'hash',
39+
'stream' => 'stream',
40+
'ReJSON-RL' => 'rejson',
41+
];
42+
43+
/**
44+
* @param array<string, mixed> $server
45+
*
46+
* @throws DashboardException
47+
*/
48+
public function __construct(array $server) {
49+
$cluster_options = ['cluster' => 'redis'];
50+
51+
if (isset($server['password'])) {
52+
$cluster_options['parameters']['password'] = $server['password'];
53+
}
54+
55+
try {
56+
parent::__construct($server['nodes'], $cluster_options);
57+
$this->connect();
58+
59+
foreach ($server['nodes'] as $node) {
60+
$this->nodes[] = new PredisClient('tcp://'.$node, $cluster_options);
61+
}
62+
} catch (Exception $e) {
63+
throw new DashboardException($e->getMessage().' ['.implode(', ', $server['nodes']).']');
64+
}
65+
}
66+
67+
public function getType(string|int $type): string {
68+
return $this->data_types[$type] ?? 'unknown';
69+
}
70+
71+
public function getKeyType(string $key): string {
72+
$type = $this->type($key);
73+
74+
if ($type === 'none') {
75+
$type = $this->executeRaw(['TYPE', $key]);
76+
}
77+
78+
return $this->getType($type);
79+
}
80+
81+
/**
82+
* @param list<string>|null $combine
83+
*
84+
* @return array<string, array<string, mixed>>
85+
*/
86+
public function getInfo(?string $option = null, ?array $combine = null): array {
87+
static $info = [];
88+
89+
$options = ['Server', 'Clients', 'Memory', 'Persistence', 'Stats', 'Replication', 'CPU', 'Cluster', 'Keyspace'];
90+
91+
foreach ($options as $option_name) {
92+
/**
93+
* @var array<string, array<int, mixed>|array<string, array<int, mixed>>> $combined
94+
*/
95+
$combined = [];
96+
97+
foreach ($this->nodes as $node) {
98+
/**
99+
* @var array<string, mixed> $node_info
100+
*/
101+
$node_info = $node->info()[$option_name];
102+
103+
foreach ($node_info as $key => $value) {
104+
if (is_array($value)) {
105+
foreach ($value as $sub_key => $sub_val) {
106+
$combined[$key][$sub_key][] = $sub_val;
107+
}
108+
} else {
109+
$combined[$key][] = $value;
110+
}
111+
}
112+
}
113+
114+
foreach ($combined as $key => $values) {
115+
if (is_array(reset($values))) {
116+
foreach ($values as $sub_key => $sub_values) {
117+
$combined[$key][$sub_key] = $this->combineValues($sub_key, $sub_values, $combine);
118+
}
119+
} else {
120+
$combined[$key] = $this->combineValues($key, $values, $combine);
121+
}
122+
}
123+
124+
$info[strtolower($option_name)] = $combined;
125+
}
126+
127+
return $option !== null ? ($info[$option] ?? []) : $info;
128+
}
129+
130+
/**
131+
* @param list<mixed> $values
132+
* @param list<string>|null $combine
133+
*/
134+
private function combineValues(string $key, array $values, ?array $combine): mixed {
135+
$unique = array_unique($values);
136+
137+
if (count($unique) === 1) {
138+
return $unique[0];
139+
}
140+
141+
$numeric = array_filter($values, 'is_numeric');
142+
143+
if ($combine && in_array($key, $combine, true) && count($numeric) === count($values)) {
144+
return array_sum($values);
145+
}
146+
147+
if ($key === 'mem_fragmentation_ratio' && count($numeric) === count($values)) {
148+
return round(array_sum($values) / count($values), 2);
149+
}
150+
151+
if ($key === 'used_memory_peak' && count($numeric) === count($values)) {
152+
return max($values);
153+
}
154+
155+
return $values;
156+
}
157+
158+
/**
159+
* @return array<int, string>
160+
*/
161+
public function keys(string $pattern): array {
162+
$keys = [];
163+
164+
foreach ($this->nodes as $node) {
165+
foreach ($node->keys($pattern) as $key) {
166+
$keys[] = $key;
167+
}
168+
}
169+
170+
return $keys;
171+
}
172+
173+
/**
174+
* @return array<int, string>
175+
*/
176+
public function scanKeys(string $pattern, int $count): array {
177+
$keys = [];
178+
179+
foreach ($this->nodes as $node) {
180+
foreach (new Keyspace($node, $pattern) as $item) {
181+
$keys[] = $item;
182+
183+
if (count($keys) === $count) {
184+
break;
185+
}
186+
}
187+
}
188+
189+
return $keys;
190+
}
191+
192+
public function listRem(string $key, string $value, int $count): int {
193+
return $this->lrem($key, $count, $value);
194+
}
195+
196+
/**
197+
* @param array<string, string> $messages
198+
*/
199+
public function streamAdd(string $key, string $id, array $messages): string {
200+
return $this->xadd($key, $messages, $id);
201+
}
202+
203+
/**
204+
* @param array<int, string> $keys
205+
*
206+
* @return array<string, mixed>
207+
*/
208+
public function pipelineKeys(array $keys): array {
209+
$lua_script = file_get_contents(__DIR__.'/../get_key_info.lua');
210+
211+
if ($lua_script === false) {
212+
return [];
213+
}
214+
215+
$script_sha = null;
216+
217+
foreach ($this->nodes as $node) {
218+
$script_sha = $node->script('load', $lua_script);
219+
}
220+
221+
if (empty($script_sha)) {
222+
return [];
223+
}
224+
225+
$data = [];
226+
227+
foreach ($keys as $key) {
228+
$results = $this->evalsha($script_sha, 1, $key);
229+
230+
if (is_array($results) && count($results) >= 3) {
231+
$data[$key] = [
232+
'ttl' => $results[0], 'type' => $results[1], 'size' => $results[2] ?? 0,
233+
'count' => isset($results[3]) && is_numeric($results[3]) ? (int) $results[3] : null,
234+
];
235+
}
236+
}
237+
238+
return $data;
239+
}
240+
241+
public function size(string $key): int {
242+
foreach ($this->nodes as $node) {
243+
$size = $node->executeRaw(['MEMORY', 'USAGE', $key]);
244+
if ($size !== false && $size !== null) {
245+
return (int) $size;
246+
}
247+
}
248+
249+
return 0;
250+
}
251+
252+
public function flushDatabase(): bool {
253+
foreach ($this->nodes as $node) {
254+
$node->flushdb();
255+
}
256+
257+
return true;
258+
}
259+
260+
public function databaseSize(): int {
261+
$total = 0;
262+
263+
foreach ($this->nodes as $node) {
264+
$total += $node->dbsize();
265+
}
266+
267+
return $total;
268+
}
269+
270+
/**
271+
* @throws InvalidArgumentException
272+
*/
273+
public function execConfig(string $operation, mixed ...$args): mixed {
274+
switch (strtoupper($operation)) {
275+
case 'GET':
276+
if ($args === []) {
277+
throw new InvalidArgumentException('CONFIG GET requires a parameter name.');
278+
}
279+
280+
$result = $this->nodes[0]->executeRaw(['CONFIG', 'GET', $args[0]]);
281+
282+
return isset($result[0], $result[1]) ? [$result[0] => $result[1]] : [];
283+
case 'SET':
284+
if (count($args) < 2) {
285+
throw new InvalidArgumentException('CONFIG SET requires a parameter name and a value.');
286+
}
287+
288+
foreach ($this->nodes as $node) {
289+
$node->executeRaw(['CONFIG', 'SET', $args[0], $args[1]]);
290+
}
291+
292+
return true;
293+
case 'REWRITE':
294+
case 'RESETSTAT':
295+
foreach ($this->nodes as $node) {
296+
$node->executeRaw(['CONFIG', strtoupper($operation)]);
297+
}
298+
299+
return true;
300+
default:
301+
throw new InvalidArgumentException('Unsupported CONFIG operation: '.$operation);
302+
}
303+
}
304+
305+
/**
306+
* @return null|array<int, mixed>
307+
*/
308+
public function getSlowlog(int $count): ?array {
309+
$all_logs = [];
310+
311+
foreach ($this->nodes as $node) {
312+
$logs = $node->executeRaw(['SLOWLOG', 'GET', (string) $count]);
313+
314+
if (is_array($logs) && !empty($logs)) {
315+
array_push($all_logs, ...$logs);
316+
}
317+
}
318+
319+
usort($all_logs, static fn ($a, $b): int => $b[1] <=> $a[1]);
320+
321+
return $all_logs;
322+
}
323+
324+
public function resetSlowlog(): bool {
325+
foreach ($this->nodes as $node) {
326+
$node->slowlog('RESET');
327+
}
328+
329+
return true;
330+
}
331+
}

src/Dashboards/Redis/Compatibility/Cluster/RedisCluster.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,15 @@ public function getInfo(?string $option = null, ?array $combine = null): array {
9191
$options = ['SERVER', 'CLIENTS', 'MEMORY', 'PERSISTENCE', 'STATS', 'REPLICATION', 'CPU', 'CLUSTER', 'KEYSPACE'];
9292

9393
foreach ($options as $option_name) {
94-
/** @var array<string, array<int, mixed>|array<string, array<int, mixed>>> $combined */
94+
/**
95+
* @var array<string, array<int, mixed>|array<string, array<int, mixed>>> $combined
96+
*/
9597
$combined = [];
9698

9799
foreach ($this->nodes as $node) {
98-
/** @var array<string, mixed> $node_info */
100+
/**
101+
* @var array<string, mixed> $node_info
102+
*/
99103
$node_info = $this->info($node, $option_name);
100104

101105
foreach ($node_info as $key => $value) {
@@ -155,6 +159,7 @@ private function combineValues(string $key, array $values, ?array $combine): mix
155159

156160
/**
157161
* @return array<int, string>
162+
*
158163
* @throws RedisClusterException
159164
*/
160165
public function scanKeys(string $pattern, int $count): array {

src/Dashboards/Redis/RedisDashboard.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class RedisDashboard implements DashboardInterface {
2727

2828
private int $current_server;
2929

30-
public Compatibility\Redis|Compatibility\Predis|Compatibility\Cluster\RedisCluster $redis;
30+
public Compatibility\Redis|Compatibility\Predis|Compatibility\Cluster\RedisCluster|Compatibility\Cluster\PredisCluster $redis;
3131

3232
public string $client = '';
3333

@@ -76,7 +76,7 @@ public function dashboardInfo(): array {
7676
*
7777
* @throws DashboardException
7878
*/
79-
public function connect(array $server): Compatibility\Redis|Compatibility\Predis|Compatibility\Cluster\RedisCluster {
79+
public function connect(array $server): Compatibility\Redis|Compatibility\Predis|Compatibility\Cluster\RedisCluster|Compatibility\Cluster\PredisCluster {
8080
$server['database'] = Http::get('db', $server['database'] ?? 0);
8181

8282
if (!empty($server['authfile'])) {
@@ -88,11 +88,7 @@ public function connect(array $server): Compatibility\Redis|Compatibility\Predis
8888
if ($this->client === 'redis') {
8989
$redis = $this->is_cluster ? new Compatibility\Cluster\RedisCluster($server) : new Compatibility\Redis($server);
9090
} elseif ($this->client === 'predis') {
91-
if ($this->is_cluster) {
92-
throw new DashboardException('There is currently no support for clusters with Predis.');
93-
}
94-
95-
$redis = new Compatibility\Predis($server);
91+
$redis = $this->is_cluster ? new Compatibility\Cluster\PredisCluster($server) : new Compatibility\Predis($server);
9692
} else {
9793
throw new DashboardException('Redis extension or Predis is not installed.');
9894
}

0 commit comments

Comments
 (0)