Passed
Push — master ( b2d632...002539 )
by Darko
10:06
created

deleteFinishedAndOrphans()   F

Complexity

Conditions 16
Paths 2304

Size

Total Lines 120
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 80
c 1
b 0
f 0
dl 0
loc 120
rs 1.5563
cc 16
nc 2304
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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).
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCLI->header(.... App\Services\PHP_EOL) targeting Blacklight\ColorCLI::header() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

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.

Loading history...
Bug introduced by
Are you sure $this->colorCLI->header(.... App\Services\PHP_EOL) of type void can be used in concatenation? ( Ignorable by Annotation )

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

29
            echo /** @scrutinizer ignore-type */ $this->colorCLI->header('Process Releases -> Delete finished collections.'.PHP_EOL).
Loading history...
30
                $this->colorCLI->primary(sprintf(
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCLI->primary...etentionhours')), true) targeting Blacklight\ColorCLI::primary() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

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.

Loading history...
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;
0 ignored issues
show
Unused Code introduced by
The assignment to $affected is dead and can be removed.
Loading history...
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).
0 ignored issues
show
Bug introduced by
Are you sure $this->colorCLI->header(.... App\Services\PHP_EOL) of type void can be used in concatenation? ( Ignorable by Annotation )

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

90
                echo /** @scrutinizer ignore-type */ $this->colorCLI->header('Process Releases -> Remove CBP orphans.'.PHP_EOL).
Loading history...
Bug introduced by
Are you sure the usage of $this->colorCLI->header(.... App\Services\PHP_EOL) targeting Blacklight\ColorCLI::header() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

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.

Loading history...
91
                    $this->colorCLI->primary('Deleting orphaned collections.');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->colorCLI->primary...orphaned collections.') targeting Blacklight\ColorCLI::primary() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

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.

Loading history...
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