These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Symbiote\QueuedJobs\DataObjects; |
||
4 | |||
5 | use SilverStripe\Assets\Filesystem; |
||
6 | use SilverStripe\Core\Config\Config; |
||
7 | use SilverStripe\Core\Convert; |
||
8 | use SilverStripe\Forms\DropdownField; |
||
9 | use SilverStripe\Forms\LiteralField; |
||
10 | use SilverStripe\ORM\DataObject; |
||
11 | use SilverStripe\ORM\FieldType\DBField; |
||
12 | use SilverStripe\Security\Permission; |
||
13 | use Symbiote\QueuedJobs\Services\QueuedJob; |
||
14 | use Symbiote\QueuedJobs\Services\QueuedJobService; |
||
15 | |||
16 | /** |
||
17 | * A QueuedJobDescriptor is the stored representation of a piece of work that could take a while to execute, |
||
18 | * because of which it is desireable to not have it executing in parallel to other jobs. |
||
19 | * |
||
20 | * A queued job should always attempt to report how many potential dataobjects will be affected by being executed; |
||
21 | * this will determine which queue it is placed within so that some shorter jobs can execute immediately without needing |
||
22 | * to wait for a potentially long running job. |
||
23 | * |
||
24 | * @property string $JobTitle Name of job |
||
25 | * @property string $Signature Unique identifier for this job instance |
||
26 | * @property string $Implementation Classname of underlying job |
||
27 | * @property string $StartAfter Don't start until this date, if set |
||
28 | * @property string $JobStarted When this job was started |
||
29 | * @property string $JobFinished When this job was finished |
||
30 | * @property int $TotalSteps Number of steps |
||
31 | * @property int $StepsProcessed Number of completed steps |
||
32 | * @property int $LastProcessedCount Number at which StepsProcessed was last checked for stalled jobs |
||
33 | * @property int $ResumeCounts Number of times this job has been resumed |
||
34 | * @property string $SavedJobData serialised data for the job to use as storage |
||
35 | * @property string $SavedJobMessages List of messages saved for this job |
||
36 | * @property string $JobStatus Status of this job |
||
37 | * @property string $JobType Type of job |
||
38 | * |
||
39 | * @method Member RunAs() Member to run this job as |
||
40 | * |
||
41 | * @author Marcus Nyeholt <[email protected]> |
||
42 | * @license BSD http://silverstripe.org/bsd-license/ |
||
43 | */ |
||
44 | class QueuedJobDescriptor extends DataObject |
||
45 | { |
||
46 | /** |
||
47 | * {@inheritDoc} |
||
48 | * @var string |
||
49 | */ |
||
50 | private static $table_name = 'QueuedJobDescriptor'; |
||
51 | |||
52 | /** |
||
53 | * @var array |
||
54 | */ |
||
55 | private static $db = [ |
||
56 | 'JobTitle' => 'Varchar(255)', |
||
57 | 'Signature' => 'Varchar(64)', |
||
58 | 'Implementation' => 'Varchar(255)', |
||
59 | 'StartAfter' => 'DBDatetime', |
||
60 | 'JobStarted' => 'DBDatetime', |
||
61 | 'JobRestarted' => 'DBDatetime', |
||
62 | 'JobFinished' => 'DBDatetime', |
||
63 | 'TotalSteps' => 'Int', |
||
64 | 'StepsProcessed' => 'Int', |
||
65 | 'LastProcessedCount' => 'Int(-1)', // -1 means never checked, 0 means checked but no work is done |
||
66 | 'ResumeCounts' => 'Int', |
||
67 | 'SavedJobData' => 'Text', |
||
68 | 'SavedJobMessages' => 'Text', |
||
69 | 'JobStatus' => 'Varchar(16)', |
||
70 | 'JobType' => 'Varchar(16)', |
||
71 | ]; |
||
72 | |||
73 | /** |
||
74 | * @var array |
||
75 | */ |
||
76 | private static $has_one = [ |
||
0 ignored issues
–
show
Comprehensibility
introduced
by
Loading history...
|
|||
77 | 'RunAs' => 'SilverStripe\\Security\\Member', |
||
78 | ]; |
||
79 | |||
80 | /** |
||
81 | * @var array |
||
82 | */ |
||
83 | private static $defaults = [ |
||
84 | 'JobStatus' => 'New', |
||
85 | 'ResumeCounts' => 0, |
||
86 | 'LastProcessedCount' => -1 // -1 means never checked, 0 means checked and none were processed |
||
87 | ]; |
||
88 | |||
89 | /** |
||
90 | * @var array |
||
91 | */ |
||
92 | private static $indexes = [ |
||
93 | 'JobStatus' => true, |
||
94 | 'StartAfter' => true, |
||
95 | 'Signature' => true, |
||
96 | ]; |
||
97 | |||
98 | /** |
||
99 | * @var array |
||
100 | */ |
||
101 | private static $casting = [ |
||
102 | 'Messages' => 'HTMLText', |
||
103 | ]; |
||
104 | |||
105 | /** |
||
106 | * @var array |
||
107 | */ |
||
108 | private static $searchable_fields = [ |
||
109 | 'JobTitle', |
||
110 | ]; |
||
111 | |||
112 | /** |
||
113 | * @var string |
||
114 | */ |
||
115 | private static $default_sort = 'Created DESC'; |
||
116 | |||
117 | public function requireDefaultRecords() |
||
118 | { |
||
119 | parent::requireDefaultRecords(); |
||
120 | $this->getJobDir(); |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * @return array |
||
125 | */ |
||
126 | public function summaryFields() |
||
127 | { |
||
128 | return [ |
||
129 | 'JobTitle' => _t(__CLASS__ . '.TABLE_TITLE', 'Title'), |
||
130 | 'Created' => _t(__CLASS__ . '.TABLE_ADDE', 'Added'), |
||
131 | 'JobStarted' => _t(__CLASS__ . '.TABLE_STARTED', 'Started'), |
||
132 | 'JobFinished' => _t(__CLASS__ . '.TABLE_FINISHED', 'Finished'), |
||
133 | // 'JobRestarted' => _t(__CLASS__ . '.TABLE_RESUMED', 'Resumed'), |
||
134 | 'StartAfter' => _t(__CLASS__ . '.TABLE_START_AFTER', 'Start After'), |
||
135 | 'JobTypeString' => _t(__CLASS__ . '.JOB_TYPE', 'Job Type'), |
||
136 | 'JobStatus' => _t(__CLASS__ . '.TABLE_STATUS', 'Status'), |
||
137 | 'LastMessage' => _t(__CLASS__ . '.TABLE_MESSAGES', 'Message'), |
||
138 | 'StepsProcessed' => _t(__CLASS__ . '.TABLE_NUM_PROCESSED', 'Done'), |
||
139 | 'TotalSteps' => _t(__CLASS__ . '.TABLE_TOTAL', 'Total'), |
||
140 | ]; |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * Pause this job, but only if it is waiting, running, or init |
||
145 | * |
||
146 | * @param bool $force Pause this job even if it's not waiting, running, or init |
||
147 | * |
||
148 | * @return bool Return true if this job was paused |
||
149 | */ |
||
150 | public function pause($force = false) |
||
151 | { |
||
152 | if ($force || in_array( |
||
153 | $this->JobStatus, |
||
154 | [QueuedJob::STATUS_WAIT, QueuedJob::STATUS_RUN, QueuedJob::STATUS_INIT] |
||
155 | )) { |
||
156 | $this->JobStatus = QueuedJob::STATUS_PAUSED; |
||
157 | $this->write(); |
||
158 | return true; |
||
159 | } |
||
160 | return false; |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * Resume this job and schedules it for execution |
||
165 | * |
||
166 | * @param bool $force Resume this job even if it's not paused or broken |
||
167 | * |
||
168 | * @return bool Return true if this job was resumed |
||
169 | */ |
||
170 | public function resume($force = false) |
||
171 | { |
||
172 | if ($force || in_array($this->JobStatus, [QueuedJob::STATUS_PAUSED, QueuedJob::STATUS_BROKEN])) { |
||
173 | $this->JobStatus = QueuedJob::STATUS_WAIT; |
||
174 | $this->ResumeCounts++; |
||
175 | $this->write(); |
||
176 | singleton(QueuedJobService::class)->startJob($this); |
||
177 | return true; |
||
178 | } |
||
179 | return false; |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * Restarts this job via a forced resume |
||
184 | */ |
||
185 | public function restart() |
||
186 | { |
||
187 | $this->resume(true); |
||
188 | } |
||
189 | |||
190 | /** |
||
191 | * Called to indicate that the job is ready to be run on the queue. This is done either as the result of |
||
192 | * creating the job and adding it, or when resuming. |
||
193 | */ |
||
194 | public function activateOnQueue() |
||
195 | { |
||
196 | // if it's an immediate job, lets cache it to disk to be picked up later |
||
197 | if ($this->JobType == QueuedJob::IMMEDIATE |
||
198 | && !Config::inst()->get(QueuedJobService::class, 'use_shutdown_function') |
||
199 | ) { |
||
200 | touch($this->getJobDir() . '/queuedjob-' . $this->ID); |
||
201 | } |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * Gets the path to the queuedjob cache directory |
||
206 | * |
||
207 | * @return string |
||
208 | */ |
||
209 | protected function getJobDir() |
||
210 | { |
||
211 | // make sure our temp dir is in place. This is what will be inotify watched |
||
212 | $jobDir = Config::inst()->get(QueuedJobService::class, 'cache_dir'); |
||
213 | if ($jobDir{0} != '/') { |
||
214 | $jobDir = TEMP_FOLDER . '/' . $jobDir; |
||
215 | } |
||
216 | |||
217 | if (!is_dir($jobDir)) { |
||
218 | Filesystem::makeFolder($jobDir); |
||
219 | } |
||
220 | return $jobDir; |
||
221 | } |
||
222 | |||
223 | public function execute() |
||
224 | { |
||
225 | $service = singleton(QueuedJobService::class); |
||
226 | $service->runJob($this->ID); |
||
227 | } |
||
228 | |||
229 | /** |
||
230 | * Called when the job has completed and we want to cleanup anything the descriptor has lying around |
||
231 | * in caches or the like. |
||
232 | */ |
||
233 | public function cleanupJob() |
||
234 | { |
||
235 | // remove the job's temp file if it exists |
||
236 | $tmpFile = $this->getJobDir() . '/queuedjob-' . $this->ID; |
||
237 | if (file_exists($tmpFile)) { |
||
238 | unlink($tmpFile); |
||
239 | } |
||
240 | } |
||
241 | |||
242 | public function onBeforeDelete() |
||
243 | { |
||
244 | parent::onBeforeDelete(); |
||
245 | $this->cleanupJob(); |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * Get all job messages as an HTML unordered list. |
||
250 | * |
||
251 | * @return string|void |
||
252 | */ |
||
253 | public function getMessages() |
||
254 | { |
||
255 | if (strlen($this->SavedJobMessages)) { |
||
256 | $messages = @unserialize($this->SavedJobMessages); |
||
257 | if (!empty($messages)) { |
||
258 | return DBField::create_field('HTMLText', '<ul><li>' . nl2br(implode('</li><li>', Convert::raw2xml($messages))) . '</li></ul>'); |
||
259 | } |
||
260 | return ''; |
||
261 | } |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * Get the last job message as a raw string |
||
266 | * |
||
267 | * @return string|void |
||
268 | */ |
||
269 | public function getLastMessage() |
||
270 | { |
||
271 | if (strlen($this->SavedJobMessages)) { |
||
272 | $msgs = @unserialize($this->SavedJobMessages); |
||
273 | if (is_array($msgs) && sizeof($msgs)) { |
||
274 | $msg = array_pop($msgs); |
||
275 | return $msg; |
||
276 | } |
||
277 | } |
||
278 | } |
||
279 | |||
280 | /** |
||
281 | * @return string |
||
282 | */ |
||
283 | public function getTitle() |
||
284 | { |
||
285 | return $this->JobTitle; |
||
286 | } |
||
287 | |||
288 | /** |
||
289 | * Return a string representation of the numeric JobType |
||
290 | * @return string |
||
291 | */ |
||
292 | public function getJobTypeString() |
||
293 | { |
||
294 | $map = $this->getJobTypeValues(); |
||
295 | return isset($map[$this->JobType]) ? $map[$this->JobType] : '(Unknown)'; |
||
296 | } |
||
297 | |||
298 | |||
299 | /** |
||
300 | * Return a map of numeric JobType values to localisable string representations. |
||
301 | * @return array |
||
302 | */ |
||
303 | public function getJobTypeValues() |
||
304 | { |
||
305 | return [ |
||
306 | QueuedJob::IMMEDIATE => _t(__CLASS__ . '.TYPE_IMMEDIATE', 'Immediate'), |
||
307 | QueuedJob::QUEUED => _t(__CLASS__ . '.TYPE_QUEUED', 'Queued'), |
||
308 | QueuedJob::LARGE => _t(__CLASS__ . '.TYPE_LARGE', 'Large'), |
||
309 | ]; |
||
310 | } |
||
311 | |||
312 | /** |
||
313 | * @return FieldList |
||
314 | */ |
||
315 | public function getCMSFields() |
||
316 | { |
||
317 | $fields = parent::getCMSFields(); |
||
318 | $fields->replaceField( |
||
319 | 'JobType', |
||
320 | new DropdownField('JobType', $this->fieldLabel('JobType'), $this->getJobTypeValues()) |
||
321 | ); |
||
322 | $statuses = [ |
||
323 | QueuedJob::STATUS_NEW, |
||
324 | QueuedJob::STATUS_INIT, |
||
325 | QueuedJob::STATUS_RUN, |
||
326 | QueuedJob::STATUS_WAIT, |
||
327 | QueuedJob::STATUS_COMPLETE, |
||
328 | QueuedJob::STATUS_PAUSED, |
||
329 | QueuedJob::STATUS_CANCELLED, |
||
330 | QueuedJob::STATUS_BROKEN, |
||
331 | ]; |
||
332 | $fields->replaceField( |
||
333 | 'JobStatus', |
||
334 | DropdownField::create('JobStatus', $this->fieldLabel('JobStatus'), array_combine($statuses, $statuses)) |
||
335 | ); |
||
336 | |||
337 | $fields->removeByName('SavedJobData'); |
||
338 | $fields->removeByName('SavedJobMessages'); |
||
339 | |||
340 | if (strlen($this->SavedJobMessages)) { |
||
341 | $fields->addFieldToTab('Root.Messages', new LiteralField('Messages', $this->getMessages())); |
||
0 ignored issues
–
show
It seems like
$this->getMessages() targeting Symbiote\QueuedJobs\Data...scriptor::getMessages() can also be of type null or object<SilverStripe\ORM\FieldType\DBField> ; however, SilverStripe\Forms\LiteralField::__construct() does only seem to accept string|object<SilverStripe\Forms\FormField> , maybe add an additional type check?
This check looks at variables that are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble.
Loading history...
|
|||
342 | } |
||
343 | |||
344 | if (Permission::check('ADMIN')) { |
||
345 | return $fields; |
||
346 | } |
||
347 | |||
348 | // Readonly CMS view is a lot more useful for debugging than no view at all |
||
349 | return $fields->makeReadonly(); |
||
350 | } |
||
351 | } |
||
352 |