Passed
Push — main ( 705740...019fa2 )
by
unknown
11:26
created

ListenerCommand::onExpireEvent()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 19
c 2
b 0
f 0
nc 7
nop 1
dl 0
loc 33
rs 8.8333
1
<?php
2
3
namespace Padosoft\SuperCache\Console;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Str;
7
use Padosoft\SuperCache\RedisConnector;
8
use Illuminate\Support\Facades\Log;
9
use Padosoft\SuperCache\SuperCacheManager;
10
11
class ListenerCommand extends Command
12
{
13
    protected $signature = 'supercache:listener
14
                                {--connection_name= : (opzionale) nome della connessione redis }
15
                                {--namespace_id= : (opzionale) id del namespace da usare per suddividere i processi e da impostare se supercache.use_namespace = true }
16
                                {--checkEvent= : (opzionale) se 1 si esegue controllo su attivazione evento expired di Redis }
17
                                {--host= : (opzionale) host del nodo del cluster (da impostare solo in caso di Redis in cluster) }
18
                                {--port= : (opzionale) porta del nodo del cluster (da impostare solo in caso di Redis in cluster) }
19
                                ';
20
    protected $description = 'Listener per eventi di scadenza chiavi Redis';
21
    protected RedisConnector $redis;
22
    protected array $batch = []; // Accumula chiavi scadute
23
    protected int $batchSizeThreshold; // Numero di chiavi per batch
24
    protected int $timeThreshold; // Tempo massimo prima di processare il batch
25
    protected bool $useNamespace;
26
    protected SuperCacheManager $superCache;
27
28
    public function __construct(RedisConnector $redis, SuperCacheManager $superCache)
29
    {
30
        parent::__construct();
31
        $this->redis = $redis;
32
        // Parametri di batch processing da config
33
        $this->batchSizeThreshold = config('supercache.batch_size');
34
        $this->timeThreshold = config('supercache.time_threshold'); // secondi
35
        $this->useNamespace = (bool) config('supercache.use_namespace', false);
36
        $this->superCache = $superCache;
37
    }
38
39
    /**
40
     * Verifica se Redis è configurato per generare notifiche di scadenza.
41
     */
42
    protected function checkRedisNotifications(): bool
43
    {
44
        $checkEvent = $this->option('checkEvent');
45
        if ($checkEvent === null) {
0 ignored issues
show
introduced by
The condition $checkEvent === null is always false.
Loading history...
46
            return true;
47
        }
48
        if ((int) $checkEvent === 0) {
49
            return true;
50
        }
51
        $config = $this->redis->getRedisConnection($this->option('connection_name'))->config('GET', 'notify-keyspace-events');
52
53
        return str_contains($config['notify-keyspace-events'], 'Ex') || str_contains($config['notify-keyspace-events'], 'xE');
54
    }
55
56
    protected function onExpireEvent(string $key): void
57
    {
58
        $debug = 'EXPIRED $key: ' . $key . PHP_EOL .
0 ignored issues
show
Unused Code introduced by
The assignment to $debug is dead and can be removed.
Loading history...
59
            'Host: ' . $this->option('host') . PHP_EOL .
60
            'Port: ' . $this->option('port') . PHP_EOL .
61
            'Connection Name: ' . $this->option('connection_name') . PHP_EOL .
62
            'Namespace ID: ' . $this->option('namespace_id') . PHP_EOL;
63
        // Filtro le chiavi di competenza di questo listener, ovvero quelle che iniziano con gescat_laravel_database_supercache: e che eventualemnte terminano con ns<namespace_id> se c'è il namespace attivo
64
        // Attenzione la chiave arriva completa con il prefisso da conf redis.oprion.prefix + il prefisso della supercache
65
        // del tipo 'gescat_laravel_database_supercache:'
66
        $prefix = config('database.redis.options')['prefix'] . config('supercache.prefix');
67
        $cleanedKey = str_replace(['{', '}'], '', $key);
68
        if (!Str::startsWith($cleanedKey, $prefix)) {
69
            return;
70
        }
71
72
        if ($this->useNamespace && $this->option('namespace_id') !== null && !Str::endsWith($cleanedKey, 'ns' . $this->option('namespace_id'))) {
73
            return;
74
        }
75
76
        $original_key = str_replace(config('database.redis.options')['prefix'], '', $key);
77
        // $original_key = $this->superCache->getOriginalKey($key);
78
        $hash_key = crc32($original_key); // questo hash mi serve poi nello script LUA in quanto Redis non ha nativa la funzione crc32, ma solo il crc16 che però non è nativo in php
79
        $this->batch[] = $original_key . '|' . $hash_key; // faccio la concatenzazione con il '|' come separatore in quanto Lua non supporta array multidimensionali
80
81
        /* Inizio Log su Elastic */
82
        try {
83
            $logToElasticFunction = config('supercache.log_to_elastic_function');
84
            // Metodo del progetto
85
            if (is_callable($logToElasticFunction)) {
86
                $logToElasticFunction('REDIS_EXPIRED', $original_key);
87
            }
88
        } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
89
        }
90
        /* Fine Log su Elastic */
91
    }
92
93
    /**
94
     * Verifica se è passato abbastanza tempo da processare il batch.
95
     */
96
    protected function shouldProcessBatchByTime(): bool
97
    {
98
        static $lastBatchTime = null;
99
        if (!$lastBatchTime) {
100
            $lastBatchTime = time();
101
102
            return false;
103
        }
104
105
        if ((time() - $lastBatchTime) >= $this->timeThreshold) {
106
            $lastBatchTime = time();
107
108
            return true;
109
        }
110
111
        return false;
112
    }
113
114
    protected function processBatchOnCluster(): void
115
    {
116
        foreach ($this->batch as $key) {
117
            $explodeKey = explode('|', $key);
118
            $cleanedKey = str_replace(['{', '}'], '', $explodeKey[0]);
119
            $this->superCache->forget($cleanedKey, $this->option('connection_name'), true, true, true);
120
        }
121
122
        $this->batch = [];
123
    }
124
125
    /**
126
     * Processa le chiavi accumulate in batch tramite uno script Lua.
127
     */
128
    protected function processBatchOnStandalone(): void
129
    {
130
        $debug = 'Processo batch: ' . implode(', ', $this->batch) . PHP_EOL .
0 ignored issues
show
Unused Code introduced by
The assignment to $debug is dead and can be removed.
Loading history...
131
            'Host: ' . $this->option('host') . PHP_EOL .
132
            'Port: ' . $this->option('port') . PHP_EOL;
133
134
        $luaScript = <<<'LUA'
135
136
        local success, result = pcall(function()
137
            local keys = ARGV
138
            local prefix = ARGV[1]
139
            local database_prefix = ARGV[3]
140
            local shard_count = ARGV[2]
141
            -- redis.log(redis.LOG_NOTICE, 'prefix: ' .. prefix);
142
            -- redis.log(redis.LOG_NOTICE, 'database_prefix: ' .. database_prefix);
143
            -- redis.log(redis.LOG_NOTICE, 'shard_count: ' .. shard_count);
144
            for i, key in ipairs(keys) do
145
                -- salto le prime 3 chiavi che ho usato come settings
146
                if i > 3 then
147
                    local row = {}
148
                    for value in string.gmatch(key, "[^|]+") do
149
                        table.insert(row, value)
150
                    end
151
                    local fullKey = database_prefix .. row[1]
152
                    -- redis.log(redis.LOG_NOTICE, 'Chiave Redis Expired: ' .. fullKey)
153
                    -- Controlla se la chiave è effettivamente scaduta
154
                    if redis.call('EXISTS', fullKey) == 0 then
155
                        -- local tagsKey = prefix .. 'tags:' .. row[1]
156
                        local tagsKey = fullKey .. ':tags'
157
                        -- redis.log(redis.LOG_NOTICE, 'Tag: ' .. tagsKey);
158
                        local tags = redis.call("SMEMBERS", tagsKey)
159
                        -- redis.log(redis.LOG_NOTICE, 'Tags associati: ' .. table.concat(tags, ", "));
160
                        -- Rimuove la chiave dai set di tag associati
161
                        for j, tag in ipairs(tags) do
162
                            local shardIndex = tonumber(row[2]) % tonumber(shard_count)
163
                            local shardKey = database_prefix .. prefix .. "tag:" .. tag .. ":shard:" .. shardIndex
164
                            -- redis.log(redis.LOG_NOTICE, 'Rimuovo la chiave dallo shard: ' .. row[1]);
165
                            redis.call("SREM", shardKey, row[1])
166
                            -- redis.log(redis.LOG_NOTICE, 'Rimossa chiave tag: ' .. shardKey);
167
                        end
168
                        -- Rimuove l'associazione della chiave con i tag
169
                        redis.call("DEL", tagsKey)
170
                        -- redis.log(redis.LOG_NOTICE, 'Rimossa chiave tags: ' .. tagsKey);
171
                    else
172
                        redis.log(redis.LOG_WARNING, 'la chiave ' .. fullKey .. ' è ancora attiva');
173
                    end
174
                end
175
            end
176
        end)
177
        if not success then
178
            redis.log(redis.LOG_WARNING, "Errore durante l'esecuzione del batch: " .. result)
179
            return result;
180
        end
181
        return "OK"
182
        LUA;
183
184
185
        try {
186
            // Esegue lo script Lua passando le chiavi in batch
187
            $connection = $this->redis->getNativeRedisConnection($this->option('connection_name'), $this->option('host'), $this->option('port'));
188
189
            $return = $connection['connection']->eval($luaScript, [config('supercache.prefix'), config('supercache.num_shards'), config('database.redis.options')['prefix'], ...$this->batch], 0);
190
            if ($return !== 'OK') {
191
                Log::error('Errore durante l\'esecuzione dello script Lua: ' . $return);
192
            }
193
            // Pulisce il batch dopo il successo
194
            $this->batch = [];
195
            // Essendo una connessione nativa va chiusa
196
            $connection['connection']->close();
197
        } catch (\Throwable $e) {
198
            Log::error('Errore durante l\'esecuzione dello script Lua: ' . $e->getMessage());
199
        }
200
    }
201
202
    public function handle(): void
203
    {
204
        if (!$this->checkRedisNotifications()) {
205
            $this->error('Le notifiche di scadenza di Redis non sono abilitate. Abilitale per usare il listener.');
206
        }
207
208
        try {
209
            $async_connection = $this->redis->getNativeRedisConnection($this->option('connection_name'), $this->option('host'), $this->option('port'));
210
            $pattern = '__keyevent@' . $async_connection['database'] . '__:expired';
211
            // La psubscribe è BLOCCANTE, il command resta attivo finchè non cade la connessione
212
            $async_connection['connection']->psubscribe([$pattern], function ($redis, $channel, $message, $key) {
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type array|string expected by parameter $callback of Redis::psubscribe(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

212
            $async_connection['connection']->psubscribe([$pattern], /** @scrutinizer ignore-type */ function ($redis, $channel, $message, $key) {
Loading history...
213
                $advancedMode = config('supercache.advancedMode', 0) === 1;
214
                if ($advancedMode) {
215
                    $this->onExpireEvent($key);
216
217
                    // Verifica se è necessario processare il batch
218
                    // In caso di un cluster Redis il primo che arriva al count impostato fa scattare la pulizia.
219
                    // Possono andare in conflitto? No, perchè ogni nodo ha i suoi eventi, per cui non può esserci lo stesso evento expire su più nodi
220
                    if (count($this->batch) >= $this->batchSizeThreshold || $this->shouldProcessBatchByTime()) {
221
                        // if (config('database.redis.clusters.' . ($this->option('connection_name') ?? 'default')) !== null) {
222
                        $this->processBatchOnCluster();
223
                        // } else {
224
                        //    $this->processBatchOnStandalone();
225
                        // }
226
                    }
227
                }
228
            });
229
        } catch (\Throwable $e) {
230
            $error = 'Errore durante la sottoscrizione agli eventi EXPIRED:' . PHP_EOL .
231
                'Host: ' . $this->option('host') . PHP_EOL .
232
                'Port: ' . $this->option('port') . PHP_EOL .
233
                'Connection Name: ' . $this->option('connection_name') . PHP_EOL .
234
                'Namespace ID: ' . $this->option('namespace_id') . PHP_EOL .
235
                'Error: ' . $e->getMessage();
236
            Log::error($error);
237
        }
238
    }
239
}
240