1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.background;
16
17 import java.io.IOException;
18 import java.io.ObjectInputStream;
19 import java.lang.ref.WeakReference;
20 import java.util.PriorityQueue;
21 import java.util.concurrent.atomic.AtomicInteger;
22
23 import org.apache.commons.logging.Log;
24 import org.apache.commons.logging.LogFactory;
25 import org.htmlunit.Page;
26 import org.htmlunit.WebWindow;
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 class JavaScriptJobManagerImpl implements JavaScriptJobManager {
42
43
44
45
46
47 private final transient WeakReference<WebWindow> window_;
48
49
50
51
52
53 private transient PriorityQueue<JavaScriptJob> scheduledJobsQ_ = new PriorityQueue<>();
54
55 private transient JavaScriptJob currentlyRunningJob_;
56
57
58 private static final AtomicInteger NEXT_JOB_ID_ = new AtomicInteger(1);
59
60
61 private static final Log LOG = LogFactory.getLog(JavaScriptJobManagerImpl.class);
62
63
64
65
66
67
68 JavaScriptJobManagerImpl(final WebWindow window) {
69 window_ = new WeakReference<>(window);
70 }
71
72
73 @Override
74 public synchronized int getJobCount() {
75 return scheduledJobsQ_.size() + (currentlyRunningJob_ != null ? 1 : 0);
76 }
77
78
79 @Override
80 public synchronized int getJobCount(final JavaScriptJobFilter filter) {
81 if (filter == null) {
82 return scheduledJobsQ_.size() + (currentlyRunningJob_ != null ? 1 : 0);
83 }
84
85 int count = 0;
86 if (currentlyRunningJob_ != null && filter.passes(currentlyRunningJob_)) {
87 count++;
88 }
89 for (final JavaScriptJob job : scheduledJobsQ_) {
90 if (filter.passes(job)) {
91 count++;
92 }
93 }
94 return count;
95 }
96
97
98 @Override
99 public int addJob(final JavaScriptJob job, final Page page) {
100 final WebWindow w = getWindow();
101 if (w == null) {
102
103
104
105
106 return 0;
107 }
108 if (w.getEnclosedPage() != page) {
109
110
111
112
113 return 0;
114 }
115 final int id = NEXT_JOB_ID_.getAndIncrement();
116 job.setId(Integer.valueOf(id));
117
118 synchronized (this) {
119 scheduledJobsQ_.add(job);
120
121 if (LOG.isDebugEnabled()) {
122 LOG.debug("job added to queue");
123 LOG.debug(" window is: " + w);
124 LOG.debug(" added job: " + job);
125 LOG.debug("after adding job to the queue, the queue is: ");
126 printQueue();
127 }
128
129 notify();
130 }
131
132 return id;
133 }
134
135
136 @Override
137 public synchronized void removeJob(final int id) {
138 for (final JavaScriptJob job : scheduledJobsQ_) {
139 final int jobId = job.getId().intValue();
140 if (jobId == id) {
141 scheduledJobsQ_.remove(job);
142 notify();
143 break;
144 }
145 }
146 }
147
148
149 @Override
150 public synchronized void stopJob(final int id) {
151
152
153 removeJob(id);
154 }
155
156
157 @Override
158 public synchronized void removeAllJobs() {
159 scheduledJobsQ_.clear();
160 notify();
161 }
162
163
164 @Override
165 @SuppressWarnings("PMD.GuardLogStatement")
166 public int waitForJobs(final long timeoutMillis) {
167 final boolean debug = LOG.isDebugEnabled();
168 if (debug) {
169 LOG.debug("Waiting for all jobs to finish (will wait max " + timeoutMillis + " millis).");
170 }
171 if (timeoutMillis > 0) {
172 long now = System.currentTimeMillis();
173 final long end = now + timeoutMillis;
174
175 synchronized (this) {
176 while (getJobCount() > 0 && now < end) {
177 try {
178 wait(end - now);
179 }
180 catch (final InterruptedException e) {
181 LOG.error("InterruptedException while in waitForJobs", e);
182
183
184 Thread.currentThread().interrupt();
185 }
186
187
188 now = System.currentTimeMillis();
189 }
190 }
191 }
192 final int jobs = getJobCount();
193 if (debug) {
194 LOG.debug("Finished waiting for all jobs to finish (final job count is " + jobs + ").");
195 }
196 return jobs;
197 }
198
199
200 @Override
201 public int waitForJobsStartingBefore(final long delayMillis) {
202 return waitForJobsStartingBefore(delayMillis, -1, null);
203 }
204
205
206 @Override
207 public int waitForJobsStartingBefore(final long delayMillis, final long timeoutMillis) {
208 return waitForJobsStartingBefore(delayMillis, timeoutMillis, null);
209 }
210
211
212 @Override
213 @SuppressWarnings("PMD.GuardLogStatement")
214 public int waitForJobsStartingBefore(final long delayMillis, final JavaScriptJobFilter filter) {
215 return waitForJobsStartingBefore(delayMillis, -1, filter);
216 }
217
218
219 @Override
220 @SuppressWarnings("PMD.GuardLogStatement")
221 public int waitForJobsStartingBefore(final long delayMillis, final long timeoutMillis,
222 final JavaScriptJobFilter filter) {
223 final boolean debug = LOG.isDebugEnabled();
224
225 long now = System.currentTimeMillis();
226 long end = now + timeoutMillis;
227 if (timeoutMillis < 0 || timeoutMillis < delayMillis) {
228 end = -1;
229 }
230
231 final long latestExecutionTime = System.currentTimeMillis() + delayMillis;
232 if (debug) {
233 LOG.debug("Waiting for all jobs that have execution time before "
234 + delayMillis + " (" + latestExecutionTime + ") to finish");
235 }
236
237 final long interval = Math.max(40, delayMillis);
238 synchronized (this) {
239 JavaScriptJob earliestJob = getEarliestJob(filter);
240 boolean pending = earliestJob != null && earliestJob.getTargetExecutionTime() < latestExecutionTime;
241 pending = pending
242 || (
243 currentlyRunningJob_ != null
244 && (filter == null || filter.passes(currentlyRunningJob_))
245 && currentlyRunningJob_.getTargetExecutionTime() < latestExecutionTime
246 );
247
248 while (pending && (end == -1 || now < end)) {
249 try {
250 final long waitTime = (end == -1)
251 ? Math.max(40, interval)
252 : Math.max(40, Math.min(interval, end - now));
253 wait(waitTime);
254 }
255 catch (final InterruptedException e) {
256 LOG.error("InterruptedException while in waitForJobsStartingBefore", e);
257
258
259 Thread.currentThread().interrupt();
260 }
261
262 earliestJob = getEarliestJob(filter);
263 pending = earliestJob != null && earliestJob.getTargetExecutionTime() < latestExecutionTime;
264 pending = pending
265 || (
266 currentlyRunningJob_ != null
267 && (filter == null || filter.passes(currentlyRunningJob_))
268 && currentlyRunningJob_.getTargetExecutionTime() < latestExecutionTime
269 );
270 if (pending) {
271 now = System.currentTimeMillis();
272 }
273 }
274 }
275
276 final int jobs = getJobCount(filter);
277 if (debug) {
278 LOG.debug("Finished waiting for all jobs that have target execution time earlier than "
279 + latestExecutionTime + ", final job count is " + jobs);
280 }
281 return jobs;
282 }
283
284
285 @Override
286 public synchronized void shutdown() {
287 scheduledJobsQ_.clear();
288 notify();
289 }
290
291
292
293
294
295
296
297
298 private WebWindow getWindow() {
299 return window_.get();
300 }
301
302
303
304
305 private void printQueue() {
306 if (LOG.isDebugEnabled()) {
307 LOG.debug("------ printing JavaScript job queue -----");
308 LOG.debug(" number of jobs on the queue: " + scheduledJobsQ_.size());
309 int count = 1;
310 for (final JavaScriptJob job : scheduledJobsQ_) {
311 LOG.debug(" " + count + ") Job target execution time: " + job.getTargetExecutionTime());
312 LOG.debug(" job to string: " + job);
313 LOG.debug(" job id: " + job.getId());
314 if (job.isPeriodic()) {
315 LOG.debug(" period: " + job.getPeriod().intValue());
316 }
317 count++;
318 }
319 LOG.debug("------------------------------------------");
320 }
321 }
322
323
324
325
326 @Override
327 public synchronized String jobStatusDump(final JavaScriptJobFilter filter) {
328 final String lineSeparator = System.lineSeparator();
329
330 final StringBuilder status = new StringBuilder(110)
331 .append("------ JavaScript job status -----")
332 .append(lineSeparator);
333
334 if (null != currentlyRunningJob_ && (filter == null || filter.passes(currentlyRunningJob_))) {
335 status.append(" current running job: ").append(currentlyRunningJob_.toString())
336 .append(" job id: ").append(currentlyRunningJob_.getId())
337 .append(lineSeparator)
338 .append(lineSeparator)
339 .append(lineSeparator);
340 }
341 status.append(" number of jobs on the queue: ")
342 .append(scheduledJobsQ_.size())
343 .append(lineSeparator);
344
345 int count = 1;
346 for (final JavaScriptJob job : scheduledJobsQ_) {
347 if (filter == null || filter.passes(job)) {
348 final long now = System.currentTimeMillis();
349 final long execTime = job.getTargetExecutionTime();
350 status.append(" ").append(count).append(") Job target execution time: ")
351 .append(execTime).append(" (should start in ")
352 .append((execTime - now) / 1000d).append("s)")
353 .append(lineSeparator)
354 .append(" job to string: ").append(job)
355 .append(lineSeparator).append(" job id: ").append(job.getId())
356 .append(lineSeparator);
357 if (job.isPeriodic()) {
358 status.append(" period: ")
359 .append(job.getPeriod().toString())
360 .append(lineSeparator);
361 }
362 count++;
363 }
364 }
365 status.append("------------------------------------------")
366 .append(lineSeparator);
367
368 return status.toString();
369 }
370
371
372
373
374 @Override
375 public JavaScriptJob getEarliestJob() {
376 return scheduledJobsQ_.peek();
377 }
378
379
380
381
382 @Override
383 public synchronized JavaScriptJob getEarliestJob(final JavaScriptJobFilter filter) {
384 if (filter == null) {
385 return scheduledJobsQ_.peek();
386 }
387
388 for (final JavaScriptJob job : scheduledJobsQ_) {
389 if (filter.passes(job)) {
390 return job;
391 }
392 }
393 return null;
394 }
395
396
397
398
399 @Override
400 @SuppressWarnings("PMD.GuardLogStatement")
401 public boolean runSingleJob(final JavaScriptJob givenJob) {
402 assert givenJob != null;
403 final JavaScriptJob job = getEarliestJob();
404 if (job != givenJob) {
405 return false;
406 }
407
408 final long currentTime = System.currentTimeMillis();
409 if (job.getTargetExecutionTime() > currentTime) {
410 return false;
411 }
412
413 final boolean debug = LOG.isDebugEnabled();
414
415 synchronized (this) {
416 if (scheduledJobsQ_.remove(job)) {
417 currentlyRunningJob_ = job;
418
419 }
420
421
422
423 if (job.isPeriodic()) {
424 final long jobPeriod = job.getPeriod().longValue();
425
426
427 final long missedPeriods = (currentTime - job.getTargetExecutionTime()) / jobPeriod + 1;
428 job.setTargetExecutionTime(job.getTargetExecutionTime() + missedPeriods * jobPeriod);
429
430
431 if (debug) {
432 LOG.debug("Rescheduling periodic job " + job);
433 }
434 scheduledJobsQ_.add(job);
435 notify();
436 }
437 }
438
439 if (debug) {
440 final String periodicJob = job.isPeriodic() ? "interval " : "";
441 LOG.debug("Starting " + periodicJob + "job " + job);
442 }
443 try {
444 job.run();
445 }
446 catch (final RuntimeException e) {
447 LOG.error("Job run failed with unexpected RuntimeException: " + e.getMessage(), e);
448 }
449 finally {
450 synchronized (this) {
451 if (job == currentlyRunningJob_) {
452 currentlyRunningJob_ = null;
453 }
454 notify();
455 }
456 }
457 if (debug) {
458 final String periodicJob = job.isPeriodic() ? "interval " : "";
459 LOG.debug("Finished " + periodicJob + "job " + job);
460 }
461 return true;
462 }
463
464
465
466
467
468
469
470
471 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
472 in.defaultReadObject();
473
474
475 scheduledJobsQ_ = new PriorityQueue<>();
476 currentlyRunningJob_ = null;
477 }
478 }