|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* @author Joas Schilling <[email protected]> |
|
4
|
|
|
* @author Morris Jobke <[email protected]> |
|
5
|
|
|
* @author Robin Appelman <[email protected]> |
|
6
|
|
|
* @author Robin McCorkell <[email protected]> |
|
7
|
|
|
* |
|
8
|
|
|
* @copyright Copyright (c) 2018, ownCloud GmbH |
|
9
|
|
|
* @license AGPL-3.0 |
|
10
|
|
|
* |
|
11
|
|
|
* This code is free software: you can redistribute it and/or modify |
|
12
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
|
13
|
|
|
* as published by the Free Software Foundation. |
|
14
|
|
|
* |
|
15
|
|
|
* This program is distributed in the hope that it will be useful, |
|
16
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
17
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
18
|
|
|
* GNU Affero General Public License for more details. |
|
19
|
|
|
* |
|
20
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
|
21
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
|
22
|
|
|
* |
|
23
|
|
|
*/ |
|
24
|
|
|
|
|
25
|
|
|
namespace OC\BackgroundJob; |
|
26
|
|
|
|
|
27
|
|
|
use OCP\AppFramework\QueryException; |
|
28
|
|
|
use OCP\AppFramework\Utility\ITimeFactory; |
|
29
|
|
|
use OCP\BackgroundJob\IJob; |
|
30
|
|
|
use OCP\BackgroundJob\IJobList; |
|
31
|
|
|
use OCP\AutoloadNotAllowedException; |
|
32
|
|
|
use OCP\DB\QueryBuilder\IQueryBuilder; |
|
33
|
|
|
use OCP\IConfig; |
|
34
|
|
|
use OCP\IDBConnection; |
|
35
|
|
|
|
|
36
|
|
|
class JobList implements IJobList { |
|
37
|
|
|
|
|
38
|
|
|
/** @var IDBConnection */ |
|
39
|
|
|
protected $connection; |
|
40
|
|
|
|
|
41
|
|
|
/**@var IConfig */ |
|
42
|
|
|
protected $config; |
|
43
|
|
|
|
|
44
|
|
|
/**@var ITimeFactory */ |
|
45
|
|
|
protected $timeFactory; |
|
46
|
|
|
|
|
47
|
|
|
/** |
|
48
|
|
|
* @param IDBConnection $connection |
|
49
|
|
|
* @param IConfig $config |
|
50
|
|
|
* @param ITimeFactory $timeFactory |
|
51
|
|
|
*/ |
|
52
|
|
|
public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory) { |
|
53
|
|
|
$this->connection = $connection; |
|
54
|
|
|
$this->config = $config; |
|
55
|
|
|
$this->timeFactory = $timeFactory; |
|
56
|
|
|
} |
|
57
|
|
|
|
|
58
|
|
|
/** |
|
59
|
|
|
* @param IJob|string $job |
|
60
|
|
|
* @param mixed $argument |
|
61
|
|
|
*/ |
|
62
|
|
|
public function add($job, $argument = null) { |
|
63
|
|
|
if (!$this->has($job, $argument)) { |
|
64
|
|
|
if ($job instanceof IJob) { |
|
65
|
|
|
$class = \get_class($job); |
|
66
|
|
|
} else { |
|
67
|
|
|
$class = $job; |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
$argument = \json_encode($argument); |
|
71
|
|
|
if (\strlen($argument) > 4000) { |
|
72
|
|
|
throw new \InvalidArgumentException('Background job arguments can\'t exceed 4000 characters (json encoded)'); |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
76
|
|
|
$query->insert('jobs') |
|
77
|
|
|
->values([ |
|
78
|
|
|
'class' => $query->createNamedParameter($class), |
|
79
|
|
|
'argument' => $query->createNamedParameter($argument), |
|
80
|
|
|
'last_run' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), |
|
81
|
|
|
'last_checked' => $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT), |
|
82
|
|
|
]); |
|
83
|
|
|
$query->execute(); |
|
84
|
|
|
} |
|
85
|
|
|
} |
|
86
|
|
|
|
|
87
|
|
|
/** |
|
88
|
|
|
* @param IJob|string $job |
|
89
|
|
|
* @param mixed $argument |
|
90
|
|
|
*/ |
|
91
|
|
|
public function remove($job, $argument = null) { |
|
92
|
|
|
if ($job instanceof IJob) { |
|
93
|
|
|
$class = \get_class($job); |
|
94
|
|
|
} else { |
|
95
|
|
|
$class = $job; |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
99
|
|
|
$query->delete('jobs') |
|
100
|
|
|
->where($query->expr()->eq('class', $query->createNamedParameter($class))); |
|
101
|
|
|
if ($argument !== null) { |
|
102
|
|
|
$argument = \json_encode($argument); |
|
103
|
|
|
$query->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument))); |
|
104
|
|
|
} |
|
105
|
|
|
$query->execute(); |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
/** |
|
109
|
|
|
* @param int $id |
|
110
|
|
|
*/ |
|
111
|
|
|
public function removeById($id) { |
|
112
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
113
|
|
|
$query->delete('jobs') |
|
114
|
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); |
|
115
|
|
|
$query->execute(); |
|
116
|
|
|
} |
|
117
|
|
|
|
|
118
|
|
|
/** |
|
119
|
|
|
* check if a job is in the list |
|
120
|
|
|
* |
|
121
|
|
|
* @param IJob|string $job |
|
122
|
|
|
* @param mixed $argument |
|
123
|
|
|
* @return bool |
|
124
|
|
|
*/ |
|
125
|
|
|
public function has($job, $argument) { |
|
126
|
|
|
if ($job instanceof IJob) { |
|
127
|
|
|
$class = \get_class($job); |
|
128
|
|
|
} else { |
|
129
|
|
|
$class = $job; |
|
130
|
|
|
} |
|
131
|
|
|
$argument = \json_encode($argument); |
|
132
|
|
|
|
|
133
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
134
|
|
|
$query->select('id') |
|
135
|
|
|
->from('jobs') |
|
136
|
|
|
->where($query->expr()->eq('class', $query->createNamedParameter($class))) |
|
137
|
|
|
->andWhere($query->expr()->eq('argument', $query->createNamedParameter($argument))) |
|
138
|
|
|
->setMaxResults(1); |
|
139
|
|
|
|
|
140
|
|
|
$result = $query->execute(); |
|
141
|
|
|
$row = $result->fetch(); |
|
142
|
|
|
$result->closeCursor(); |
|
143
|
|
|
|
|
144
|
|
|
return (bool) $row; |
|
145
|
|
|
} |
|
146
|
|
|
|
|
147
|
|
|
/** |
|
148
|
|
|
* get all jobs in the list |
|
149
|
|
|
* |
|
150
|
|
|
* @return IJob[] |
|
151
|
|
|
* @deprecated 9.0.0 - This method is dangerous since it can cause load and |
|
152
|
|
|
* memory problems when creating too many instances. |
|
153
|
|
|
*/ |
|
154
|
|
|
public function getAll() { |
|
155
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
156
|
|
|
$query->select('*') |
|
157
|
|
|
->from('jobs'); |
|
158
|
|
|
$result = $query->execute(); |
|
159
|
|
|
|
|
160
|
|
|
$jobs = []; |
|
161
|
|
View Code Duplication |
while ($row = $result->fetch()) { |
|
|
|
|
|
|
162
|
|
|
$job = $this->buildJob($row); |
|
|
|
|
|
|
163
|
|
|
if ($job) { |
|
164
|
|
|
$jobs[] = $job; |
|
165
|
|
|
} |
|
166
|
|
|
} |
|
167
|
|
|
$result->closeCursor(); |
|
168
|
|
|
|
|
169
|
|
|
return $jobs; |
|
170
|
|
|
} |
|
171
|
|
|
|
|
172
|
|
|
/** |
|
173
|
|
|
* get the next job in the list |
|
174
|
|
|
* |
|
175
|
|
|
* @return IJob|null |
|
176
|
|
|
*/ |
|
177
|
|
|
public function getNext() { |
|
178
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
179
|
|
|
$query->select('*') |
|
180
|
|
|
->from('jobs') |
|
181
|
|
|
->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - 12 * 3600, IQueryBuilder::PARAM_INT))) |
|
182
|
|
|
->orderBy('last_checked', 'ASC') |
|
183
|
|
|
->setMaxResults(1); |
|
184
|
|
|
|
|
185
|
|
|
$update = $this->connection->getQueryBuilder(); |
|
186
|
|
|
$update->update('jobs') |
|
187
|
|
|
->set('reserved_at', $update->createNamedParameter($this->timeFactory->getTime())) |
|
188
|
|
|
->set('last_checked', $update->createNamedParameter($this->timeFactory->getTime())) |
|
189
|
|
|
->where($update->expr()->eq('id', $update->createParameter('jobid'))) |
|
190
|
|
|
->andWhere($update->expr()->eq('reserved_at', $update->createParameter('reserved_at'))) |
|
191
|
|
|
->andWhere($update->expr()->eq('last_checked', $update->createParameter('last_checked'))); |
|
192
|
|
|
|
|
193
|
|
|
$result = $query->execute(); |
|
194
|
|
|
$row = $result->fetch(); |
|
195
|
|
|
$result->closeCursor(); |
|
196
|
|
|
|
|
197
|
|
|
if ($row) { |
|
198
|
|
|
$update->setParameter('jobid', $row['id']); |
|
199
|
|
|
$update->setParameter('reserved_at', $row['reserved_at']); |
|
200
|
|
|
$update->setParameter('last_checked', $row['last_checked']); |
|
201
|
|
|
$count = $update->execute(); |
|
202
|
|
|
|
|
203
|
|
|
if ($count === 0) { |
|
204
|
|
|
// Background job already executed elsewhere, try again. |
|
205
|
|
|
return $this->getNext(); |
|
206
|
|
|
} |
|
207
|
|
|
$job = $this->buildJob($row); |
|
208
|
|
|
|
|
209
|
|
|
if ($job === null) { |
|
210
|
|
|
// Background job from disabled app, try again. |
|
211
|
|
|
return $this->getNext(); |
|
212
|
|
|
} |
|
213
|
|
|
|
|
214
|
|
|
return $job; |
|
215
|
|
|
} else { |
|
216
|
|
|
return null; |
|
217
|
|
|
} |
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
/** |
|
221
|
|
|
* @param int $id |
|
222
|
|
|
* @return IJob|null |
|
223
|
|
|
*/ |
|
224
|
|
View Code Duplication |
public function getById($id) { |
|
225
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
226
|
|
|
$query->select('*') |
|
227
|
|
|
->from('jobs') |
|
228
|
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); |
|
229
|
|
|
$result = $query->execute(); |
|
230
|
|
|
$row = $result->fetch(); |
|
231
|
|
|
$result->closeCursor(); |
|
232
|
|
|
|
|
233
|
|
|
if ($row) { |
|
234
|
|
|
return $this->buildJob($row); |
|
235
|
|
|
} else { |
|
236
|
|
|
return null; |
|
237
|
|
|
} |
|
238
|
|
|
} |
|
239
|
|
|
|
|
240
|
|
|
/** |
|
241
|
|
|
* get the job object from a row in the db |
|
242
|
|
|
* |
|
243
|
|
|
* @param array $row |
|
244
|
|
|
* @return IJob|null |
|
245
|
|
|
*/ |
|
246
|
|
|
private function buildJob($row) { |
|
247
|
|
|
try { |
|
248
|
|
|
try { |
|
249
|
|
|
// Try to load the job as a service |
|
250
|
|
|
/** @var IJob $job */ |
|
251
|
|
|
$job = \OC::$server->query($row['class']); |
|
252
|
|
|
} catch (QueryException $e) { |
|
253
|
|
|
if (\class_exists($row['class'])) { |
|
254
|
|
|
$class = $row['class']; |
|
255
|
|
|
$job = new $class(); |
|
256
|
|
|
} else { |
|
257
|
|
|
// job from disabled app or old version of an app, no need to do anything |
|
258
|
|
|
return null; |
|
259
|
|
|
} |
|
260
|
|
|
} |
|
261
|
|
|
|
|
262
|
|
|
$job->setId($row['id']); |
|
263
|
|
|
$job->setLastRun($row['last_run']); |
|
264
|
|
|
$job->setArgument(\json_decode($row['argument'], true)); |
|
265
|
|
|
return $job; |
|
266
|
|
|
} catch (AutoloadNotAllowedException $e) { |
|
267
|
|
|
// job is from a disabled app, ignore |
|
268
|
|
|
return null; |
|
269
|
|
|
} |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
/** |
|
273
|
|
|
* set the job that was last ran |
|
274
|
|
|
* |
|
275
|
|
|
* @param IJob $job |
|
276
|
|
|
*/ |
|
277
|
|
|
public function setLastJob($job) { |
|
278
|
|
|
$this->unlockJob($job); |
|
279
|
|
|
$this->config->setAppValue('backgroundjob', 'lastjob', $job->getId()); |
|
280
|
|
|
} |
|
281
|
|
|
|
|
282
|
|
|
/** |
|
283
|
|
|
* Remove the reservation for a job |
|
284
|
|
|
* |
|
285
|
|
|
* @param IJob $job |
|
286
|
|
|
*/ |
|
287
|
|
View Code Duplication |
public function unlockJob($job) { |
|
288
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
289
|
|
|
$query->update('jobs') |
|
290
|
|
|
->set('reserved_at', $query->expr()->literal(0, IQueryBuilder::PARAM_INT)) |
|
291
|
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT))); |
|
292
|
|
|
$query->execute(); |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
/** |
|
296
|
|
|
* get the id of the last ran job |
|
297
|
|
|
* |
|
298
|
|
|
* @return int |
|
299
|
|
|
* @deprecated 9.1.0 - The functionality behind the value is deprecated, it |
|
300
|
|
|
* only tells you which job finished last, but since we now allow multiple |
|
301
|
|
|
* executors to run in parallel, it's not used to calculate the next job. |
|
302
|
|
|
*/ |
|
303
|
|
|
public function getLastJob() { |
|
304
|
|
|
return (int) $this->config->getAppValue('backgroundjob', 'lastjob', 0); |
|
305
|
|
|
} |
|
306
|
|
|
|
|
307
|
|
|
/** |
|
308
|
|
|
* @inheritdoc |
|
309
|
|
|
*/ |
|
310
|
|
|
public function setLastRun($job) { |
|
311
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
312
|
|
|
$query->update('jobs') |
|
313
|
|
|
->set('last_run', $query->createNamedParameter(\time(), IQueryBuilder::PARAM_INT)) |
|
314
|
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT))); |
|
315
|
|
|
$query->execute(); |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
View Code Duplication |
public function setExecutionTime($job, $timeTaken) { |
|
319
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
320
|
|
|
$query->update('jobs') |
|
321
|
|
|
->set('execution_duration', $query->createNamedParameter($timeTaken, IQueryBuilder::PARAM_INT)) |
|
322
|
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT))); |
|
323
|
|
|
$query->execute(); |
|
324
|
|
|
} |
|
325
|
|
|
|
|
326
|
|
|
/** |
|
327
|
|
|
* @inheritdoc |
|
328
|
|
|
*/ |
|
329
|
|
|
public function listJobs(\Closure $callback) { |
|
330
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
331
|
|
|
$query->select('*') |
|
332
|
|
|
->from('jobs'); |
|
333
|
|
|
$result = $query->execute(); |
|
334
|
|
|
|
|
335
|
|
View Code Duplication |
while ($row = $result->fetch()) { |
|
|
|
|
|
|
336
|
|
|
$job = $this->buildJob($row); |
|
|
|
|
|
|
337
|
|
|
if ($job) { |
|
338
|
|
|
if ($callback($job) === false) { |
|
339
|
|
|
break; |
|
340
|
|
|
} |
|
341
|
|
|
} |
|
342
|
|
|
} |
|
343
|
|
|
$result->closeCursor(); |
|
344
|
|
|
} |
|
345
|
|
|
} |
|
346
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.