1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace App\Services; |
4
|
|
|
|
5
|
|
|
use App\Models\Collection; |
6
|
|
|
use App\Models\Settings; |
7
|
|
|
use Blacklight\ColorCLI; |
8
|
|
|
use Illuminate\Support\Facades\DB; |
9
|
|
|
use Illuminate\Support\Str; |
10
|
|
|
|
11
|
|
|
class CollectionCleanupService |
12
|
|
|
{ |
13
|
|
|
public function __construct( |
14
|
|
|
private readonly ColorCLI $colorCLI |
15
|
|
|
) {} |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Deletes finished/old collections, cleans orphans, and removes collections missed after NZB creation. |
19
|
|
|
* Mirrors the previous ProcessReleases::deleteCollections logic. |
20
|
|
|
* |
21
|
|
|
* @return int total deleted rows across operations (approximate) |
22
|
|
|
*/ |
23
|
|
|
public function deleteFinishedAndOrphans(bool $echoCLI): int |
24
|
|
|
{ |
25
|
|
|
$startTime = now()->toImmutable(); |
26
|
|
|
$deletedCount = 0; |
27
|
|
|
|
28
|
|
|
if ($echoCLI) { |
29
|
|
|
echo $this->colorCLI->header('Process Releases -> Delete finished collections.'.PHP_EOL). |
|
|
|
|
30
|
|
|
$this->colorCLI->primary(sprintf( |
|
|
|
|
31
|
|
|
'Deleting collections/binaries/parts older than %d hours.', |
32
|
|
|
Settings::settingValue('partretentionhours') |
33
|
|
|
), true); |
34
|
|
|
} |
35
|
|
|
|
36
|
|
|
// Batch-delete old collections using a safe id-subselect to avoid read-then-delete races. |
37
|
|
|
$cutoff = now()->subHours(Settings::settingValue('partretentionhours')); |
38
|
|
|
$batchDeleted = 0; |
39
|
|
|
$maxRetries = 5; |
40
|
|
|
do { |
41
|
|
|
$affected = 0; |
|
|
|
|
42
|
|
|
$attempt = 0; |
43
|
|
|
do { |
44
|
|
|
try { |
45
|
|
|
// Delete by id list derived in a nested subquery to avoid "Record has changed since last read". |
46
|
|
|
$affected = DB::affectingStatement( |
47
|
|
|
'DELETE FROM collections WHERE id IN ( |
48
|
|
|
SELECT id FROM ( |
49
|
|
|
SELECT id FROM collections WHERE dateadded < ? ORDER BY id LIMIT 500 |
50
|
|
|
) AS x |
51
|
|
|
)', |
52
|
|
|
[$cutoff] |
53
|
|
|
); |
54
|
|
|
break; // success |
55
|
|
|
} catch (\Throwable $e) { |
56
|
|
|
// Retry on lock/timeout errors |
57
|
|
|
$attempt++; |
58
|
|
|
if ($attempt >= $maxRetries) { |
59
|
|
|
if ($echoCLI) { |
60
|
|
|
$this->colorCLI->error('Cleanup delete failed after retries: '.$e->getMessage()); |
61
|
|
|
} |
62
|
|
|
break; |
63
|
|
|
} |
64
|
|
|
usleep(20000 * $attempt); |
65
|
|
|
} |
66
|
|
|
} while (true); |
67
|
|
|
|
68
|
|
|
$batchDeleted += $affected; |
69
|
|
|
if ($affected < 500) { |
70
|
|
|
break; |
71
|
|
|
} |
72
|
|
|
// Brief pause to reduce pressure on the lock manager in busy systems. |
73
|
|
|
usleep(10000); |
74
|
|
|
} while (true); |
75
|
|
|
|
76
|
|
|
$deletedCount += $batchDeleted; |
77
|
|
|
|
78
|
|
|
if ($echoCLI) { |
79
|
|
|
$elapsed = now()->diffInSeconds($startTime, true); |
80
|
|
|
$this->colorCLI->primary( |
81
|
|
|
'Finished deleting '.$batchDeleted.' old collections/binaries/parts in '. |
82
|
|
|
$elapsed.Str::plural(' second', $elapsed), |
83
|
|
|
true |
84
|
|
|
); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
// Occasionally prune CBP orphans (low frequency to avoid heavy load). |
88
|
|
|
if (random_int(0, 200) <= 1) { |
89
|
|
|
if ($echoCLI) { |
90
|
|
|
echo $this->colorCLI->header('Process Releases -> Remove CBP orphans.'.PHP_EOL). |
|
|
|
|
91
|
|
|
$this->colorCLI->primary('Deleting orphaned collections.'); |
|
|
|
|
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
$deleted = 0; |
95
|
|
|
// NOTE: This JOIN DELETE can be heavy; consider batching if it becomes an issue in practice. |
96
|
|
|
$deleteQuery = Collection::query() |
97
|
|
|
->whereNull('binaries.id') |
98
|
|
|
->orWhereNull('parts.binaries_id') |
99
|
|
|
->leftJoin('binaries', 'collections.id', '=', 'binaries.collections_id') |
100
|
|
|
->leftJoin('parts', 'binaries.id', '=', 'parts.binaries_id') |
101
|
|
|
->delete(); |
102
|
|
|
|
103
|
|
|
if ($deleteQuery > 0) { |
104
|
|
|
$deleted = $deleteQuery; |
105
|
|
|
$deletedCount += $deleted; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
$totalTime = now()->diffInSeconds($startTime); |
109
|
|
|
|
110
|
|
|
if ($echoCLI) { |
111
|
|
|
$this->colorCLI->primary('Finished deleting '.$deleted.' orphaned collections in '.$totalTime.Str::plural(' second', $totalTime), true); |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
if ($echoCLI) { |
116
|
|
|
$this->colorCLI->primary('Deleting collections that were missed after NZB creation.', true); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
$deleted = 0; |
120
|
|
|
$collections = Collection::query() |
121
|
|
|
->where('releases.nzbstatus', '=', 1) |
122
|
|
|
->leftJoin('releases', 'releases.id', '=', 'collections.releases_id') |
123
|
|
|
->select('collections.id') |
124
|
|
|
->get(); |
125
|
|
|
|
126
|
|
|
foreach ($collections as $collection) { |
127
|
|
|
$deleted++; |
128
|
|
|
Collection::query()->where('id', $collection->id)->delete(); |
129
|
|
|
} |
130
|
|
|
$deletedCount += $deleted; |
131
|
|
|
|
132
|
|
|
$totalTime = now()->diffInSeconds($startTime, true); |
133
|
|
|
|
134
|
|
|
if ($echoCLI) { |
135
|
|
|
$this->colorCLI->primary( |
136
|
|
|
'Finished deleting '.$deleted.' collections missed after NZB creation in '.($totalTime).Str::plural(' second', $totalTime). |
137
|
|
|
PHP_EOL.'Removed '.number_format($deletedCount).' parts/binaries/collection rows in '.$totalTime.Str::plural(' second', $totalTime), |
138
|
|
|
true |
139
|
|
|
); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
return $deletedCount; |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.