Completed
Push — master ( f9b24c...50df29 )
by Morris
48:33 queued 23:11
created

JobList::isAnyJobRunning()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 0
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Joas Schilling <[email protected]>
6
 * @author Jörn Friedrich Dreyer <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Noveen Sachdeva <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Robin McCorkell <[email protected]>
12
 *
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
namespace OC\BackgroundJob;
30
31
use OCP\AppFramework\QueryException;
32
use OCP\AppFramework\Utility\ITimeFactory;
33
use OCP\BackgroundJob\IJob;
34
use OCP\BackgroundJob\IJobList;
35
use OCP\AutoloadNotAllowedException;
36
use OCP\DB\QueryBuilder\IQueryBuilder;
37
use OCP\IConfig;
38
use OCP\IDBConnection;
39
40
class JobList implements IJobList {
41
42
	/** @var IDBConnection */
43
	protected $connection;
44
45
	/**@var IConfig */
46
	protected $config;
47
48
	/**@var ITimeFactory */
49
	protected $timeFactory;
50
51
	/** @var int - 12 hours * 3600 seconds*/
52
	private $jobTimeOut = 43200;
53
54
	/**
55
	 * @param IDBConnection $connection
56
	 * @param IConfig $config
57
	 * @param ITimeFactory $timeFactory
58
	 */
59
	public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory) {
60
		$this->connection = $connection;
61
		$this->config = $config;
62
		$this->timeFactory = $timeFactory;
63
	}
64
65
	/**
66
	 * @param IJob|string $job
67
	 * @param mixed $argument
68
	 */
69
	public function add($job, $argument = null) {
70
		if (!$this->has($job, $argument)) {
71
			if ($job instanceof IJob) {
72
				$class = get_class($job);
73
			} else {
74
				$class = $job;
75
			}
76
77
			$argument = json_encode($argument);
78
			if (strlen($argument) > 4000) {
79
				throw new \InvalidArgumentException('Background job arguments can\'t exceed 4000 characters (json encoded)');
80
			}
81
82
			$query = $this->connection->getQueryBuilder();
83
			$query->insert('jobs')
84
				->values([
85
					'class' => $query->createNamedParameter($class),
86
					'argument' => $query->createNamedParameter($argument),
87
					'last_run' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT),
88
					'last_checked' => $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT),
89
				]);
90
			$query->execute();
91
		}
92
	}
93
94
	/**
95
	 * @param IJob|string $job
96
	 * @param mixed $argument
97
	 */
98
	public function remove($job, $argument = null) {
99
		if ($job instanceof IJob) {
100
			$class = get_class($job);
101
		} else {
102
			$class = $job;
103
		}
104
105
		$query = $this->connection->getQueryBuilder();
106
		$query->delete('jobs')
107
			->where($query->expr()->eq('class', $query->createNamedParameter($class)));
108
		if (!is_null($argument)) {
109
			$argument = json_encode($argument);
110
			$query->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument)));
111
		}
112
		$query->execute();
113
	}
114
115
	/**
116
	 * @param int $id
117
	 */
118
	protected function removeById($id) {
119
		$query = $this->connection->getQueryBuilder();
120
		$query->delete('jobs')
121
			->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
122
		$query->execute();
123
	}
124
125
	/**
126
	 * check if a job is in the list
127
	 *
128
	 * @param IJob|string $job
129
	 * @param mixed $argument
130
	 * @return bool
131
	 */
132
	public function has($job, $argument) {
133
		if ($job instanceof IJob) {
134
			$class = get_class($job);
135
		} else {
136
			$class = $job;
137
		}
138
		$argument = json_encode($argument);
139
140
		$query = $this->connection->getQueryBuilder();
141
		$query->select('id')
142
			->from('jobs')
143
			->where($query->expr()->eq('class', $query->createNamedParameter($class)))
144
			->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument)))
145
			->setMaxResults(1);
146
147
		$result = $query->execute();
148
		$row = $result->fetch();
149
		$result->closeCursor();
150
151
		return (bool) $row;
152
	}
153
154
	/**
155
	 * get all jobs in the list
156
	 *
157
	 * @return IJob[]
158
	 * @deprecated 9.0.0 - This method is dangerous since it can cause load and
159
	 * memory problems when creating too many instances.
160
	 */
161
	public function getAll() {
162
		$query = $this->connection->getQueryBuilder();
163
		$query->select('*')
164
			->from('jobs');
165
		$result = $query->execute();
166
167
		$jobs = [];
168
		while ($row = $result->fetch()) {
169
			$job = $this->buildJob($row);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $job is correct as $this->buildJob($row) (which targets OC\BackgroundJob\JobList::buildJob()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
170
			if ($job) {
171
				$jobs[] = $job;
172
			}
173
		}
174
		$result->closeCursor();
175
176
		return $jobs;
177
	}
178
179
	/**
180
	 * get the next job in the list
181
	 *
182
	 * @return IJob|null
183
	 */
184
	public function getNext() {
185
		$query = $this->connection->getQueryBuilder();
186
		$query->select('*')
187
			->from('jobs')
188
			->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - $this->jobTimeOut, IQueryBuilder::PARAM_INT)))
189
			->orderBy('last_checked', 'ASC')
190
			->setMaxResults(1);
191
192
		$update = $this->connection->getQueryBuilder();
193
		$update->update('jobs')
194
			->set('reserved_at', $update->createNamedParameter($this->timeFactory->getTime()))
195
			->set('last_checked', $update->createNamedParameter($this->timeFactory->getTime()))
196
			->where($update->expr()->eq('id', $update->createParameter('jobid')))
197
			->andWhere($update->expr()->eq('reserved_at', $update->createParameter('reserved_at')))
198
			->andWhere($update->expr()->eq('last_checked', $update->createParameter('last_checked')));
199
200
		$result = $query->execute();
201
		$row = $result->fetch();
202
		$result->closeCursor();
203
204
		if ($row) {
205
			$update->setParameter('jobid', $row['id']);
206
			$update->setParameter('reserved_at', $row['reserved_at']);
207
			$update->setParameter('last_checked', $row['last_checked']);
208
			$count = $update->execute();
209
210
			if ($count === 0) {
211
				// Background job already executed elsewhere, try again.
212
				return $this->getNext();
213
			}
214
			$job = $this->buildJob($row);
215
216
			if ($job === null) {
217
				// Background job from disabled app, try again.
218
				return $this->getNext();
219
			}
220
221
			return $job;
222
		} else {
223
			return null;
224
		}
225
	}
226
227
	/**
228
	 * @param int $id
229
	 * @return IJob|null
230
	 */
231 View Code Duplication
	public function getById($id) {
232
		$query = $this->connection->getQueryBuilder();
233
		$query->select('*')
234
			->from('jobs')
235
			->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
236
		$result = $query->execute();
237
		$row = $result->fetch();
238
		$result->closeCursor();
239
240
		if ($row) {
241
			return $this->buildJob($row);
242
		} else {
243
			return null;
244
		}
245
	}
246
247
	/**
248
	 * get the job object from a row in the db
249
	 *
250
	 * @param array $row
251
	 * @return IJob|null
252
	 */
253
	private function buildJob($row) {
254
		try {
255
			try {
256
				// Try to load the job as a service
257
				/** @var IJob $job */
258
				$job = \OC::$server->query($row['class']);
259
			} catch (QueryException $e) {
260
				if (class_exists($row['class'])) {
261
					$class = $row['class'];
262
					$job = new $class();
263
				} else {
264
					// job from disabled app or old version of an app, no need to do anything
265
					return null;
266
				}
267
			}
268
269
			$job->setId($row['id']);
270
			$job->setLastRun($row['last_run']);
271
			$job->setArgument(json_decode($row['argument'], true));
272
			return $job;
273
		} catch (AutoloadNotAllowedException $e) {
274
			// job is from a disabled app, ignore
275
			return null;
276
		}
277
	}
278
279
	/**
280
	 * set the job that was last ran
281
	 *
282
	 * @param IJob $job
283
	 */
284
	public function setLastJob(IJob $job) {
285
		$this->unlockJob($job);
286
		$this->config->setAppValue('backgroundjob', 'lastjob', $job->getId());
287
	}
288
289
	/**
290
	 * Remove the reservation for a job
291
	 *
292
	 * @param IJob $job
293
	 * @suppress SqlInjectionChecker
294
	 */
295 View Code Duplication
	public function unlockJob(IJob $job) {
296
		$query = $this->connection->getQueryBuilder();
297
		$query->update('jobs')
298
			->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT))
299
			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
300
		$query->execute();
301
	}
302
303
	/**
304
	 * get the id of the last ran job
305
	 *
306
	 * @return int
307
	 * @deprecated 9.1.0 - The functionality behind the value is deprecated, it
308
	 *    only tells you which job finished last, but since we now allow multiple
309
	 *    executors to run in parallel, it's not used to calculate the next job.
310
	 */
311
	public function getLastJob() {
312
		return (int) $this->config->getAppValue('backgroundjob', 'lastjob', 0);
313
	}
314
315
	/**
316
	 * set the lastRun of $job to now
317
	 *
318
	 * @param IJob $job
319
	 */
320
	public function setLastRun(IJob $job) {
321
		$query = $this->connection->getQueryBuilder();
322
		$query->update('jobs')
323
			->set('last_run', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
324
			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
325
		$query->execute();
326
	}
327
328
	/**
329
	 * @param IJob $job
330
	 * @param $timeTaken
331
	 */
332
	public function setExecutionTime(IJob $job, $timeTaken) {
333
		$query = $this->connection->getQueryBuilder();
334
		$query->update('jobs')
335
			->set('execution_duration', $query->createNamedParameter($timeTaken, IQueryBuilder::PARAM_INT))
336
			->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
337
		$query->execute();
338
	}
339
340
	/**
341
	 * checks if a job is still running (reserved_at time is smaller than 12 hours ago)
342
	 *
343
	 * Background information:
344
	 *
345
	 * The 12 hours is the same timeout that is also used to re-schedule an non-terminated
346
	 * job (see getNext()). The idea here is to give a job enough time to run very
347
	 * long but still be able to recognize that it maybe crashed and re-schedule it
348
	 * after the timeout. It's more likely to be crashed at that time than it ran
349
	 * that long.
350
	 *
351
	 * In theory it could lead to an nearly endless loop (as in - at most 12 hours).
352
	 * The cron command will not start new jobs when maintenance mode is active and
353
	 * this method is only executed in maintenance mode (see where it is called in
354
	 * the upgrader class. So this means in the worst case we wait 12 hours when a
355
	 * job has crashed. On the other hand: then the instance should be fixed anyways.
356
	 *
357
	 * @return bool
358
	 */
359
	public function isAnyJobRunning(): bool {
360
		$query = $this->connection->getQueryBuilder();
361
		$query->select('*')
362
			->from('jobs')
363
			->where($query->expr()->gt('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - $this->jobTimeOut, IQueryBuilder::PARAM_INT)))
364
			->setMaxResults(1);
365
		$result = $query->execute();
366
		$row = $result->fetch();
367
		$result->closeCursor();
368
369
		if ($row) {
370
			return true;
371
		}
372
		return false;
373
	}
374
}
375