Passed
Push — master ( 9ace8e...7a7578 )
by Blizzz
15:49 queued 14s
created
lib/private/BackgroundJob/JobList.php 1 patch
Indentation   +367 added lines, -367 removed lines patch added patch discarded remove patch
@@ -42,371 +42,371 @@
 block discarded – undo
42 42
 use Psr\Log\LoggerInterface;
43 43
 
44 44
 class JobList implements IJobList {
45
-	protected IDBConnection $connection;
46
-	protected IConfig $config;
47
-	protected ITimeFactory $timeFactory;
48
-	protected LoggerInterface $logger;
49
-
50
-	public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory, LoggerInterface $logger) {
51
-		$this->connection = $connection;
52
-		$this->config = $config;
53
-		$this->timeFactory = $timeFactory;
54
-		$this->logger = $logger;
55
-	}
56
-
57
-	/**
58
-	 * @param IJob|class-string<IJob> $job
59
-	 * @param mixed $argument
60
-	 */
61
-	public function add($job, $argument = null): void {
62
-		if ($job instanceof IJob) {
63
-			$class = get_class($job);
64
-		} else {
65
-			$class = $job;
66
-		}
67
-
68
-		$argumentJson = json_encode($argument);
69
-		if (strlen($argumentJson) > 4000) {
70
-			throw new \InvalidArgumentException('Background job arguments can\'t exceed 4000 characters (json encoded)');
71
-		}
72
-
73
-		$query = $this->connection->getQueryBuilder();
74
-		if (!$this->has($job, $argument)) {
75
-			$query->insert('jobs')
76
-				->values([
77
-					'class' => $query->createNamedParameter($class),
78
-					'argument' => $query->createNamedParameter($argumentJson),
79
-					'argument_hash' => $query->createNamedParameter(md5($argumentJson)),
80
-					'last_run' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT),
81
-					'last_checked' => $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT),
82
-				]);
83
-		} else {
84
-			$query->update('jobs')
85
-				->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT))
86
-				->set('last_checked', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT))
87
-				->where($query->expr()->eq('class', $query->createNamedParameter($class)))
88
-				->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argumentJson))));
89
-		}
90
-		$query->executeStatement();
91
-	}
92
-
93
-	/**
94
-	 * @param IJob|string $job
95
-	 * @param mixed $argument
96
-	 */
97
-	public function remove($job, $argument = null): void {
98
-		if ($job instanceof IJob) {
99
-			$class = get_class($job);
100
-		} else {
101
-			$class = $job;
102
-		}
103
-
104
-		$query = $this->connection->getQueryBuilder();
105
-		$query->delete('jobs')
106
-			->where($query->expr()->eq('class', $query->createNamedParameter($class)));
107
-		if (!is_null($argument)) {
108
-			$argumentJson = json_encode($argument);
109
-			$query->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argumentJson))));
110
-		}
111
-
112
-		// Add galera safe delete chunking if using mysql
113
-		// Stops us hitting wsrep_max_ws_rows when large row counts are deleted
114
-		if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
115
-			// Then use chunked delete
116
-			$max = IQueryBuilder::MAX_ROW_DELETION;
117
-
118
-			$query->setMaxResults($max);
119
-
120
-			do {
121
-				$deleted = $query->execute();
122
-			} while ($deleted === $max);
123
-		} else {
124
-			// Dont use chunked delete - let the DB handle the large row count natively
125
-			$query->execute();
126
-		}
127
-	}
128
-
129
-	protected function removeById(int $id): void {
130
-		$query = $this->connection->getQueryBuilder();
131
-		$query->delete('jobs')
132
-			->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
133
-		$query->executeStatement();
134
-	}
135
-
136
-	/**
137
-	 * check if a job is in the list
138
-	 *
139
-	 * @param IJob|class-string<IJob> $job
140
-	 * @param mixed $argument
141
-	 */
142
-	public function has($job, $argument): bool {
143
-		if ($job instanceof IJob) {
144
-			$class = get_class($job);
145
-		} else {
146
-			$class = $job;
147
-		}
148
-		$argument = json_encode($argument);
149
-
150
-		$query = $this->connection->getQueryBuilder();
151
-		$query->select('id')
152
-			->from('jobs')
153
-			->where($query->expr()->eq('class', $query->createNamedParameter($class)))
154
-			->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argument))))
155
-			->setMaxResults(1);
156
-
157
-		$result = $query->executeQuery();
158
-		$row = $result->fetch();
159
-		$result->closeCursor();
160
-
161
-		return (bool) $row;
162
-	}
163
-
164
-	public function getJobs($job, ?int $limit, int $offset): array {
165
-		$iterable = $this->getJobsIterator($job, $limit, $offset);
166
-		if (is_array($iterable)) {
167
-			return $iterable;
168
-		} else {
169
-			return iterator_to_array($iterable);
170
-		}
171
-	}
172
-
173
-	/**
174
-	 * @param IJob|class-string<IJob>|null $job
175
-	 * @return iterable<IJob> Avoid to store these objects as they may share a Singleton instance. You should instead use these IJobs instances while looping on the iterable.
176
-	 */
177
-	public function getJobsIterator($job, ?int $limit, int $offset): iterable {
178
-		$query = $this->connection->getQueryBuilder();
179
-		$query->select('*')
180
-			->from('jobs')
181
-			->setMaxResults($limit)
182
-			->setFirstResult($offset);
183
-
184
-		if ($job !== null) {
185
-			if ($job instanceof IJob) {
186
-				$class = get_class($job);
187
-			} else {
188
-				$class = $job;
189
-			}
190
-			$query->where($query->expr()->eq('class', $query->createNamedParameter($class)));
191
-		}
192
-
193
-		$result = $query->executeQuery();
194
-
195
-		while ($row = $result->fetch()) {
196
-			$job = $this->buildJob($row);
197
-			if ($job) {
198
-				yield $job;
199
-			}
200
-		}
201
-		$result->closeCursor();
202
-	}
203
-
204
-	/**
205
-	 * Get the next job in the list
206
-	 * @return ?IJob the next job to run. Beware that this object may be a singleton and may be modified by the next call to buildJob.
207
-	 */
208
-	public function getNext(bool $onlyTimeSensitive = false): ?IJob {
209
-		$query = $this->connection->getQueryBuilder();
210
-		$query->select('*')
211
-			->from('jobs')
212
-			->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - 12 * 3600, IQueryBuilder::PARAM_INT)))
213
-			->andWhere($query->expr()->lte('last_checked', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)))
214
-			->orderBy('last_checked', 'ASC')
215
-			->setMaxResults(1);
216
-
217
-		if ($onlyTimeSensitive) {
218
-			$query->andWhere($query->expr()->eq('time_sensitive', $query->createNamedParameter(IJob::TIME_SENSITIVE, IQueryBuilder::PARAM_INT)));
219
-		}
220
-
221
-		$update = $this->connection->getQueryBuilder();
222
-		$update->update('jobs')
223
-			->set('reserved_at', $update->createNamedParameter($this->timeFactory->getTime()))
224
-			->set('last_checked', $update->createNamedParameter($this->timeFactory->getTime()))
225
-			->where($update->expr()->eq('id', $update->createParameter('jobid')))
226
-			->andWhere($update->expr()->eq('reserved_at', $update->createParameter('reserved_at')))
227
-			->andWhere($update->expr()->eq('last_checked', $update->createParameter('last_checked')));
228
-
229
-		$result = $query->executeQuery();
230
-		$row = $result->fetch();
231
-		$result->closeCursor();
232
-
233
-		if ($row) {
234
-			$update->setParameter('jobid', $row['id']);
235
-			$update->setParameter('reserved_at', $row['reserved_at']);
236
-			$update->setParameter('last_checked', $row['last_checked']);
237
-			$count = $update->executeStatement();
238
-
239
-			if ($count === 0) {
240
-				// Background job already executed elsewhere, try again.
241
-				return $this->getNext($onlyTimeSensitive);
242
-			}
243
-			$job = $this->buildJob($row);
244
-
245
-			if ($job === null) {
246
-				// set the last_checked to 12h in the future to not check failing jobs all over again
247
-				$reset = $this->connection->getQueryBuilder();
248
-				$reset->update('jobs')
249
-					->set('reserved_at', $reset->expr()->literal(0, IQueryBuilder::PARAM_INT))
250
-					->set('last_checked', $reset->createNamedParameter($this->timeFactory->getTime() + 12 * 3600, IQueryBuilder::PARAM_INT))
251
-					->where($reset->expr()->eq('id', $reset->createNamedParameter($row['id'], IQueryBuilder::PARAM_INT)));
252
-				$reset->executeStatement();
253
-
254
-				// Background job from disabled app, try again.
255
-				return $this->getNext($onlyTimeSensitive);
256
-			}
257
-
258
-			return $job;
259
-		} else {
260
-			return null;
261
-		}
262
-	}
263
-
264
-	/**
265
-	 * @return ?IJob The job matching the id. Beware that this object may be a singleton and may be modified by the next call to buildJob.
266
-	 */
267
-	public function getById(int $id): ?IJob {
268
-		$row = $this->getDetailsById($id);
269
-
270
-		if ($row) {
271
-			return $this->buildJob($row);
272
-		}
273
-
274
-		return null;
275
-	}
276
-
277
-	public function getDetailsById(int $id): ?array {
278
-		$query = $this->connection->getQueryBuilder();
279
-		$query->select('*')
280
-			->from('jobs')
281
-			->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
282
-		$result = $query->executeQuery();
283
-		$row = $result->fetch();
284
-		$result->closeCursor();
285
-
286
-		if ($row) {
287
-			return $row;
288
-		}
289
-
290
-		return null;
291
-	}
292
-
293
-	/**
294
-	 * get the job object from a row in the db
295
-	 *
296
-	 * @param array{class:class-string<IJob>, id:mixed, last_run:mixed, argument:string} $row
297
-	 * @return ?IJob the next job to run. Beware that this object may be a singleton and may be modified by the next call to buildJob.
298
-	 */
299
-	private function buildJob(array $row): ?IJob {
300
-		try {
301
-			try {
302
-				// Try to load the job as a service
303
-				/** @var IJob $job */
304
-				$job = \OCP\Server::get($row['class']);
305
-			} catch (QueryException $e) {
306
-				if (class_exists($row['class'])) {
307
-					$class = $row['class'];
308
-					$job = new $class();
309
-				} else {
310
-					// Remove job from disabled app or old version of an app
311
-					$this->removeById($row['id']);
312
-					return null;
313
-				}
314
-			}
315
-
316
-			if (!($job instanceof IJob)) {
317
-				// This most likely means an invalid job was enqueued. We can ignore it.
318
-				return null;
319
-			}
320
-			$job->setId((int) $row['id']);
321
-			$job->setLastRun((int) $row['last_run']);
322
-			$job->setArgument(json_decode($row['argument'], true));
323
-			return $job;
324
-		} catch (AutoloadNotAllowedException $e) {
325
-			// job is from a disabled app, ignore
326
-			return null;
327
-		}
328
-	}
329
-
330
-	/**
331
-	 * set the job that was last ran
332
-	 */
333
-	public function setLastJob(IJob $job): void {
334
-		$this->unlockJob($job);
335
-		$this->config->setAppValue('backgroundjob', 'lastjob', (string)$job->getId());
336
-	}
337
-
338
-	/**
339
-	 * Remove the reservation for a job
340
-	 */
341
-	public function unlockJob(IJob $job): void {
342
-		$query = $this->connection->getQueryBuilder();
343
-		$query->update('jobs')
344
-			->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT))
345
-			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
346
-		$query->executeStatement();
347
-	}
348
-
349
-	/**
350
-	 * set the lastRun of $job to now
351
-	 */
352
-	public function setLastRun(IJob $job): void {
353
-		$query = $this->connection->getQueryBuilder();
354
-		$query->update('jobs')
355
-			->set('last_run', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
356
-			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
357
-
358
-		if ($job instanceof \OCP\BackgroundJob\TimedJob
359
-			&& !$job->isTimeSensitive()) {
360
-			$query->set('time_sensitive', $query->createNamedParameter(IJob::TIME_INSENSITIVE));
361
-		}
362
-
363
-		$query->executeStatement();
364
-	}
365
-
366
-	/**
367
-	 * @param int $timeTaken
368
-	 */
369
-	public function setExecutionTime(IJob $job, $timeTaken): void {
370
-		$query = $this->connection->getQueryBuilder();
371
-		$query->update('jobs')
372
-			->set('execution_duration', $query->createNamedParameter($timeTaken, IQueryBuilder::PARAM_INT))
373
-			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
374
-		$query->executeStatement();
375
-	}
376
-
377
-	/**
378
-	 * Reset the $job so it executes on the next trigger
379
-	 *
380
-	 * @since 23.0.0
381
-	 */
382
-	public function resetBackgroundJob(IJob $job): void {
383
-		$query = $this->connection->getQueryBuilder();
384
-		$query->update('jobs')
385
-			->set('last_run', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
386
-			->set('reserved_at', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
387
-			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId()), IQueryBuilder::PARAM_INT));
388
-		$query->executeStatement();
389
-	}
390
-
391
-	public function hasReservedJob(?string $className = null): bool {
392
-		$query = $this->connection->getQueryBuilder();
393
-		$query->select('*')
394
-			->from('jobs')
395
-			->where($query->expr()->neq('reserved_at', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
396
-			->setMaxResults(1);
397
-
398
-		if ($className !== null) {
399
-			$query->andWhere($query->expr()->eq('class', $query->createNamedParameter($className)));
400
-		}
401
-
402
-		try {
403
-			$result = $query->executeQuery();
404
-			$hasReservedJobs = $result->fetch() !== false;
405
-			$result->closeCursor();
406
-			return $hasReservedJobs;
407
-		} catch (Exception $e) {
408
-			$this->logger->debug('Querying reserved jobs failed', ['exception' => $e]);
409
-			return false;
410
-		}
411
-	}
45
+    protected IDBConnection $connection;
46
+    protected IConfig $config;
47
+    protected ITimeFactory $timeFactory;
48
+    protected LoggerInterface $logger;
49
+
50
+    public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory, LoggerInterface $logger) {
51
+        $this->connection = $connection;
52
+        $this->config = $config;
53
+        $this->timeFactory = $timeFactory;
54
+        $this->logger = $logger;
55
+    }
56
+
57
+    /**
58
+     * @param IJob|class-string<IJob> $job
59
+     * @param mixed $argument
60
+     */
61
+    public function add($job, $argument = null): void {
62
+        if ($job instanceof IJob) {
63
+            $class = get_class($job);
64
+        } else {
65
+            $class = $job;
66
+        }
67
+
68
+        $argumentJson = json_encode($argument);
69
+        if (strlen($argumentJson) > 4000) {
70
+            throw new \InvalidArgumentException('Background job arguments can\'t exceed 4000 characters (json encoded)');
71
+        }
72
+
73
+        $query = $this->connection->getQueryBuilder();
74
+        if (!$this->has($job, $argument)) {
75
+            $query->insert('jobs')
76
+                ->values([
77
+                    'class' => $query->createNamedParameter($class),
78
+                    'argument' => $query->createNamedParameter($argumentJson),
79
+                    'argument_hash' => $query->createNamedParameter(md5($argumentJson)),
80
+                    'last_run' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT),
81
+                    'last_checked' => $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT),
82
+                ]);
83
+        } else {
84
+            $query->update('jobs')
85
+                ->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT))
86
+                ->set('last_checked', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT))
87
+                ->where($query->expr()->eq('class', $query->createNamedParameter($class)))
88
+                ->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argumentJson))));
89
+        }
90
+        $query->executeStatement();
91
+    }
92
+
93
+    /**
94
+     * @param IJob|string $job
95
+     * @param mixed $argument
96
+     */
97
+    public function remove($job, $argument = null): void {
98
+        if ($job instanceof IJob) {
99
+            $class = get_class($job);
100
+        } else {
101
+            $class = $job;
102
+        }
103
+
104
+        $query = $this->connection->getQueryBuilder();
105
+        $query->delete('jobs')
106
+            ->where($query->expr()->eq('class', $query->createNamedParameter($class)));
107
+        if (!is_null($argument)) {
108
+            $argumentJson = json_encode($argument);
109
+            $query->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argumentJson))));
110
+        }
111
+
112
+        // Add galera safe delete chunking if using mysql
113
+        // Stops us hitting wsrep_max_ws_rows when large row counts are deleted
114
+        if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
115
+            // Then use chunked delete
116
+            $max = IQueryBuilder::MAX_ROW_DELETION;
117
+
118
+            $query->setMaxResults($max);
119
+
120
+            do {
121
+                $deleted = $query->execute();
122
+            } while ($deleted === $max);
123
+        } else {
124
+            // Dont use chunked delete - let the DB handle the large row count natively
125
+            $query->execute();
126
+        }
127
+    }
128
+
129
+    protected function removeById(int $id): void {
130
+        $query = $this->connection->getQueryBuilder();
131
+        $query->delete('jobs')
132
+            ->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
133
+        $query->executeStatement();
134
+    }
135
+
136
+    /**
137
+     * check if a job is in the list
138
+     *
139
+     * @param IJob|class-string<IJob> $job
140
+     * @param mixed $argument
141
+     */
142
+    public function has($job, $argument): bool {
143
+        if ($job instanceof IJob) {
144
+            $class = get_class($job);
145
+        } else {
146
+            $class = $job;
147
+        }
148
+        $argument = json_encode($argument);
149
+
150
+        $query = $this->connection->getQueryBuilder();
151
+        $query->select('id')
152
+            ->from('jobs')
153
+            ->where($query->expr()->eq('class', $query->createNamedParameter($class)))
154
+            ->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(md5($argument))))
155
+            ->setMaxResults(1);
156
+
157
+        $result = $query->executeQuery();
158
+        $row = $result->fetch();
159
+        $result->closeCursor();
160
+
161
+        return (bool) $row;
162
+    }
163
+
164
+    public function getJobs($job, ?int $limit, int $offset): array {
165
+        $iterable = $this->getJobsIterator($job, $limit, $offset);
166
+        if (is_array($iterable)) {
167
+            return $iterable;
168
+        } else {
169
+            return iterator_to_array($iterable);
170
+        }
171
+    }
172
+
173
+    /**
174
+     * @param IJob|class-string<IJob>|null $job
175
+     * @return iterable<IJob> Avoid to store these objects as they may share a Singleton instance. You should instead use these IJobs instances while looping on the iterable.
176
+     */
177
+    public function getJobsIterator($job, ?int $limit, int $offset): iterable {
178
+        $query = $this->connection->getQueryBuilder();
179
+        $query->select('*')
180
+            ->from('jobs')
181
+            ->setMaxResults($limit)
182
+            ->setFirstResult($offset);
183
+
184
+        if ($job !== null) {
185
+            if ($job instanceof IJob) {
186
+                $class = get_class($job);
187
+            } else {
188
+                $class = $job;
189
+            }
190
+            $query->where($query->expr()->eq('class', $query->createNamedParameter($class)));
191
+        }
192
+
193
+        $result = $query->executeQuery();
194
+
195
+        while ($row = $result->fetch()) {
196
+            $job = $this->buildJob($row);
197
+            if ($job) {
198
+                yield $job;
199
+            }
200
+        }
201
+        $result->closeCursor();
202
+    }
203
+
204
+    /**
205
+     * Get the next job in the list
206
+     * @return ?IJob the next job to run. Beware that this object may be a singleton and may be modified by the next call to buildJob.
207
+     */
208
+    public function getNext(bool $onlyTimeSensitive = false): ?IJob {
209
+        $query = $this->connection->getQueryBuilder();
210
+        $query->select('*')
211
+            ->from('jobs')
212
+            ->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - 12 * 3600, IQueryBuilder::PARAM_INT)))
213
+            ->andWhere($query->expr()->lte('last_checked', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT)))
214
+            ->orderBy('last_checked', 'ASC')
215
+            ->setMaxResults(1);
216
+
217
+        if ($onlyTimeSensitive) {
218
+            $query->andWhere($query->expr()->eq('time_sensitive', $query->createNamedParameter(IJob::TIME_SENSITIVE, IQueryBuilder::PARAM_INT)));
219
+        }
220
+
221
+        $update = $this->connection->getQueryBuilder();
222
+        $update->update('jobs')
223
+            ->set('reserved_at', $update->createNamedParameter($this->timeFactory->getTime()))
224
+            ->set('last_checked', $update->createNamedParameter($this->timeFactory->getTime()))
225
+            ->where($update->expr()->eq('id', $update->createParameter('jobid')))
226
+            ->andWhere($update->expr()->eq('reserved_at', $update->createParameter('reserved_at')))
227
+            ->andWhere($update->expr()->eq('last_checked', $update->createParameter('last_checked')));
228
+
229
+        $result = $query->executeQuery();
230
+        $row = $result->fetch();
231
+        $result->closeCursor();
232
+
233
+        if ($row) {
234
+            $update->setParameter('jobid', $row['id']);
235
+            $update->setParameter('reserved_at', $row['reserved_at']);
236
+            $update->setParameter('last_checked', $row['last_checked']);
237
+            $count = $update->executeStatement();
238
+
239
+            if ($count === 0) {
240
+                // Background job already executed elsewhere, try again.
241
+                return $this->getNext($onlyTimeSensitive);
242
+            }
243
+            $job = $this->buildJob($row);
244
+
245
+            if ($job === null) {
246
+                // set the last_checked to 12h in the future to not check failing jobs all over again
247
+                $reset = $this->connection->getQueryBuilder();
248
+                $reset->update('jobs')
249
+                    ->set('reserved_at', $reset->expr()->literal(0, IQueryBuilder::PARAM_INT))
250
+                    ->set('last_checked', $reset->createNamedParameter($this->timeFactory->getTime() + 12 * 3600, IQueryBuilder::PARAM_INT))
251
+                    ->where($reset->expr()->eq('id', $reset->createNamedParameter($row['id'], IQueryBuilder::PARAM_INT)));
252
+                $reset->executeStatement();
253
+
254
+                // Background job from disabled app, try again.
255
+                return $this->getNext($onlyTimeSensitive);
256
+            }
257
+
258
+            return $job;
259
+        } else {
260
+            return null;
261
+        }
262
+    }
263
+
264
+    /**
265
+     * @return ?IJob The job matching the id. Beware that this object may be a singleton and may be modified by the next call to buildJob.
266
+     */
267
+    public function getById(int $id): ?IJob {
268
+        $row = $this->getDetailsById($id);
269
+
270
+        if ($row) {
271
+            return $this->buildJob($row);
272
+        }
273
+
274
+        return null;
275
+    }
276
+
277
+    public function getDetailsById(int $id): ?array {
278
+        $query = $this->connection->getQueryBuilder();
279
+        $query->select('*')
280
+            ->from('jobs')
281
+            ->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
282
+        $result = $query->executeQuery();
283
+        $row = $result->fetch();
284
+        $result->closeCursor();
285
+
286
+        if ($row) {
287
+            return $row;
288
+        }
289
+
290
+        return null;
291
+    }
292
+
293
+    /**
294
+     * get the job object from a row in the db
295
+     *
296
+     * @param array{class:class-string<IJob>, id:mixed, last_run:mixed, argument:string} $row
297
+     * @return ?IJob the next job to run. Beware that this object may be a singleton and may be modified by the next call to buildJob.
298
+     */
299
+    private function buildJob(array $row): ?IJob {
300
+        try {
301
+            try {
302
+                // Try to load the job as a service
303
+                /** @var IJob $job */
304
+                $job = \OCP\Server::get($row['class']);
305
+            } catch (QueryException $e) {
306
+                if (class_exists($row['class'])) {
307
+                    $class = $row['class'];
308
+                    $job = new $class();
309
+                } else {
310
+                    // Remove job from disabled app or old version of an app
311
+                    $this->removeById($row['id']);
312
+                    return null;
313
+                }
314
+            }
315
+
316
+            if (!($job instanceof IJob)) {
317
+                // This most likely means an invalid job was enqueued. We can ignore it.
318
+                return null;
319
+            }
320
+            $job->setId((int) $row['id']);
321
+            $job->setLastRun((int) $row['last_run']);
322
+            $job->setArgument(json_decode($row['argument'], true));
323
+            return $job;
324
+        } catch (AutoloadNotAllowedException $e) {
325
+            // job is from a disabled app, ignore
326
+            return null;
327
+        }
328
+    }
329
+
330
+    /**
331
+     * set the job that was last ran
332
+     */
333
+    public function setLastJob(IJob $job): void {
334
+        $this->unlockJob($job);
335
+        $this->config->setAppValue('backgroundjob', 'lastjob', (string)$job->getId());
336
+    }
337
+
338
+    /**
339
+     * Remove the reservation for a job
340
+     */
341
+    public function unlockJob(IJob $job): void {
342
+        $query = $this->connection->getQueryBuilder();
343
+        $query->update('jobs')
344
+            ->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT))
345
+            ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
346
+        $query->executeStatement();
347
+    }
348
+
349
+    /**
350
+     * set the lastRun of $job to now
351
+     */
352
+    public function setLastRun(IJob $job): void {
353
+        $query = $this->connection->getQueryBuilder();
354
+        $query->update('jobs')
355
+            ->set('last_run', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
356
+            ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
357
+
358
+        if ($job instanceof \OCP\BackgroundJob\TimedJob
359
+            && !$job->isTimeSensitive()) {
360
+            $query->set('time_sensitive', $query->createNamedParameter(IJob::TIME_INSENSITIVE));
361
+        }
362
+
363
+        $query->executeStatement();
364
+    }
365
+
366
+    /**
367
+     * @param int $timeTaken
368
+     */
369
+    public function setExecutionTime(IJob $job, $timeTaken): void {
370
+        $query = $this->connection->getQueryBuilder();
371
+        $query->update('jobs')
372
+            ->set('execution_duration', $query->createNamedParameter($timeTaken, IQueryBuilder::PARAM_INT))
373
+            ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
374
+        $query->executeStatement();
375
+    }
376
+
377
+    /**
378
+     * Reset the $job so it executes on the next trigger
379
+     *
380
+     * @since 23.0.0
381
+     */
382
+    public function resetBackgroundJob(IJob $job): void {
383
+        $query = $this->connection->getQueryBuilder();
384
+        $query->update('jobs')
385
+            ->set('last_run', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
386
+            ->set('reserved_at', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
387
+            ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId()), IQueryBuilder::PARAM_INT));
388
+        $query->executeStatement();
389
+    }
390
+
391
+    public function hasReservedJob(?string $className = null): bool {
392
+        $query = $this->connection->getQueryBuilder();
393
+        $query->select('*')
394
+            ->from('jobs')
395
+            ->where($query->expr()->neq('reserved_at', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
396
+            ->setMaxResults(1);
397
+
398
+        if ($className !== null) {
399
+            $query->andWhere($query->expr()->eq('class', $query->createNamedParameter($className)));
400
+        }
401
+
402
+        try {
403
+            $result = $query->executeQuery();
404
+            $hasReservedJobs = $result->fetch() !== false;
405
+            $result->closeCursor();
406
+            return $hasReservedJobs;
407
+        } catch (Exception $e) {
408
+            $this->logger->debug('Querying reserved jobs failed', ['exception' => $e]);
409
+            return false;
410
+        }
411
+    }
412 412
 }
Please login to merge, or discard this patch.