Issues (11)

src/Console/CleanOrphanedKeysCommand.php (1 issue)

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
The assignment to $prefix is dead and can be removed.
Loading history...
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