Skip to content

Commit 5791d0f

Browse files
committed
feat: add redis cleanup commands
1 parent cbd7d72 commit 5791d0f

File tree

5 files changed

+473
-0
lines changed

5 files changed

+473
-0
lines changed

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ parameters:
66
ignoreErrors:
77
-
88
message: "#no value type specified in iterable type array#"
9+
excludePaths:
10+
- vendor (?)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Frosh\Tools\Command;
6+
7+
use Frosh\Tools\Components\CacheRegistry;
8+
use Symfony\Component\Console\Attribute\AsCommand;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\InputInterface;
11+
use Symfony\Component\Console\Input\InputOption;
12+
use Symfony\Component\Console\Output\OutputInterface;
13+
use Symfony\Component\Console\Style\SymfonyStyle;
14+
15+
#[AsCommand(
16+
name: 'frosh:redis-namespace:cleanup',
17+
description: 'Delete all Redis namespaces except the active one [Experimental]',
18+
)]
19+
class RedisNamespaceCleanupCommand extends Command
20+
{
21+
public function __construct(private readonly CacheRegistry $cacheRegistry)
22+
{
23+
parent::__construct();
24+
}
25+
26+
protected function configure(): void
27+
{
28+
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be deleted without actually deleting');
29+
}
30+
31+
protected function execute(InputInterface $input, OutputInterface $output): int
32+
{
33+
$io = new SymfonyStyle($input, $output);
34+
$cacheAdapter = $this->cacheRegistry->get('cache.object');
35+
$dryRun = $input->getOption('dry-run');
36+
37+
try {
38+
$redis = $cacheAdapter->getRedisOrFail();
39+
} catch (\RuntimeException $e) {
40+
$io->error($e->getMessage());
41+
42+
return Command::FAILURE;
43+
}
44+
45+
$activeNamespace = $cacheAdapter->getNamespace();
46+
$io->title('Redis Namespace Cleanup');
47+
$io->writeln(\sprintf('Active namespace: <info>%s</info>', $activeNamespace));
48+
49+
if ($dryRun) {
50+
$io->note('Running in dry-run mode - no keys will be deleted');
51+
}
52+
53+
// Group keys by namespace (first 10 characters)
54+
$namespaces = [];
55+
$totalKeys = 0;
56+
$keysToDelete = [];
57+
58+
// Use SCAN to iterate through all keys efficiently
59+
$iterator = null;
60+
do {
61+
$keys = $redis->scan($iterator, null, 1000);
62+
if ($keys === false) {
63+
break;
64+
}
65+
66+
foreach ($keys as $key) {
67+
++$totalKeys;
68+
$namespace = substr($key, 0, 10);
69+
70+
if (!isset($namespaces[$namespace])) {
71+
$namespaces[$namespace] = 0;
72+
}
73+
++$namespaces[$namespace];
74+
75+
// Track keys that are not in the active namespace
76+
if ($namespace !== $activeNamespace) {
77+
$keysToDelete[] = $key;
78+
}
79+
}
80+
} while ($iterator > 0);
81+
82+
// Display namespace summary
83+
$tableData = [];
84+
foreach ($namespaces as $namespace => $count) {
85+
$status = $namespace === $activeNamespace ? 'KEEP' : 'DELETE';
86+
$tableData[] = [$namespace, $count, $status];
87+
}
88+
89+
$io->section('Namespace Summary');
90+
$io->table(
91+
['Namespace', 'Key Count', 'Action'],
92+
$tableData
93+
);
94+
95+
$deleteCount = \count($keysToDelete);
96+
97+
if ($deleteCount === 0) {
98+
$io->success('No keys to delete - only the active namespace exists');
99+
100+
return Command::SUCCESS;
101+
}
102+
103+
$io->writeln(\sprintf('Keys to delete: <comment>%d</comment> out of <comment>%d</comment> total keys', $deleteCount, $totalKeys));
104+
105+
if (!$dryRun) {
106+
if (!$io->confirm('Do you want to proceed with deleting these keys?', false)) {
107+
$io->warning('Operation cancelled');
108+
109+
return Command::SUCCESS;
110+
}
111+
112+
$io->progressStart($deleteCount);
113+
114+
// Delete keys in batches for better performance
115+
$batchSize = 1000;
116+
for ($i = 0; $i < $deleteCount; $i += $batchSize) {
117+
$batch = \array_slice($keysToDelete, $i, $batchSize);
118+
$redis->del(...$batch);
119+
$io->progressAdvance(\count($batch));
120+
}
121+
122+
$io->progressFinish();
123+
$io->success(\sprintf('Successfully deleted %d keys from inactive namespaces', $deleteCount));
124+
} else {
125+
$io->warning(\sprintf('Dry run complete - would have deleted %d keys', $deleteCount));
126+
}
127+
128+
return Command::SUCCESS;
129+
}
130+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Frosh\Tools\Command;
6+
7+
use Frosh\Tools\Components\CacheRegistry;
8+
use Symfony\Component\Console\Attribute\AsCommand;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Input\InputInterface;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
use Symfony\Component\Console\Style\SymfonyStyle;
13+
14+
#[AsCommand(
15+
name: 'frosh:redis-namespace:list',
16+
description: 'List all Redis namespaces [Experimental]',
17+
)]
18+
class RedisNamespaceListCommand extends Command
19+
{
20+
public function __construct(private readonly CacheRegistry $cacheRegistry)
21+
{
22+
parent::__construct();
23+
}
24+
25+
protected function execute(InputInterface $input, OutputInterface $output): int
26+
{
27+
$io = new SymfonyStyle($input, $output);
28+
$cacheAdapter = $this->cacheRegistry->get('cache.object');
29+
30+
try {
31+
$redis = $cacheAdapter->getRedisOrFail();
32+
} catch (\RuntimeException $e) {
33+
$io->error($e->getMessage());
34+
35+
return Command::FAILURE;
36+
}
37+
38+
$namespace = $cacheAdapter->getNamespace();
39+
$io->title('Redis Key Groupping by Namespace');
40+
41+
// Group keys by first 10 characters
42+
$keyGroups = [];
43+
$totalKeys = 0;
44+
45+
// Use SCAN to iterate through all keys efficiently
46+
$iterator = null;
47+
do {
48+
$keys = $redis->scan($iterator, null, 1000);
49+
if ($keys === false) {
50+
break;
51+
}
52+
53+
foreach ($keys as $key) {
54+
++$totalKeys;
55+
$prefix = substr($key, 0, 10);
56+
if (!isset($keyGroups[$prefix])) {
57+
$keyGroups[$prefix] = 0;
58+
}
59+
++$keyGroups[$prefix];
60+
}
61+
} while ($iterator > 0);
62+
63+
// Sort by count descending
64+
arsort($keyGroups);
65+
66+
// Display results in a table
67+
$tableData = [];
68+
foreach ($keyGroups as $prefix => $count) {
69+
$tableData[] = [$prefix, $count, \sprintf('%.1f%%', ($count / $totalKeys) * 100), $namespace === $prefix ? 'Yes' : 'No'];
70+
}
71+
72+
$io->table(
73+
['Prefix', 'Count', 'Percentage', 'Active'],
74+
$tableData
75+
);
76+
77+
$io->success(\sprintf('Total keys analyzed: %d', $totalKeys));
78+
79+
return Command::SUCCESS;
80+
}
81+
}

0 commit comments

Comments
 (0)