processEvents()   B
last analyzed

Complexity

Conditions 11
Paths 22

Size

Total Lines 58
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 11
eloc 31
c 4
b 0
f 0
nc 22
nop 0
dl 0
loc 58
rs 7.3166

How to fix   Long Method    Complexity   

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\SuperCacheInvalidate\Console;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Carbon;
7
use Illuminate\Support\Facades\Cache;
8
use Illuminate\Support\Facades\DB;
9
use Illuminate\Support\Str;
10
use Padosoft\SuperCacheInvalidate\Events\BatchCompletedEvent;
11
use Padosoft\SuperCacheInvalidate\Helpers\SuperCacheInvalidationHelper;
12
13
class ProcessCacheInvalidationEventsCommand extends Command
14
{
15
    protected $signature = 'supercache:process-invalidation
16
                            {--shard= : The shard number to process}
17
                            {--priority= : The priority level}
18
                            {--limit= : The maximum number of events to fetch per batch}
19
                            {--tag-batch-size= : The number of identifiers to process per invalidation batch}
20
                            {--connection_name= : The Redis connection name}
21
                            {--log_attivo= : Indica se il log delle operazioni è attivo (0=no, 1=si)}';
22
    protected $description = 'Process cache invalidation events for a given shard and priority';
23
    protected SuperCacheInvalidationHelper $helper;
24
    protected bool $log_attivo = false;
25
    protected string $connection_name = 'cache';
26
    protected int $tagBatchSize = 100;
27
    protected int $limit = 1000;
28
    protected int $priority = 0;
29
    protected int $shardId = 0;
30
    protected int $invalidation_window = 600;
31
32
    public function __construct(SuperCacheInvalidationHelper $helper)
33
    {
34
        parent::__construct();
35
        $this->helper = $helper;
36
    }
37
38
    private function getEventsToInvalidate(Carbon $processingStartTime): array
39
    {
40
        $partitionCache_invalidation_events = $this->helper->getCacheInvalidationEventsUnprocessedPartitionName($this->shardId, $this->priority);
41
42
        return DB::table(DB::raw("`cache_invalidation_events` PARTITION ({$partitionCache_invalidation_events})"))
43
            ->select(['id', 'type', 'identifier', 'connection_name', 'partition_key', 'event_time'])
44
            ->where('processed', '=', 0)
45
            ->where('shard', '=', $this->shardId)
46
            ->where('priority', '=', $this->priority)
47
            // Cerco tutte le chiavi/tag da invalidare per questo database redis
48
            ->where('connection_name', '=', $this->connection_name)
49
            ->where('event_time', '<', $processingStartTime)
50
            ->orderBy('event_time')
0 ignored issues
show
Bug introduced by
'event_time' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

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

50
            ->orderBy(/** @scrutinizer ignore-type */ 'event_time')
Loading history...
51
            ->limit($this->limit)
52
            ->get()
53
            ->toArray()
54
        ;
55
    }
56
57
    private function getStoreFromConnectionName(string $connection_name): ?string
58
    {
59
        // Cerca il nome dello store associato alla connessione Redis
60
        foreach (config('cache.stores') as $storeName => $storeConfig) {
61
            if (
62
                isset($storeConfig['driver'], $storeConfig['connection']) &&
63
                $storeConfig['driver'] === 'redis' &&
64
                $storeConfig['connection'] === $connection_name
65
            ) {
66
                return $storeName;
67
            }
68
        }
69
70
        return null;
71
    }
72
73
    private function logIf(string $message, string $level = 'info')
74
    {
75
        if (!$this->log_attivo && $level === 'info') {
76
            return;
77
        }
78
        $this->$level(now()->toDateTimeString() . ' Shard[' . $this->shardId . '] Priority[' . $this->priority . '] Connection[' . $this->connection_name . '] : ' . PHP_EOL . $message);
79
    }
80
81
    protected function processEvents(): void
82
    {
83
        $processingStartTime = now();
84
        // Recupero gli eventi da invalidare
85
        $events = $this->getEventsToInvalidate($processingStartTime);
86
        $this->logIf('Trovati ' . count($events) . ' Eventi da invalidare');
87
        if (count($events) === 0) {
88
            return;
89
        }
90
91
        // Creiamo un array per memorizzare il valore più vecchio per ogni coppia (type, identifier) così elimino i doppioni che si sono accumulati nel tempo di apertura della finestra
92
        $unique_events = [];
93
94
        foreach ($events as $event) {
95
            $key = $event->type . ':' . $event->identifier; // Chiave univoca per type + identifier
96
            $event->event_time = \Illuminate\Support\Carbon::parse($event->event_time);
97
            // Quando la chiave non esiste o il nuovo valore ha un event_time più vecchio, lo sostituisco così a parità di tag ho sempre quello più vecchio e mi baso su quello per verificare la finestra
98
            if (!isset($unique_events[$key]) || $event->event_time <= $unique_events[$key]->event_time) {
99
                $unique_events[$key] = $event;
100
            }
101
        }
102
103
        $unique_events = array_values($unique_events);
104
105
        $this->logIf('Eventi unici ' . count($unique_events));
106
        // Quando il numero di eventi unici è inferiore al batchSize e quello più vecchio aspetta da almeno due minuti, mando l'invalidazione.
107
        // Questo serve per i siti piccoli che hanno pochi eventi, altrimenti si rischia di attendere troppo per invalidare i tags
108
        // In questo caso invalido i tag/key "unici" e setto a processed = 1 tutti quelli recuperati
109
        if (count($unique_events) < $this->tagBatchSize && $processingStartTime->diffInSeconds($unique_events[0]->event_time) >= 120) {
110
            $this->logIf('Il numero di eventi unici è inferiore al batchSize ( ' . $this->tagBatchSize . ' ) e sono passati più di due minuti, procedo');
111
            $this->processBatch($events, $unique_events);
112
113
            return;
114
        }
115
116
        // Altrimenti ho raggiunto il tagbatchsize e devo prendere solo quelli che hanno la finestra di invalidazione attiva
117
        $eventsToUpdate = [];
118
        $eventsAll = [];
119
        foreach ($unique_events as $event) {
120
            $elapsed = $processingStartTime->diffInSeconds($event->event_time, true);
121
            $typeFilter = $event->type;
122
            $identifierFilter = $event->identifier;
123
            if ($elapsed < $this->invalidation_window) {
124
                // Se la richiesta (cmq del più vecchio) non è nella finestra di invalidazione salto l'evento
125
                continue;
126
            }
127
            // altrimenti aggiungo l'evento a quelli da processare
128
            $eventsToUpdate[] = $event;
129
            // e recupero tutti gli ID che hanno quel tag/key
130
            $eventsAll[] = array_filter($events, function ($event) use ($typeFilter, $identifierFilter) {
131
                return $event->type === $typeFilter && $event->identifier === $identifierFilter;
132
            });
133
        }
134
        if (count($eventsToUpdate) === 0) {
135
            return;
136
        }
137
138
        $this->processBatch(array_merge(...$eventsAll), $eventsToUpdate);
139
    }
140
141
    protected function processBatch(array $allEvents, array $eventsToInvalidate): void
142
    {
143
        // Separo le chiavi dai tags
144
        $keys = [];
145
        $tags = [];
146
        // Prima di processare tutti gli eventi, assegno un batch_ID univoco a tutti gli eventi
147
        $batch_ID = (string) Str::uuid();
148
149
        foreach ($eventsToInvalidate as $item) {
150
            switch ($item->type) {
151
                case 'key':
152
                    $keys[] = $item->identifier . '§' . $item->connection_name;
153
                    break;
154
                case 'tag':
155
                    $tags[] = $item->identifier . '§' . $item->connection_name;
156
                    break;
157
            }
158
        }
159
160
        $this->logIf('Invalido ' . count($keys) . ' chiavi e ' . count($tags) . ' tags' . ' per un totale di ' . count($allEvents) . ' events_ID');
161
162
        if (!empty($keys)) {
163
            $this->invalidateKeys($keys);
164
        }
165
166
        if (!empty($tags)) {
167
            $this->invalidateTags($tags);
168
        }
169
170
        // Disabilita i controlli delle chiavi esterne e dei vincoli univoci
171
        DB::statement('SET FOREIGN_KEY_CHECKS=0;');
172
        DB::statement('SET UNIQUE_CHECKS=0;');
173
174
        // Mark event as processed
175
        // QUI NON VA USATA PARTITION perchè la cross partition è più lenta! Però è necessario utilizzare la $partition_key per sfruttare l'indice della primary key (id+partition_key)
176
        DB::table('cache_invalidation_events')
177
            ->whereIn('id', array_map(fn ($event) => $event->id, $allEvents))
178
            ->whereIn('partition_key', array_map(fn ($event) => $event->partition_key, $allEvents))
179
            ->update(['processed' => 1, 'batch_ID' => $batch_ID, 'updated_at' => now()])
180
        ;
181
        // Riattiva i controlli
182
        DB::statement('SET FOREIGN_KEY_CHECKS=1;');
183
        DB::statement('SET UNIQUE_CHECKS=1;');
184
185
        // A questo punto avviso il gescat che le chiavi/tags sono stati puliti, per cui può procedere alla pulizia della CDN
186
        event(new BatchCompletedEvent($batch_ID, $this->shardId));
187
    }
188
189
    /**
190
     * Invalidate cache keys.
191
     *
192
     * @param array $keys Array of cache keys to invalidate
193
     */
194
    protected function invalidateKeys(array $keys): void
195
    {
196
        $callback = config('super_cache_invalidate.key_invalidation_callback');
197
198
        // Anche in questo caso va fatto un loop perchè le chiavi potrebbero stare in database diversi
199
        foreach ($keys as $keyAndConnectionName) {
200
            [$key, $connection_name] = explode('§', $keyAndConnectionName);
201
202
            // Metodo del progetto
203
            if (is_callable($callback)) {
204
                $callback($key, $connection_name);
205
                continue;
206
            }
207
            // oppure di default uso Laravel
208
            $storeName =  $this->getStoreFromConnectionName($connection_name);
209
210
            if ($storeName === null) {
211
                continue;
212
            }
213
            Cache::store($storeName)->forget($key);
214
        }
215
    }
216
217
    /**
218
     * Invalidate cache tags.
219
     *
220
     * @param array $tags Array of cache tags to invalidate
221
     */
222
    protected function invalidateTags(array $tags): void
223
    {
224
        $callback = config('super_cache_invalidate.tag_invalidation_callback');
225
226
        $groupByConnection = [];
227
228
        // Anche in questo caso va fatto un loop perchè i tags potrebbero stare in database diversi,
229
        // ma per ottimizzare possiamo raggruppare le operazioni per connessione
230
        foreach ($tags as $tagAndConnectionName) {
231
            // chiave e connessione
232
            [$key, $connection] = explode('§', $tagAndConnectionName);
233
234
            // Aggiungo la chiave alla connessione appropriata
235
            $groupByConnection[$connection][] = $key;
236
        }
237
        if (is_callable($callback)) {
238
            foreach ($groupByConnection as $connection_name => $arrTags) {
239
                $callback($arrTags, $connection_name);
240
            }
241
242
            return;
243
        }
244
        foreach ($groupByConnection as $connection_name => $arrTags) {
245
            $storeName =  $this->getStoreFromConnectionName($connection_name);
246
            if ($storeName === null) {
247
                continue;
248
            }
249
            Cache::store($storeName)->tags($arrTags)->flush();
250
        }
251
    }
252
253
    /**
254
     * Execute the console command.
255
     */
256
    public function handle(): void
257
    {
258
        // Recupero dei parametri impostati nel command
259
        $this->shardId = (int) $this->option('shard');
260
        $this->priority = (int) $this->option('priority');
261
        $limit = $this->option('limit') ?? config('super_cache_invalidate.processing_limit');
262
        $this->limit = (int) $limit;
263
        $tagBatchSize = $this->option('tag-batch-size') ?? config('super_cache_invalidate.tag_batch_size');
264
        $this->tagBatchSize = (int) $tagBatchSize;
265
        $this->connection_name = $this->option('connection_name') ?? config('super_cache_invalidate.default_connection_name');
266
        $this->log_attivo = $this->option('log_attivo') && (int)$this->option('log_attivo') === 1;
267
        $this->invalidation_window = (int) config('super_cache_invalidate.invalidation_window');
268
        $lockTimeout = (int) config('super_cache_invalidate.lock_timeout');
0 ignored issues
show
Unused Code introduced by
The assignment to $lockTimeout is dead and can be removed.
Loading history...
269
270
        // Acquisisco il lock in modo da essere sicura che le esecuzioni non si accavallino
271
        // $lockValue = $this->helper->acquireShardLock($this->shardId, $this->priority, $lockTimeout, $this->connection_name);
272
        $this->logIf('Starting Elaborazione ...' . $this->invalidation_window);
273
        // if (!$lockValue) {
274
        //    return;
275
        // }
276
        $startTime = microtime(true);
277
        try {
278
            $this->processEvents();
279
        } catch (\Throwable $e) {
280
            $this->logIf(sprintf(
281
                "Eccezione: %s in %s on line %d\nStack trace:\n%s",
282
                $e->getMessage(),
283
                $e->getFile(),
284
                $e->getLine(),
285
                $e->getTraceAsString()
286
            ), 'error');
287
        }
288
        $executionTime = (microtime(true) - $startTime) * 1000;
289
        $this->logIf('Fine Elaborazione - Tempo di esecuzione: ' . $executionTime . ' millisec.');
290
    }
291
}
292