SuperCacheInvalidationHelper::acquireShardLock()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 4
eloc 9
c 3
b 1
f 0
nc 5
nop 4
dl 0
loc 17
rs 9.9666
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
        ?int $shard = -1,
35
    ): void {
36
        if ($shard < 0) {
37
            $shard = crc32($identifier) % ($totalShards > 0 ? $totalShards : config('super_cache_invalidate.total_shards', 10));
38
        }
39
        $redisConnectionName = $connection_name ?? config('super_cache_invalidate.default_connection_name');
40
        $data = [
41
            'type' => $type,
42
            'identifier' => $identifier,
43
            'connection_name' => $redisConnectionName,
44
            'reason' => $reason,
45
            'priority' => $priority,
46
            'event_time' => $event_time ?? now(),
47
            'processed' => $processed, // ATTENZIONE, poichè abbiamo solo 2 priorità, nel caso di priorità 1 verrà passato 1 perchè l'invalidazione la fa il progetto
48
            'shard' => $shard,
49
        ];
50
51
        $maxAttempts = 5;
52
        $attempts = 0;
53
        $insertOk = false;
54
55
        while ($attempts < $maxAttempts && !$insertOk) {
56
            // DB::beginTransaction();
57
58
            try {
59
                // Cerca di bloccare il record per l'inserimento
60
                switch ($processed) {
61
                    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...
62
                        $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

62
                        $partitionCache_invalidation_events = $this->getCacheInvalidationEventsUnprocessedPartitionName($shard, /** @scrutinizer ignore-type */ $priority);
Loading history...
63
                        break;
64
                    case 1:
65
                        $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

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