Files
2026-05-30 23:31:00 +02:00

197 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Binaries;
use App\Models\MissedPart;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Handles missed parts tracking and repair during header processing.
*/
final class MissedPartHandler
{
private int $partRepairLimit;
private int $partRepairMaxTries;
private int $chunkSize;
public function __construct(int $partRepairLimit = 15000, int $partRepairMaxTries = 3, int $chunkSize = 500)
{
$this->partRepairLimit = $partRepairLimit;
$this->partRepairMaxTries = $partRepairMaxTries;
$this->chunkSize = max(50, min(1000, $chunkSize));
}
/**
* Add missing article numbers to the repair queue.
*
* @param array<int, int|string> $numbers
*/
public function addMissingParts(array $numbers, int $groupId): void
{
if (empty($numbers)) {
return;
}
$driver = DB::getDriverName();
if ($driver === 'sqlite') {
$this->addMissingPartsSqlite($numbers, $groupId);
return;
}
$this->addMissingPartsMysql($numbers, $groupId);
}
/**
* @param array<int, int|string> $numbers
*/
private function addMissingPartsSqlite(array $numbers, int $groupId): void
{
foreach (array_chunk(array_unique($numbers), $this->chunkSize) as $chunk) {
$placeholders = [];
$bindings = [];
foreach ($chunk as $number) {
$placeholders[] = '(?, ?, 1)';
$bindings[] = $number;
$bindings[] = $groupId;
}
DB::statement(
'INSERT INTO missed_parts (numberid, groups_id, attempts) VALUES '.implode(',', $placeholders).' ON CONFLICT(numberid, groups_id) DO UPDATE SET attempts = attempts + 1',
$bindings
);
}
}
/**
* @param array<int, int|string> $numbers
*/
private function addMissingPartsMysql(array $numbers, int $groupId): void
{
foreach (array_chunk(array_unique($numbers), $this->chunkSize) as $chunk) {
$placeholders = [];
$bindings = [];
foreach ($chunk as $number) {
$placeholders[] = '(?, ?, 1)';
$bindings[] = $number;
$bindings[] = $groupId;
}
DB::insert(
'INSERT INTO missed_parts (numberid, groups_id, attempts) VALUES '.implode(',', $placeholders).' ON DUPLICATE KEY UPDATE attempts = attempts + 1',
$bindings
);
}
}
/**
* Remove successfully repaired parts from the queue.
*
* @param array<int, int|string> $numbers
*/
public function removeRepairedParts(array $numbers, int $groupId): void
{
if (empty($numbers)) {
return;
}
try {
// Single DELETE — InnoDB autocommits one statement atomically, so
// an explicit transaction here would just add round-trips.
DB::table('missed_parts')
->where('groups_id', $groupId)
->whereIn('numberid', $numbers)
->delete();
} catch (\Throwable $e) {
if (config('app.debug') === true) {
Log::warning('removeRepairedParts failed: '.$e->getMessage());
}
}
}
/**
* Get parts that need repair for a group.
*
* @return array<int, \stdClass> Array of missed parts
*/
public function getMissingParts(int $groupId): array
{
try {
return DB::table('missed_parts')
->where('groups_id', $groupId)
->where('attempts', '<', $this->partRepairMaxTries)
->orderBy('numberid')
->limit($this->partRepairLimit)
->get()
->all();
} catch (\PDOException $e) {
if ($e->getMessage() === 'SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction') {
Log::notice('Deadlock occurred while fetching missed parts');
}
return [];
}
}
/**
* Increment attempts for parts that weren't repaired.
*/
public function incrementAttempts(int $groupId, int $maxNumberId): void
{
DB::table('missed_parts')
->where('groups_id', $groupId)
->where('numberid', '<=', $maxNumberId)
->increment('attempts');
}
/**
* Increment attempts for specific article range (part repair NNTP failures).
*/
public function incrementRangeAttempts(int $groupId, int $first, int $last): void
{
if ($first === $last) {
MissedPart::query()
->where('groups_id', $groupId)
->where('numberid', $first)
->increment('attempts');
} else {
MissedPart::query()
->where('groups_id', $groupId)
->whereIn('numberid', range($first, $last))
->increment('attempts');
}
}
/**
* Get count of remaining missed parts.
*/
public function getCount(int $groupId, int $maxNumberId): int
{
return DB::table('missed_parts')
->where('groups_id', $groupId)
->where('numberid', '<=', $maxNumberId)
->count('id');
}
/**
* Remove parts that exceeded max tries.
*/
public function cleanupExhaustedParts(int $groupId): void
{
// Single DELETE — InnoDB autocommits atomically; the explicit
// transaction wrapper would just add round-trips.
DB::table('missed_parts')
->where('groups_id', $groupId)
->where('attempts', '>=', $this->partRepairMaxTries)
->delete();
}
}