View Javadoc
1   /*
2    * Copyright (c) 2002-2017 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.javascript.background;
16  
17  import java.io.IOException;
18  import java.io.ObjectInputStream;
19  import java.lang.ref.WeakReference;
20  import java.util.ArrayList;
21  import java.util.PriorityQueue;
22  import java.util.concurrent.atomic.AtomicInteger;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  
27  import com.gargoylesoftware.htmlunit.Page;
28  import com.gargoylesoftware.htmlunit.WebWindow;
29  
30  /**
31   * <p>Default implementation of {@link JavaScriptJobManager}.</p>
32   *
33   * <p>This job manager class is guaranteed not to keep old windows in memory (no window memory leaks).</p>
34   *
35   * <p>This job manager is serializable, but any running jobs are transient and are not serialized.</p>
36   *
37   * @author Daniel Gredler
38   * @author Katharina Probst
39   * @author Amit Manjhi
40   * @author Ronald Brill
41   * @author Carsten Steul
42   */
43  class JavaScriptJobManagerImpl implements JavaScriptJobManager {
44  
45      /**
46       * The window to which this job manager belongs (weakly referenced, so as not
47       * to leak memory).
48       */
49      private final transient WeakReference<WebWindow> window_;
50  
51      /**
52       * Queue of jobs that are scheduled to run. This is a priority queue, sorted
53       * by closest target execution time.
54       */
55      private transient PriorityQueue<JavaScriptJob> scheduledJobsQ_ = new PriorityQueue<>();
56  
57      private transient ArrayList<Integer> cancelledJobs_ = new ArrayList<>();
58  
59      private transient JavaScriptJob currentlyRunningJob_ = null;
60  
61      /** A counter used to generate the IDs assigned to {@link JavaScriptJob}s. */
62      private static final AtomicInteger NEXT_JOB_ID_ = new AtomicInteger(1);
63  
64      /** Logging support. */
65      private static final Log LOG = LogFactory.getLog(JavaScriptJobManagerImpl.class);
66  
67      /**
68       * Creates a new instance.
69       *
70       * @param window the window associated with the new job manager
71       */
72      JavaScriptJobManagerImpl(final WebWindow window) {
73          window_ = new WeakReference<>(window);
74      }
75  
76      /** {@inheritDoc} */
77      @Override
78      public synchronized int getJobCount() {
79          return scheduledJobsQ_.size() + (currentlyRunningJob_ != null ? 1 : 0);
80      }
81  
82      /** {@inheritDoc} */
83      @Override
84      public synchronized int getJobCount(final JavaScriptJobFilter filter) {
85          if (filter == null) {
86              return scheduledJobsQ_.size() + (currentlyRunningJob_ != null ? 1 : 0);
87          }
88  
89          int count = 0;
90          if (currentlyRunningJob_ != null && filter.passes(currentlyRunningJob_)) {
91              count++;
92          }
93          for (JavaScriptJob job : scheduledJobsQ_) {
94              if (filter.passes(job)) {
95                  count++;
96              }
97          }
98          return count;
99      }
100 
101     /** {@inheritDoc} */
102     @Override
103     public int addJob(final JavaScriptJob job, final Page page) {
104         final WebWindow w = getWindow();
105         if (w == null) {
106             /*
107              * The window to which this job manager belongs has been garbage
108              * collected. Don't spawn any more jobs for it.
109              */
110             return 0;
111         }
112         if (w.getEnclosedPage() != page) {
113             /*
114              * The page requesting the addition of the job is no longer contained by
115              * our owner window. Don't let it spawn any more jobs.
116              */
117             return 0;
118         }
119         final int id = NEXT_JOB_ID_.getAndIncrement();
120         job.setId(Integer.valueOf(id));
121 
122         synchronized (this) {
123             scheduledJobsQ_.add(job);
124 
125             if (LOG.isDebugEnabled()) {
126                 LOG.debug("job added to queue");
127                 LOG.debug("    window is: " + w);
128                 LOG.debug("    added job: " + job.toString());
129                 LOG.debug("after adding job to the queue, the queue is: ");
130                 printQueue();
131             }
132 
133             notify();
134         }
135 
136         return id;
137     }
138 
139     /** {@inheritDoc} */
140     @Override
141     public synchronized void removeJob(final int id) {
142         for (final JavaScriptJob job : scheduledJobsQ_) {
143             final int jobId = job.getId().intValue();
144             if (jobId == id) {
145                 scheduledJobsQ_.remove(job);
146                 break;
147             }
148         }
149         cancelledJobs_.add(Integer.valueOf(id));
150         notify();
151     }
152 
153     /** {@inheritDoc} */
154     @Override
155     public synchronized void stopJob(final int id) {
156         for (final JavaScriptJob job : scheduledJobsQ_) {
157             final int jobId = job.getId().intValue();
158             if (jobId == id) {
159                 scheduledJobsQ_.remove(job);
160                 // TODO: should we try to interrupt the job if it is running?
161                 break;
162             }
163         }
164         cancelledJobs_.add(Integer.valueOf(id));
165         notify();
166     }
167 
168     /** {@inheritDoc} */
169     @Override
170     public synchronized void removeAllJobs() {
171         if (currentlyRunningJob_ != null) {
172             cancelledJobs_.add(currentlyRunningJob_.getId());
173         }
174         for (final JavaScriptJob job : scheduledJobsQ_) {
175             cancelledJobs_.add(job.getId());
176         }
177         scheduledJobsQ_.clear();
178         notify();
179     }
180 
181     /** {@inheritDoc} */
182     @Override
183     public int waitForJobs(final long timeoutMillis) {
184         final boolean debug = LOG.isDebugEnabled();
185         if (debug) {
186             LOG.debug("Waiting for all jobs to finish (will wait max " + timeoutMillis + " millis).");
187         }
188         if (timeoutMillis > 0) {
189             long now = System.currentTimeMillis();
190             final long end = now + timeoutMillis;
191 
192             synchronized (this) {
193                 while (getJobCount() > 0 && now < end) {
194                     try {
195                         wait(end - now);
196                     }
197                     catch (final InterruptedException e) {
198                         LOG.error("InterruptedException while in waitForJobs", e);
199                     }
200                     // maybe a change triggers the wakup; we have to recalculate the
201                     // wait time
202                     now = System.currentTimeMillis();
203                 }
204             }
205         }
206         final int jobs = getJobCount();
207         if (debug) {
208             LOG.debug("Finished waiting for all jobs to finish (final job count is " + jobs + ").");
209         }
210         return jobs;
211     }
212 
213     /** {@inheritDoc} */
214     @Override
215     public int waitForJobsStartingBefore(final long delayMillis) {
216         return waitForJobsStartingBefore(delayMillis, null);
217     }
218 
219     /** {@inheritDoc} */
220     @Override
221     public int waitForJobsStartingBefore(final long delayMillis, final JavaScriptJobFilter filter) {
222         final boolean debug = LOG.isDebugEnabled();
223 
224         final long latestExecutionTime = System.currentTimeMillis() + delayMillis;
225         if (debug) {
226             LOG.debug("Waiting for all jobs that have execution time before "
227                   + delayMillis + " (" + latestExecutionTime + ") to finish");
228         }
229 
230         final long interval = Math.max(40, delayMillis);
231         synchronized (this) {
232             JavaScriptJob earliestJob = getEarliestJob(filter);
233             boolean pending = earliestJob != null && earliestJob.getTargetExecutionTime() < latestExecutionTime;
234             pending = pending
235                     || (
236                             currentlyRunningJob_ != null
237                             && (filter == null || filter.passes(currentlyRunningJob_))
238                             && currentlyRunningJob_.getTargetExecutionTime() < latestExecutionTime
239                        );
240 
241             while (pending) {
242                 try {
243                     wait(interval);
244                 }
245                 catch (final InterruptedException e) {
246                     LOG.error("InterruptedException while in waitForJobsStartingBefore", e);
247                 }
248 
249                 earliestJob = getEarliestJob(filter);
250                 pending = earliestJob != null && earliestJob.getTargetExecutionTime() < latestExecutionTime;
251                 pending = pending
252                         || (
253                                 currentlyRunningJob_ != null
254                                 && (filter == null || filter.passes(currentlyRunningJob_))
255                                 && currentlyRunningJob_.getTargetExecutionTime() < latestExecutionTime
256                            );
257             }
258         }
259 
260         final int jobs = getJobCount(filter);
261         if (debug) {
262             LOG.debug("Finished waiting for all jobs that have target execution time earlier than "
263                 + latestExecutionTime + ", final job count is " + jobs);
264         }
265         return jobs;
266     }
267 
268     /** {@inheritDoc} */
269     @Override
270     public synchronized void shutdown() {
271         scheduledJobsQ_.clear();
272         notify();
273     }
274 
275     /**
276      * Returns the window to which this job manager belongs, or {@code null} if
277      * it has been garbage collected.
278      *
279      * @return the window to which this job manager belongs, or {@code null} if
280      *         it has been garbage collected
281      */
282     private WebWindow getWindow() {
283         return window_.get();
284     }
285 
286     /**
287      * Utility method to print current queue.
288      */
289     private void printQueue() {
290         if (LOG.isDebugEnabled()) {
291             LOG.debug("------ printing JavaScript job queue -----");
292             LOG.debug("  number of jobs on the queue: " + scheduledJobsQ_.size());
293             int count = 1;
294             for (final JavaScriptJob job : scheduledJobsQ_) {
295                 LOG.debug("  " + count + ")  Job target execution time: " + job.getTargetExecutionTime());
296                 LOG.debug("      job to string: " + job.toString());
297                 LOG.debug("      job id: " + job.getId());
298                 if (job.isPeriodic()) {
299                     LOG.debug("      period: " + job.getPeriod().intValue());
300                 }
301                 count++;
302             }
303             LOG.debug("------------------------------------------");
304         }
305     }
306 
307     /**
308      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
309      */
310     @Override
311     public synchronized String jobStatusDump(final JavaScriptJobFilter filter) {
312         final StringBuilder status = new StringBuilder();
313         final String lineSeparator = System.lineSeparator();
314         status.append("------ JavaScript job status -----");
315         status.append(lineSeparator);
316         if (null != currentlyRunningJob_ && (filter == null || filter.passes(currentlyRunningJob_))) {
317             status.append("  current running job: ").append(currentlyRunningJob_.toString());
318             status.append("      job id: " + currentlyRunningJob_.getId());
319             status.append(lineSeparator);
320             status.append(lineSeparator);
321             status.append(lineSeparator);
322         }
323         status.append("  number of jobs on the queue: " + scheduledJobsQ_.size());
324         status.append(lineSeparator);
325         int count = 1;
326         for (final JavaScriptJob job : scheduledJobsQ_) {
327             if (filter == null || filter.passes(job)) {
328                 final long now = System.currentTimeMillis();
329                 final long execTime = job.getTargetExecutionTime();
330                 status.append("  " + count);
331                 status.append(")  Job target execution time: " + execTime);
332                 status.append(" (should start in " + ((execTime - now) / 1000d) + "s)");
333                 status.append(lineSeparator);
334                 status.append("      job to string: ").append(job.toString());
335                 status.append(lineSeparator);
336                 status.append("      job id: " + job.getId());
337                 status.append(lineSeparator);
338                 if (job.isPeriodic()) {
339                     status.append("      period: " + job.getPeriod().intValue());
340                     status.append(lineSeparator);
341                 }
342                 count++;
343             }
344         }
345         status.append("------------------------------------------");
346         status.append(lineSeparator);
347 
348         return status.toString();
349     }
350 
351     /**
352      * {@inheritDoc}
353      */
354     @Override
355     public JavaScriptJob getEarliestJob() {
356         return scheduledJobsQ_.peek();
357     }
358 
359     /**
360      * {@inheritDoc}
361      */
362     @Override
363     public synchronized JavaScriptJob getEarliestJob(final JavaScriptJobFilter filter) {
364         if (filter == null) {
365             return scheduledJobsQ_.peek();
366         }
367 
368         for (JavaScriptJob job : scheduledJobsQ_) {
369             if (filter.passes(job)) {
370                 return job;
371             }
372         }
373         return null;
374     }
375 
376     /**
377      * {@inheritDoc}
378      */
379     @Override
380     public boolean runSingleJob(final JavaScriptJob givenJob) {
381         assert givenJob != null;
382         final JavaScriptJob job = getEarliestJob();
383         if (job != givenJob) {
384             return false;
385         }
386 
387         final long currentTime = System.currentTimeMillis();
388         if (job.getTargetExecutionTime() > currentTime) {
389             return false;
390         }
391         synchronized (this) {
392             if (scheduledJobsQ_.remove(job)) {
393                 currentlyRunningJob_ = job;
394             }
395             // no need to notify if processing is started
396         }
397 
398         final boolean debug = LOG.isDebugEnabled();
399         final boolean isPeriodicJob = job.isPeriodic();
400         if (isPeriodicJob) {
401             final long jobPeriod = job.getPeriod().longValue();
402 
403             // reference: http://ejohn.org/blog/how-javascript-timers-work/
404             long timeDifference = currentTime - job.getTargetExecutionTime();
405             timeDifference = (timeDifference / jobPeriod) * jobPeriod + jobPeriod;
406             job.setTargetExecutionTime(job.getTargetExecutionTime() + timeDifference);
407 
408             // queue
409             synchronized (this) {
410                 if (!cancelledJobs_.contains(job.getId())) {
411                     if (debug) {
412                         LOG.debug("Reschedulling job " + job);
413                     }
414                     scheduledJobsQ_.add(job);
415                     notify();
416                 }
417             }
418         }
419         if (debug) {
420             final String periodicJob = isPeriodicJob ? "interval " : "";
421             LOG.debug("Starting " + periodicJob + "job " + job);
422         }
423         try {
424             job.run();
425         }
426         catch (final RuntimeException e) {
427             LOG.error("Job run failed with unexpected RuntimeException: " + e.getMessage(), e);
428         }
429         finally {
430             synchronized (this) {
431                 if (job == currentlyRunningJob_) {
432                     currentlyRunningJob_ = null;
433                 }
434                 notify();
435             }
436         }
437         if (debug) {
438             final String periodicJob = isPeriodicJob ? "interval " : "";
439             LOG.debug("Finished " + periodicJob + "job " + job);
440         }
441         return true;
442     }
443 
444     /**
445      * Our own serialization (to handle the weak reference)
446      * @param in the stream to read form
447      * @throws IOException in case of error
448      * @throws ClassNotFoundException in case of error
449      */
450     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
451         in.defaultReadObject();
452 
453         // we do not store the jobs (at the moment)
454         scheduledJobsQ_ = new PriorityQueue<>();
455         cancelledJobs_ = new ArrayList<>();
456         currentlyRunningJob_ = null;
457     }
458 }