1 | <?php |
||
2 | |||
3 | namespace Padosoft\SuperCache\Console; |
||
4 | |||
5 | use Illuminate\Console\Command; |
||
6 | use Illuminate\Support\Facades\Log; |
||
7 | use Padosoft\SuperCache\RedisConnector; |
||
8 | use Padosoft\SuperCache\Service\GetClusterNodesService; |
||
9 | use Padosoft\SuperCache\SuperCacheManager; |
||
10 | |||
11 | class CleanOrphanedKeysCommand extends Command |
||
12 | { |
||
13 | protected $signature = 'supercache:clean-orphans {--connection_name= : (opzionale) nome della connessione redis}'; |
||
14 | protected $description = 'Clean orphaned cache keys'; |
||
15 | protected RedisConnector $redis; |
||
16 | protected int $numShards; |
||
17 | protected SuperCacheManager $superCache; |
||
18 | protected GetClusterNodesService $getClusterNodesService; |
||
19 | |||
20 | public function __construct(RedisConnector $redis, SuperCacheManager $superCache, GetClusterNodesService $getClusterNodesService) |
||
21 | { |
||
22 | parent::__construct(); |
||
23 | $this->redis = $redis; |
||
24 | $this->numShards = (int) config('supercache.num_shards'); // Numero di shard configurato |
||
25 | $this->superCache = $superCache; |
||
26 | $this->getClusterNodesService = $getClusterNodesService; |
||
27 | } |
||
28 | |||
29 | public function handle(): void |
||
30 | { |
||
31 | if (config('database.redis.clusters.' . ($this->option('connection_name') ?? 'default')) !== null) { |
||
32 | $this->handleOnCluster(); |
||
33 | } else { |
||
34 | $this->handleOnStandalone(); |
||
35 | } |
||
36 | } |
||
37 | |||
38 | public function handleOnCluster(): void |
||
39 | { |
||
40 | // Tenta di acquisire un lock globale |
||
41 | $lockAcquired = $this->superCache->lock('clean_orphans:lock', $this->option('connection_name'), 300); |
||
42 | if (!$lockAcquired) { |
||
43 | return; |
||
44 | } |
||
45 | // Purtroppo lo scan non funziona sul cluster per cui va eseguito su ogni nodo (master) |
||
46 | $array_nodi = $this->getClusterNodesService->getClusterNodes($this->option('connection_name')); |
||
47 | |||
48 | foreach ($array_nodi as $node) { |
||
49 | [$host, $port] = explode(':', $node); |
||
50 | |||
51 | // Per ogni shard vado a cercare i bussolotti (SET) dei tags che contengono le chiavi |
||
52 | for ($i = 0; $i < $this->numShards; $i++) { |
||
53 | // Inserisco nel pattern supercache: cosรฌ sonop sicura di trovare solo i SET che riguardano il contesto della supercache |
||
54 | $shard_key = '*' . config('supercache.prefix') . 'tag:*:shard:' . $i; |
||
55 | // Creo una connessione persistente perchรจ considerando la durata dell'elaborazione si evita che dopo 30 secondi muoia tutto! |
||
56 | $connection = $this->redis->getNativeRedisConnection($this->option('connection_name'), $host, $port); |
||
57 | |||
58 | $cursor = null; |
||
59 | do { |
||
60 | $response = $connection['connection']->scan($cursor, $shard_key); |
||
61 | |||
62 | if ($response === false) { |
||
63 | //Nessuna chiave trovata ... |
||
64 | break; |
||
65 | } |
||
66 | |||
67 | foreach ($response as $key) { |
||
68 | // Ho trovato un bussolotto che matcha, vado a recuperare i membri del SET |
||
69 | $members = $connection['connection']->sMembers($key); |
||
70 | foreach ($members as $member) { |
||
71 | // Controllo che la chiave sia morta, ma ATTENZIONE non posso usare la connessione che ho giร perchรจ sono su un singolo nodo, mentre nel bussolotto potrebbero esserci chiavi in sharding diversi |
||
72 | if ($this->redis->getRedisConnection($this->option('connection_name'))->exists($member)) { |
||
73 | // La chiave รจ viva! vado avanti |
||
74 | continue; |
||
75 | } |
||
76 | // Altrimenti rimuovo i cadaveri dal bussolotto e dai tags |
||
77 | // Qui posso usare la connessione che giร ho in quanto sto modificando il bussolotto che sicuramente รจ nello shard corretto del nodo |
||
78 | $connection['connection']->srem($key, $member); |
||
79 | // Rimuovo anche i tag, che perรฒ potrebbero essere in un altro nodo quindi uso una nuova connessione |
||
80 | $this->redis->getRedisConnection($this->option('connection_name'))->del($member . ':tags'); |
||
81 | } |
||
82 | } |
||
83 | } while ($cursor !== 0); // Continua fino a completamento |
||
84 | |||
85 | // Chiudo la connessione |
||
86 | $connection['connection']->close(); |
||
87 | } |
||
88 | } |
||
89 | // Rilascio il lock globale |
||
90 | $this->superCache->unLock('clean_orphans:lock', $this->option('connection_name')); |
||
91 | } |
||
92 | |||
93 | public function handleOnStandalone(): void |
||
94 | { |
||
95 | // Carica il prefisso di default per le chiavi |
||
96 | $prefix = config('supercache.prefix'); |
||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||
97 | // Per ogni shard vado a cercare i bussolotti (SET) dei tags che contengono le chiavi |
||
98 | for ($i = 0; $i < $this->numShards; $i++) { |
||
99 | // Inserisco nel pattern supercache: cosรฌ sonop sicura di trovare solo i SET che riguardano il contesto della supercache |
||
100 | $shard_key = '*' . config('supercache.prefix') . 'tag:*:shard:' . $i; |
||
101 | // Creo una connessione persistente perchรจ considerando la durata dell'elaborazione si evita che dopo 30 secondi muoia tutto! |
||
102 | $connection = $this->redis->getNativeRedisConnection($this->option('connection_name')); |
||
103 | |||
104 | $cursor = null; |
||
105 | do { |
||
106 | $response = $connection['connection']->scan($cursor, $shard_key); |
||
107 | |||
108 | if ($response === false) { |
||
109 | //Nessuna chiave trovata ... |
||
110 | break; |
||
111 | } |
||
112 | |||
113 | foreach ($response as $key) { |
||
114 | // Ho trovato un bussolotto che matcha, vado a recuperare i membri del SET |
||
115 | $members = $connection['connection']->sMembers($key); |
||
116 | foreach ($members as $member) { |
||
117 | // Controllo che la chiave sia morta, ma ATTENZIONE non posso usare la connessione che ho giร perchรจ sono su un singolo nodo, mentre nel bussolotto potrebbero esserci chiavi in sharding diversi |
||
118 | if ($this->redis->getRedisConnection($this->option('connection_name'))->exists($member)) { |
||
119 | // La chiave รจ viva! vado avanti |
||
120 | continue; |
||
121 | } |
||
122 | // Altrimenti rimuovo i cadaveri dal bussolotto e dai tags |
||
123 | // Qui posso usare la connessione che giร ho in quanto sto modificando il bussolotto che sicuramente รจ nello shard corretto del nodo |
||
124 | $connection['connection']->srem($key, $member); |
||
125 | // Rimuovo anche i tag, che perรฒ potrebbero essere in un altro nodo quindi uso una nuova connessione |
||
126 | $this->redis->getRedisConnection($this->option('connection_name'))->del($member . ':tags'); |
||
127 | } |
||
128 | } |
||
129 | } while ($cursor !== 0); // Continua fino a completamento |
||
130 | |||
131 | // Chiudo la connessione |
||
132 | $connection['connection']->close(); |
||
133 | } |
||
134 | |||
135 | /* |
||
136 | // ATTENZIONE! il comando SMEMBERS non supporta *, per cui va usata la combinazipone di SCAN e SMEMBERS |
||
137 | // Non usare MAI il comando KEYS se non si vuole distruggere il server! |
||
138 | |||
139 | // Script Lua per pulire le chiavi orfane |
||
140 | $script = <<<'LUA' |
||
141 | local success, result = pcall(function() |
||
142 | local database_prefix = string.gsub(KEYS[5], "temp", "") |
||
143 | local shard_prefix = KEYS[1] |
||
144 | local num_shards = string.gsub(KEYS[2], database_prefix, "") |
||
145 | local lock_key = KEYS[3] |
||
146 | local lock_ttl = string.gsub(KEYS[4], database_prefix, "") |
||
147 | |||
148 | -- Tenta di acquisire un lock globale |
||
149 | if redis.call("SET", lock_key, "1", "NX", "EX", lock_ttl) then |
||
150 | -- Scansiona tutti gli shard |
||
151 | -- redis.log(redis.LOG_NOTICE, "Scansiona tutti gli shard: " .. num_shards) |
||
152 | for i = 0, num_shards - 1 do |
||
153 | local shard_key = shard_prefix .. ":" .. i |
||
154 | -- redis.log(redis.LOG_NOTICE, "shard_key: " .. shard_key) |
||
155 | -- Ottieni tutte le chiavi dallo shard |
||
156 | local cursor = "0" |
||
157 | local keys = {} |
||
158 | repeat |
||
159 | local result = redis.call("SCAN", cursor, "MATCH", shard_key) |
||
160 | cursor = result[1] |
||
161 | for _, key in ipairs(result[2]) do |
||
162 | table.insert(keys, key) |
||
163 | end |
||
164 | until cursor == "0" |
||
165 | -- Cerco tutte le chiavi associate a questa chiave |
||
166 | for _, key in ipairs(keys) do |
||
167 | -- redis.log(redis.LOG_NOTICE, "CHIAVE: " .. key) |
||
168 | local members = redis.call("SMEMBERS", key) |
||
169 | for _, member in ipairs(members) do |
||
170 | redis.log(redis.LOG_NOTICE, "member: " .. database_prefix .. member) |
||
171 | if redis.call("EXISTS", database_prefix .. member) == 0 then |
||
172 | -- La chiave รจ orfana, rimuovila dallo shard |
||
173 | redis.call("SREM", key, member) |
||
174 | -- redis.log(redis.LOG_NOTICE, "Rimossa chiave orfana key: " .. key .. " member: " .. member) |
||
175 | -- Devo rimuovere anche la chiave con i tags |
||
176 | local tagsKey = database_prefix .. member .. ":tags" |
||
177 | -- redis.log(redis.LOG_NOTICE, "Rimuovo la chiave con tagsKey: " .. tagsKey) |
||
178 | redis.call("DEL", tagsKey) |
||
179 | end |
||
180 | end |
||
181 | end |
||
182 | end |
||
183 | -- Rilascia il lock |
||
184 | redis.call("DEL", lock_key) |
||
185 | end |
||
186 | end) |
||
187 | if not success then |
||
188 | redis.log(redis.LOG_WARNING, "Errore durante l'esecuzione del batch: " .. result) |
||
189 | return result; |
||
190 | end |
||
191 | return "OK" |
||
192 | LUA; |
||
193 | |||
194 | try { |
||
195 | // Parametri dello script |
||
196 | $shardPrefix = $prefix . 'tag:*:shard'; |
||
197 | $tagPrefix = $prefix; |
||
198 | $lockKey = $prefix . 'clean_orphans:lock'; |
||
199 | $lockTTL = 300; // Timeout lock di 300 secondi |
||
200 | |||
201 | // Esegui lo script Lua |
||
202 | $return = $this->redis->getRedisConnection($this->option('connection_name'))->eval( |
||
203 | $script, |
||
204 | 5, // Numero di parametri passati a Lua come KEYS |
||
205 | $shardPrefix, |
||
206 | $this->numShards, |
||
207 | $lockKey, |
||
208 | $lockTTL, |
||
209 | 'temp', |
||
210 | ); |
||
211 | |||
212 | if ($return !== 'OK') { |
||
213 | Log::error('Errore durante l\'esecuzione dello script Lua: ' . $return); |
||
214 | } |
||
215 | } catch (\Exception $e) { |
||
216 | Log::error('Errore durante l\'esecuzione dello script Lua: ' . $e->getMessage()); |
||
217 | } |
||
218 | */ |
||
219 | } |
||
220 | } |
||
221 |