Passed
Push — master ( ae3f18...b8bba4 )
by Darko
10:59
created

MissedPartHandler   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 165
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 72
dl 0
loc 165
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A removeRepairedParts() 0 18 5
A __construct() 0 4 1
A getMissingParts() 0 18 3
A incrementRangeAttempts() 0 12 2
A getCount() 0 11 1
A addMissingPartsSqlite() 0 6 2
A cleanupExhaustedParts() 0 11 1
A addMissingParts() 0 15 3
A incrementAttempts() 0 7 1
A addMissingPartsMysql() 0 8 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Services\Binaries;
6
7
use App\Models\MissedPart;
8
use Illuminate\Support\Facades\DB;
9
use Illuminate\Support\Facades\Log;
10
11
/**
12
 * Handles missed parts tracking and repair during header processing.
13
 */
14
final class MissedPartHandler
15
{
16
    private int $partRepairLimit;
17
18
    private int $partRepairMaxTries;
19
20
    public function __construct(int $partRepairLimit = 15000, int $partRepairMaxTries = 3)
21
    {
22
        $this->partRepairLimit = $partRepairLimit;
23
        $this->partRepairMaxTries = $partRepairMaxTries;
24
    }
25
26
    /**
27
     * Add missing article numbers to the repair queue.
28
     */
29
    public function addMissingParts(array $numbers, int $groupId): void
30
    {
31
        if (empty($numbers)) {
32
            return;
33
        }
34
35
        $driver = DB::getDriverName();
36
37
        if ($driver === 'sqlite') {
38
            $this->addMissingPartsSqlite($numbers, $groupId);
39
40
            return;
41
        }
42
43
        $this->addMissingPartsMysql($numbers, $groupId);
44
    }
45
46
    private function addMissingPartsSqlite(array $numbers, int $groupId): void
47
    {
48
        foreach ($numbers as $number) {
49
            DB::statement(
50
                'INSERT INTO missed_parts (numberid, groups_id, attempts) VALUES (?, ?, 1) ON CONFLICT(numberid, groups_id) DO UPDATE SET attempts = attempts + 1',
51
                [$number, $groupId]
52
            );
53
        }
54
    }
55
56
    private function addMissingPartsMysql(array $numbers, int $groupId): void
57
    {
58
        $insertStr = 'INSERT INTO missed_parts (numberid, groups_id) VALUES ';
59
        foreach ($numbers as $number) {
60
            $insertStr .= '('.$number.','.$groupId.'),';
61
        }
62
63
        DB::insert(rtrim($insertStr, ',').' ON DUPLICATE KEY UPDATE attempts=attempts+1');
64
    }
65
66
    /**
67
     * Remove successfully repaired parts from the queue.
68
     */
69
    public function removeRepairedParts(array $numbers, int $groupId): void
70
    {
71
        if (empty($numbers)) {
72
            return;
73
        }
74
75
        $sql = 'DELETE FROM missed_parts WHERE numberid in (';
76
        foreach ($numbers as $number) {
77
            $sql .= $number.',';
78
        }
79
80
        try {
81
            DB::transaction(static function () use ($groupId, $sql) {
82
                DB::delete(rtrim($sql, ',').') AND groups_id = '.$groupId);
83
            }, 10);
84
        } catch (\Throwable $e) {
85
            if (config('app.debug') === true) {
86
                Log::warning('removeRepairedParts failed: '.$e->getMessage());
87
            }
88
        }
89
    }
90
91
    /**
92
     * Get parts that need repair for a group.
93
     *
94
     * @return array Array of missed parts
95
     */
96
    public function getMissingParts(int $groupId): array
97
    {
98
        try {
99
            return DB::select(
100
                sprintf(
101
                    'SELECT * FROM missed_parts WHERE groups_id = %d AND attempts < %d ORDER BY numberid ASC LIMIT %d',
102
                    $groupId,
103
                    $this->partRepairMaxTries,
104
                    $this->partRepairLimit
105
                )
106
            );
107
        } catch (\PDOException $e) {
108
            if ($e->getMessage() === 'SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction') {
109
                Log::notice('Deadlock occurred while fetching missed parts');
110
                DB::rollBack();
111
            }
112
113
            return [];
114
        }
115
    }
116
117
    /**
118
     * Increment attempts for parts that weren't repaired.
119
     */
120
    public function incrementAttempts(int $groupId, int $maxNumberId): void
121
    {
122
        DB::update(
123
            sprintf(
124
                'UPDATE missed_parts SET attempts = attempts + 1 WHERE groups_id = %d AND numberid <= %d',
125
                $groupId,
126
                $maxNumberId
127
            )
128
        );
129
    }
130
131
    /**
132
     * Increment attempts for specific article range (part repair NNTP failures).
133
     */
134
    public function incrementRangeAttempts(int $groupId, int $first, int $last): void
135
    {
136
        if ($first === $last) {
137
            MissedPart::query()
138
                ->where('groups_id', $groupId)
139
                ->where('numberid', $first)
140
                ->increment('attempts');
141
        } else {
142
            MissedPart::query()
143
                ->where('groups_id', $groupId)
144
                ->whereIn('numberid', range($first, $last))
145
                ->increment('attempts');
146
        }
147
    }
148
149
    /**
150
     * Get count of remaining missed parts.
151
     */
152
    public function getCount(int $groupId, int $maxNumberId): int
153
    {
154
        $result = DB::select(
155
            sprintf(
156
                'SELECT COUNT(id) AS num FROM missed_parts WHERE groups_id = %d AND numberid <= %d',
157
                $groupId,
158
                $maxNumberId
159
            )
160
        );
161
162
        return $result[0]->num ?? 0;
163
    }
164
165
    /**
166
     * Remove parts that exceeded max tries.
167
     */
168
    public function cleanupExhaustedParts(int $groupId): void
169
    {
170
        DB::transaction(function () use ($groupId) {
171
            DB::delete(
172
                sprintf(
173
                    'DELETE FROM missed_parts WHERE attempts >= %d AND groups_id = %d',
174
                    $this->partRepairMaxTries,
175
                    $groupId
176
                )
177
            );
178
        }, 10);
179
    }
180
}
181
182