Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
RecoverPendingNotifications
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
2 / 2
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\BackgroundJob;
10
11use OCA\Libresign\AppInfo\Application;
12use OCA\Libresign\Db\NotificationDeliveryMapper;
13use OCP\AppFramework\Utility\ITimeFactory;
14use OCP\BackgroundJob\IJob;
15use OCP\BackgroundJob\IJobList;
16use OCP\BackgroundJob\TimedJob;
17use OCP\IAppConfig;
18
19/** Restore due jobs lost after a crash, without transmitting or replaying uncertain results. */
20final class RecoverPendingNotifications extends TimedJob {
21    private const BATCH_SIZE = 50;
22    private const MAX_INSPECTED = 500;
23    private const MAX_QUEUED = 50;
24    private const CURSOR_KEY = 'notification_recovery_last_scanned_id';
25
26    public function __construct(
27        ITimeFactory $time,
28        private NotificationDeliveryMapper $deliveries,
29        private IJobList $jobs,
30        private IAppConfig $appConfig,
31    ) {
32        parent::__construct($time);
33        $this->setInterval(60);
34        $this->setTimeSensitivity(IJob::TIME_SENSITIVE);
35    }
36
37    #[\Override]
38    public function run($argument): void {
39        $cursor = max(0, $this->appConfig->getValueInt(Application::APP_ID, self::CURSOR_KEY, 0));
40        $inspected = 0;
41        $queued = 0;
42        while ($inspected < self::MAX_INSPECTED && $queued < self::MAX_QUEUED) {
43            $limit = min(self::BATCH_SIZE, self::MAX_INSPECTED - $inspected);
44            $batch = $this->deliveries->findDue($limit, $cursor);
45            if ($batch === []) {
46                $cursor = 0;
47                break;
48            }
49            foreach ($batch as $delivery) {
50                $cursor = $delivery->getId();
51                $inspected++;
52                $jobArgument = ['id' => $cursor];
53                // Nextcloud 36 add() resets an existing job's reservation.
54                if (!$this->jobs->has(DeliverNotification::class, $jobArgument)) {
55                    $this->jobs->add(DeliverNotification::class, $jobArgument);
56                    $queued++;
57                    if ($queued === self::MAX_QUEUED) {
58                        break;
59                    }
60                }
61            }
62            if (count($batch) < $limit && $queued < self::MAX_QUEUED) {
63                // End of the eligible ID range; next cron pass starts at the oldest.
64                $cursor = 0;
65                break;
66            }
67        }
68        // Keep progress across cron processes even when the first 500 due rows
69        // already have queued jobs. No delivery state or payload is stored here.
70        $this->appConfig->setValueInt(Application::APP_ID, self::CURSOR_KEY, $cursor);
71    }
72}