• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 20917021827

12 Jan 2026 11:03AM UTC coverage: 43.867%. First build
20917021827

Pull #6436

github

web-flow
Merge ed16f8a03 into 8fe916f99
Pull Request #6436: feat: async parallel signing

242 of 775 new or added lines in 26 files covered. (31.23%)

6920 of 15775 relevant lines covered (43.87%)

4.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.06
/lib/Service/WorkerHealthService.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Service;
10

11
use OCA\Libresign\AppInfo\Application;
12
use OCA\Libresign\BackgroundJob\SignFileJob;
13
use OCA\Libresign\BackgroundJob\SignSingleFileJob;
14
use OCP\AppFramework\Utility\ITimeFactory;
15
use OCP\BackgroundJob\IJobList;
16
use OCP\IAppConfig;
17
use OCP\IBinaryFinder;
18
use Psr\Log\LoggerInterface;
19

20
/**
21
 * Service to ensure the signing worker is running and healthy.
22
 *
23
 * Checks if a worker process is needed and starts it on demand.
24
 */
25
class WorkerHealthService {
26
        private const CONFIG_KEY = 'worker_last_start_attempt';
27
        private const MIN_INTERVAL_BETWEEN_STARTS = 10; // seconds, avoid hammering with start attempts
28

29
        public function __construct(
30
                private IJobList $jobList,
31
                private IAppConfig $appConfig,
32
                private ITimeFactory $timeFactory,
33
                private IBinaryFinder $binaryFinder,
34
                private LoggerInterface $logger,
35
        ) {
36
        }
12✔
37

38
        public function isAsyncLocalEnabled(): bool {
39
                $signingMode = $this->appConfig->getValueString(Application::APP_ID, 'signing_mode', 'async');
3✔
40
                if ($signingMode !== 'async') {
3✔
41
                        return false;
1✔
42
                }
43

44
                $workerType = $this->appConfig->getValueString(Application::APP_ID, 'worker_type', 'local');
2✔
45
                if ($workerType !== 'local') {
2✔
46
                        return false;
1✔
47
                }
48

49
                return true;
1✔
50
        }
51

52
        /**
53
         * Ensure a worker is running to process signing jobs.
54
         * Start a local worker only when there are LibreSign jobs queued.
55
         */
56
        public function ensureWorkerRunning(): bool {
57
                try {
58
                        if (!$this->isAsyncLocalEnabled()) {
3✔
59
                                return false;
1✔
60
                        }
61

62
                        $desiredWorkers = $this->getDesiredWorkerCount();
2✔
63
                        $runningWorkers = $this->countRunningWorkers();
2✔
64
                        $needed = max(0, $desiredWorkers - $runningWorkers);
2✔
65

66
                        if ($needed === 0) {
2✔
67
                                return true; // already have enough workers
1✔
68
                        }
69

70
                        $this->attemptStartWorker($needed);
1✔
71
                        return true;
1✔
NEW
72
                } catch (\Throwable $e) {
×
73
                        // Log but don't fail the request if worker check/start fails
NEW
74
                        $this->logger->error('Failed to ensure worker is running: {error}', [
×
NEW
75
                                'error' => $e->getMessage(),
×
NEW
76
                                'exception' => $e,
×
NEW
77
                        ]);
×
NEW
78
                        return false;
×
79
                }
80
        }
81

82
        // NOTE: no health probing here; we rely on throttled start attempts and Nextcloud's worker lifecycle
83

84
        /**
85
         * Attempt to start a worker process.
86
         * Checks throttling to avoid starting too many processes.
87
         */
88
        private function attemptStartWorker(int $needed): void {
89
                $lastAttempt = (int)$this->appConfig->getValueInt('libresign', self::CONFIG_KEY, 0);
1✔
90
                $now = $this->timeFactory->getTime();
1✔
91
                $timeSinceLastAttempt = $now - $lastAttempt;
1✔
92

93
                if ($needed <= 0) {
1✔
NEW
94
                        return;
×
95
                }
96

97
                if ($timeSinceLastAttempt < self::MIN_INTERVAL_BETWEEN_STARTS) {
1✔
98
                        return;
1✔
99
                }
100

NEW
101
                $this->appConfig->setValueInt('libresign', self::CONFIG_KEY, $now);
×
NEW
102
                $this->startWorkerProcess($needed);
×
103
        }
104

105
        /**
106
         * Start multiple signing workers in background.
107
         * Uses Nextcloud's native background job worker to handle both SignFileJob (envelope coordinator)
108
         * and SignSingleFileJob (individual file signing).
109
         * Number of workers configurable via app config 'parallel_workers' (default: 4).
110
         */
111
        private function startWorkerProcess(int $count): void {
NEW
112
                $occPath = \OC::$SERVERROOT . '/occ';
×
113
                // Resolve the PHP CLI binary via IBinaryFinder (avoid PHP_BINARY under FPM)
NEW
114
                $phpPath = $this->binaryFinder->findBinaryPath('php');
×
NEW
115
                if ($phpPath === false) {
×
NEW
116
                        $phpPath = 'php';
×
117
                }
118

119
                // Clamp count between 1 and 32
NEW
120
                $numWorkers = max(1, min($count, 32));
×
121

122
                // Start multiple workers in parallel
NEW
123
                for ($i = 0; $i < $numWorkers; $i++) {
×
124
                        // SECURITY: Specify LibreSign job classes explicitly to prevent processing other apps' jobs
125
                        // This ensures our workers only handle LibreSign signing tasks
NEW
126
                        $jobClasses = [
×
NEW
127
                                SignFileJob::class,
×
NEW
128
                                SignSingleFileJob::class,
×
NEW
129
                        ];
×
NEW
130
                        $jobClassesArg = implode(' ', array_map('escapeshellarg', $jobClasses));
×
131

NEW
132
                        $cmd = sprintf(
×
NEW
133
                                '%s %s background-job:worker %s --stop_after=1h >> /dev/null 2>&1 &',
×
NEW
134
                                escapeshellarg($phpPath),
×
NEW
135
                                escapeshellarg($occPath),
×
NEW
136
                                $jobClassesArg
×
NEW
137
                        );
×
NEW
138
                        shell_exec($cmd);
×
139
                }
140
        }
141

142
        private function getDesiredWorkerCount(): int {
143
                $numWorkers = $this->appConfig->getValueInt(Application::APP_ID, 'parallel_workers', 4);
2✔
144
                return max(1, min($numWorkers, 32));
2✔
145
        }
146

147
        protected function countRunningWorkers(): int {
148
                try {
NEW
149
                        $occPath = \OC::$SERVERROOT . '/occ';
×
NEW
150
                        $cmd = sprintf(
×
NEW
151
                                "ps -eo args | grep -F %s | grep -F 'background-job:worker' | grep -E 'SignFileJob|SignSingleFileJob' | grep -v grep | wc -l",
×
NEW
152
                                escapeshellarg($occPath)
×
NEW
153
                        );
×
NEW
154
                        $output = shell_exec($cmd);
×
NEW
155
                        return max(0, (int)trim((string)$output));
×
NEW
156
                } catch (\Throwable $e) {
×
NEW
157
                        $this->logger->debug('Failed to count running workers', [
×
NEW
158
                                'error' => $e->getMessage(),
×
NEW
159
                        ]);
×
NEW
160
                        return 0;
×
161
                }
162
        }
163
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc