Passed
Pull Request — main (#3)
by
unknown
03:28
created

SuperCacheInvalidationHelper::releaseShardLock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 2
b 0
f 0
nc 2
nop 4
dl 0
loc 6
rs 10
1
<?php
2
3
namespace Padosoft\SuperCacheInvalidate\Helpers;
4
5
use Illuminate\Support\Facades\DB;
6
use Illuminate\Support\Facades\Log;
7
use Illuminate\Support\Facades\Redis;
8
use Illuminate\Support\Carbon;
9
10
class SuperCacheInvalidationHelper
11
{
12
    /**
13
     * Insert a cache invalidation event into the database.
14
     *
15
     * @param string      $type            'key' or 'tag'
16
     * @param string      $identifier      The cache key or tag to invalidate
17
     * @param string|null $connection_name The Redis Connection name (optional, 'default')
18
     * @param string|null $reason          Reason for invalidation (optional)
19
     * @param int|null    $totalShards     Total number of shards (from config if null)
20
     * @param int|null    $priority        Priority of the event
21
     * @param int|null    $processed       Processed = 0 o 1
22
     * @param Carbon|null $event_time      Orario vero in cui si è richiesta l'invalidazione
23
     */
24
    public function insertInvalidationEvent(
25
        string $type,
26
        string $identifier,
27
        ?string $connection_name = null,
28
        ?string $reason = null,
29
        ?int $totalShards = 0,
30
        ?int $priority = 0,
31
        ?int $processed = 0,
32
        ?Carbon $event_time = null,
33
        /*?array $associatedIdentifiers = [],*/
34
    ): void {
35
        $shard = crc32($identifier) % ($totalShards > 0 ? $totalShards : config('super_cache_invalidate.total_shards', 10));
36
        $redisConnectionName = $connection_name ?? config('super_cache_invalidate.default_connection_name');
37
        $data = [
38
            'type' => $type,
39
            'identifier' => $identifier,
40
            'connection_name' => $redisConnectionName,
41
            'reason' => $reason,
42
            'priority' => $priority,
43
            'event_time' => $event_time ?? now(),
44
            'processed' => $processed, // ATTENZIONE, poichè abbiamo solo 2 priorità, nel caso di priorità 1 verrà passato 1 perchè l'invalidazione la fa il progetto
45
            'shard' => $shard,
46
        ];
47
48
        $maxAttempts = 5;
49
        $attempts = 0;
50
        $insertOk = false;
51
52
        while ($attempts < $maxAttempts && !$insertOk) {
53
            //DB::beginTransaction();
54
55
            try {
56
                // Cerca di bloccare il record per l'inserimento
57
                switch ($processed) {
58
                    case 0:
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $processed of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
59
                        $partitionCache_invalidation_events = $this->getCacheInvalidationEventsUnprocessedPartitionName($shard, $priority);
0 ignored issues
show
Bug introduced by
It seems like $priority can also be of type null; however, parameter $priorityId of Padosoft\SuperCacheInval...rocessedPartitionName() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

59
                        $partitionCache_invalidation_events = $this->getCacheInvalidationEventsUnprocessedPartitionName($shard, /** @scrutinizer ignore-type */ $priority);
Loading history...
60
                        break;
61
                    case 1:
62
                        $partitionCache_invalidation_events = $this->getCacheInvalidationEventsProcessedPartitionName($shard, $priority, $event_time ?? now());
0 ignored issues
show
Bug introduced by
It seems like $priority can also be of type null; however, parameter $priorityId of Padosoft\SuperCacheInval...rocessedPartitionName() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

62
                        $partitionCache_invalidation_events = $this->getCacheInvalidationEventsProcessedPartitionName($shard, /** @scrutinizer ignore-type */ $priority, $event_time ?? now());
Loading history...
63
                        break;
64
                    default:
65
                        $attempts = 5; // Mi fermo
66
                        throw new \RuntimeException('Invalid value for processed');
67
                }
68
                //$eventId = DB::table(DB::raw("`cache_invalidation_events` PARTITION ({$partitionCache_invalidation_events})"))->insertGetId($data);
69
                DB::table(DB::raw("`cache_invalidation_events` PARTITION ({$partitionCache_invalidation_events})"))->insert($data);
70
                // Insert associated identifiers
71
                // TODO JB 31/12/2024: per adesso commentato, da riattivare quando tutto funziona alla perfezione usando la partizione,
72
                // anche perchè la chiave primaria è data da id e partiton_key, per cui va capito cosa restituisce insertGetId e se è bloccante
73
                /*
74
                if (!empty($associatedIdentifiers)) {
75
                    $associations = [];
76
                    foreach ($associatedIdentifiers as $associated) {
77
                        $associations[] = [
78
                            'event_id' => $eventId,
79
                            'associated_type' => $associated['type'], // 'key' or 'tag'
80
                            'associated_identifier' => $associated['identifier'],
81
                            'connection_name' => $associated['connection_name'],
82
                            'created_at' => now(),
83
                        ];
84
                    }
85
                    DB::table('cache_invalidation_event_associations')->insert($associations);
86
                }
87
                */
88
                $insertOk = true;
89
                //DB::commit(); // Completa la transazione
90
            } catch (\Throwable $e) {
91
                //DB::rollBack(); // Annulla la transazione in caso di errore
92
                $attempts++;
93
                Log::error("SuperCacheInvalidate: impossibile eseguire insert, tentativo $attempts di $maxAttempts: " . $e->getMessage());
94
                // Logica per gestire i tentativi falliti
95
                if ($attempts >= $maxAttempts) {
96
                    // Salta il record dopo il numero massimo di tentativi
97
                    Log::error("SuperCacheInvalidate: impossibile eseguire insert dopo $maxAttempts tentativi: " . $e->getMessage());
98
                }
99
            }
100
        }
101
    }
102
103
    /**
104
     * Acquire a lock for processing a shard.
105
     *
106
     * @param  int          $shardId         The shard number
107
     * @param  int          $lockTimeout     Lock timeout in seconds
108
     * @param  string       $connection_name The Redis Connection name
109
     * @return string|false The lock value if acquired, false otherwise
110
     */
111
    public function acquireShardLock(int $shardId, int $priority, int $lockTimeout, string $connection_name): bool|string
112
    {
113
        $lockKey = 'shard_lock:' . $shardId . '_' . $priority;
114
        // Il metodo has/exists occupa troppa memoria!!!
115
        $retrieveValue = Redis::connection($connection_name)->get($lockKey);
116
        if ($retrieveValue !== null) {
117
            // Lock già attivo
118
            return false;
119
        }
120
        $lockValue = uniqid('', true);
121
        $isLocked = Redis::connection($connection_name)->set($lockKey, $lockValue);
122
123
        if ($lockTimeout > 0) {
124
            Redis::connection($connection_name)->expire($lockKey, $lockTimeout);
125
        }
126
127
        return $isLocked ? $lockValue : false;
128
    }
129
130
    /**
131
     * Release the lock for a shard.
132
     *
133
     * @param int    $shardId         The shard number
134
     * @param string $lockValue       The lock value to validate ownership
135
     * @param string $connection_name The Redis Connection name
136
     */
137
    public function releaseShardLock(int $shardId, int $priority, string $lockValue, string $connection_name): void
138
    {
139
        $lockKey = 'shard_lock:' . $shardId . '_' . $priority;
140
        $currentValue = Redis::connection($connection_name)->get($lockKey);
141
        if ($currentValue === $lockValue) {
142
            Redis::connection($connection_name)->del($lockKey);
143
        }
144
    }
145
146
    public function getCacheInvalidationEventsUnprocessedPartitionName(int $shardId, int $priorityId): string
147
    {
148
        $partitionValue = ($priorityId * 10) + $shardId;
149
        return "p_unprocessed_{$partitionValue}";
150
    }
151
152
    public function getCacheInvalidationEventsProcessedPartitionName(int $shardId, int $priorityId, Carbon $event_time): string
153
    {
154
        $partitionValue = ($event_time->year * 10000) + ($event_time->weekOfYear * 100) + ($priorityId * 10) + $shardId;
155
        return "p_processed_{$partitionValue}";
156
    }
157
}
158