Passed
Push — main ( f5d7d4...b3268f )
by
unknown
05:10 queued 02:13
created

CleanOrphanedKeysCommand::handleOnStandalone()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 86
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 70
c 0
b 0
f 0
nc 4
nop 0
dl 0
loc 86
rs 8.6545

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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');
97
98
        // ATTENZIONE! il comando SMEMBERS non supporta *, per cui va usata la combinazipone di SCAN e SMEMBERS
99
        // Non usare MAI il comando KEYS se non si vuole distruggere il server!
100
101
        // Script Lua per pulire le chiavi orfane
102
        $script = <<<'LUA'
103
        local success, result = pcall(function()
104
            local database_prefix = string.gsub(KEYS[5], "temp", "")
105
            local shard_prefix = KEYS[1]
106
            local num_shards = string.gsub(KEYS[2], database_prefix, "")
107
            local lock_key = KEYS[3]
108
            local lock_ttl = string.gsub(KEYS[4], database_prefix, "")
109
110
            -- Tenta di acquisire un lock globale
111
            if redis.call("SET", lock_key, "1", "NX", "EX", lock_ttl) then
112
                -- Scansiona tutti gli shard
113
                -- redis.log(redis.LOG_NOTICE, "Scansiona tutti gli shard: " .. num_shards)
114
                for i = 0, num_shards - 1 do
115
                    local shard_key = shard_prefix .. ":" .. i
116
                    -- redis.log(redis.LOG_NOTICE, "shard_key: " .. shard_key)
117
                    -- Ottieni tutte le chiavi dallo shard
118
                    local cursor = "0"
119
                    local keys = {}
120
                    repeat
121
                        local result = redis.call("SCAN", cursor, "MATCH", shard_key)
122
                        cursor = result[1]
123
                        for _, key in ipairs(result[2]) do
124
                            table.insert(keys, key)
125
                        end
126
                    until cursor == "0"
127
                    -- Cerco tutte le chiavi associate a questa chiave
128
                    for _, key in ipairs(keys) do
129
                        -- redis.log(redis.LOG_NOTICE, "CHIAVE: " .. key)
130
                        local members = redis.call("SMEMBERS", key)
131
                        for _, member in ipairs(members) do
132
                            redis.log(redis.LOG_NOTICE, "member: " .. database_prefix .. member)
133
                            if redis.call("EXISTS", database_prefix .. member) == 0 then
134
                                -- La chiave è orfana, rimuovila dallo shard
135
                                redis.call("SREM", key, member)
136
                                -- redis.log(redis.LOG_NOTICE, "Rimossa chiave orfana key: " .. key .. " member: " .. member)
137
                                -- Devo rimuovere anche la chiave con i tags
138
                                local tagsKey = database_prefix .. member .. ":tags"
139
                                -- redis.log(redis.LOG_NOTICE, "Rimuovo la chiave con tagsKey: " .. tagsKey)
140
                                redis.call("DEL", tagsKey)
141
                            end
142
                        end
143
                    end
144
                end
145
                -- Rilascia il lock
146
                redis.call("DEL", lock_key)
147
            end
148
        end)
149
        if not success then
150
            redis.log(redis.LOG_WARNING, "Errore durante l'esecuzione del batch: " .. result)
151
            return result;
152
        end
153
        return "OK"
154
        LUA;
155
156
        try {
157
            // Parametri dello script
158
            $shardPrefix = $prefix . 'tag:*:shard';
159
            $tagPrefix = $prefix;
0 ignored issues
show
Unused Code introduced by
The assignment to $tagPrefix is dead and can be removed.
Loading history...
160
            $lockKey = $prefix . 'clean_orphans:lock';
161
            $lockTTL = 300; // Timeout lock di 300 secondi
162
163
            // Esegui lo script Lua
164
            $return = $this->redis->getRedisConnection($this->option('connection_name'))->eval(
165
                $script,
166
                5, // Numero di parametri passati a Lua come KEYS
167
                $shardPrefix,
168
                $this->numShards,
169
                $lockKey,
170
                $lockTTL,
171
                'temp',
172
            );
173
174
            if ($return !== 'OK') {
175
                Log::error('Errore durante l\'esecuzione dello script Lua: ' . $return);
176
            }
177
        } catch (\Exception $e) {
178
            Log::error('Errore durante l\'esecuzione dello script Lua: ' . $e->getMessage());
179
        }
180
    }
181
}
182