All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJobManagerImpl Maven / Gradle / Ivy

There is a newer version: 2.70.0
Show newest version
/*
 * Copyright (c) 2002-2022 Gargoyle Software Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gargoylesoftware.htmlunit.javascript.background;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebWindow;

/**
 * 

Default implementation of {@link JavaScriptJobManager}.

* *

This job manager class is guaranteed not to keep old windows in memory (no window memory leaks).

* *

This job manager is serializable, but any running jobs are transient and are not serialized.

* * @author Daniel Gredler * @author Katharina Probst * @author Amit Manjhi * @author Ronald Brill * @author Carsten Steul */ class JavaScriptJobManagerImpl implements JavaScriptJobManager { /** * The window to which this job manager belongs (weakly referenced, so as not * to leak memory). */ private final transient WeakReference window_; /** * Queue of jobs that are scheduled to run. This is a priority queue, sorted * by closest target execution time. */ private transient PriorityQueue scheduledJobsQ_ = new PriorityQueue<>(); private transient ArrayList cancelledJobs_ = new ArrayList<>(); private transient JavaScriptJob currentlyRunningJob_; /** A counter used to generate the IDs assigned to {@link JavaScriptJob}s. */ private static final AtomicInteger NEXT_JOB_ID_ = new AtomicInteger(1); /** Logging support. */ private static final Log LOG = LogFactory.getLog(JavaScriptJobManagerImpl.class); /** * Creates a new instance. * * @param window the window associated with the new job manager */ JavaScriptJobManagerImpl(final WebWindow window) { window_ = new WeakReference<>(window); } /** {@inheritDoc} */ @Override public synchronized int getJobCount() { return scheduledJobsQ_.size() + (currentlyRunningJob_ != null ? 1 : 0); } /** {@inheritDoc} */ @Override public synchronized int getJobCount(final JavaScriptJobFilter filter) { if (filter == null) { return scheduledJobsQ_.size() + (currentlyRunningJob_ != null ? 1 : 0); } int count = 0; if (currentlyRunningJob_ != null && filter.passes(currentlyRunningJob_)) { count++; } for (final JavaScriptJob job : scheduledJobsQ_) { if (filter.passes(job)) { count++; } } return count; } /** {@inheritDoc} */ @Override public int addJob(final JavaScriptJob job, final Page page) { final WebWindow w = getWindow(); if (w == null) { /* * The window to which this job manager belongs has been garbage * collected. Don't spawn any more jobs for it. */ return 0; } if (w.getEnclosedPage() != page) { /* * The page requesting the addition of the job is no longer contained by * our owner window. Don't let it spawn any more jobs. */ return 0; } final int id = NEXT_JOB_ID_.getAndIncrement(); job.setId(Integer.valueOf(id)); synchronized (this) { scheduledJobsQ_.add(job); if (LOG.isDebugEnabled()) { LOG.debug("job added to queue"); LOG.debug(" window is: " + w); LOG.debug(" added job: " + job); LOG.debug("after adding job to the queue, the queue is: "); printQueue(); } notify(); } return id; } /** {@inheritDoc} */ @Override public synchronized void removeJob(final int id) { for (final JavaScriptJob job : scheduledJobsQ_) { final int jobId = job.getId().intValue(); if (jobId == id) { scheduledJobsQ_.remove(job); break; } } cancelledJobs_.add(Integer.valueOf(id)); notify(); } /** {@inheritDoc} */ @Override public synchronized void stopJob(final int id) { for (final JavaScriptJob job : scheduledJobsQ_) { final int jobId = job.getId().intValue(); if (jobId == id) { scheduledJobsQ_.remove(job); // TODO: should we try to interrupt the job if it is running? break; } } cancelledJobs_.add(Integer.valueOf(id)); notify(); } /** {@inheritDoc} */ @Override public synchronized void removeAllJobs() { if (currentlyRunningJob_ != null) { cancelledJobs_.add(currentlyRunningJob_.getId()); } for (final JavaScriptJob job : scheduledJobsQ_) { cancelledJobs_.add(job.getId()); } scheduledJobsQ_.clear(); notify(); } /** {@inheritDoc} */ @Override public int waitForJobs(final long timeoutMillis) { final boolean debug = LOG.isDebugEnabled(); if (debug) { LOG.debug("Waiting for all jobs to finish (will wait max " + timeoutMillis + " millis)."); } if (timeoutMillis > 0) { long now = System.currentTimeMillis(); final long end = now + timeoutMillis; synchronized (this) { while (getJobCount() > 0 && now < end) { try { wait(end - now); } catch (final InterruptedException e) { LOG.error("InterruptedException while in waitForJobs", e); } // maybe a change triggers the wakup; we have to recalculate the // wait time now = System.currentTimeMillis(); } } } final int jobs = getJobCount(); if (debug) { LOG.debug("Finished waiting for all jobs to finish (final job count is " + jobs + ")."); } return jobs; } /** {@inheritDoc} */ @Override public int waitForJobsStartingBefore(final long delayMillis) { return waitForJobsStartingBefore(delayMillis, null); } /** {@inheritDoc} */ @Override public int waitForJobsStartingBefore(final long delayMillis, final JavaScriptJobFilter filter) { final boolean debug = LOG.isDebugEnabled(); final long latestExecutionTime = System.currentTimeMillis() + delayMillis; if (debug) { LOG.debug("Waiting for all jobs that have execution time before " + delayMillis + " (" + latestExecutionTime + ") to finish"); } final long interval = Math.max(40, delayMillis); synchronized (this) { JavaScriptJob earliestJob = getEarliestJob(filter); boolean pending = earliestJob != null && earliestJob.getTargetExecutionTime() < latestExecutionTime; pending = pending || ( currentlyRunningJob_ != null && (filter == null || filter.passes(currentlyRunningJob_)) && currentlyRunningJob_.getTargetExecutionTime() < latestExecutionTime ); while (pending) { try { wait(interval); } catch (final InterruptedException e) { LOG.error("InterruptedException while in waitForJobsStartingBefore", e); } earliestJob = getEarliestJob(filter); pending = earliestJob != null && earliestJob.getTargetExecutionTime() < latestExecutionTime; pending = pending || ( currentlyRunningJob_ != null && (filter == null || filter.passes(currentlyRunningJob_)) && currentlyRunningJob_.getTargetExecutionTime() < latestExecutionTime ); } } final int jobs = getJobCount(filter); if (debug) { LOG.debug("Finished waiting for all jobs that have target execution time earlier than " + latestExecutionTime + ", final job count is " + jobs); } return jobs; } /** {@inheritDoc} */ @Override public synchronized void shutdown() { scheduledJobsQ_.clear(); notify(); } /** * Returns the window to which this job manager belongs, or {@code null} if * it has been garbage collected. * * @return the window to which this job manager belongs, or {@code null} if * it has been garbage collected */ private WebWindow getWindow() { return window_.get(); } /** * Utility method to print current queue. */ private void printQueue() { if (LOG.isDebugEnabled()) { LOG.debug("------ printing JavaScript job queue -----"); LOG.debug(" number of jobs on the queue: " + scheduledJobsQ_.size()); int count = 1; for (final JavaScriptJob job : scheduledJobsQ_) { LOG.debug(" " + count + ") Job target execution time: " + job.getTargetExecutionTime()); LOG.debug(" job to string: " + job); LOG.debug(" job id: " + job.getId()); if (job.isPeriodic()) { LOG.debug(" period: " + job.getPeriod().intValue()); } count++; } LOG.debug("------------------------------------------"); } } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*/ @Override public synchronized String jobStatusDump(final JavaScriptJobFilter filter) { final String lineSeparator = System.lineSeparator(); final StringBuilder status = new StringBuilder(110) .append("------ JavaScript job status -----") .append(lineSeparator); if (null != currentlyRunningJob_ && (filter == null || filter.passes(currentlyRunningJob_))) { status.append(" current running job: ").append(currentlyRunningJob_.toString()) .append(" job id: ").append(currentlyRunningJob_.getId()) .append(lineSeparator) .append(lineSeparator) .append(lineSeparator); } status.append(" number of jobs on the queue: ") .append(scheduledJobsQ_.size()) .append(lineSeparator); int count = 1; for (final JavaScriptJob job : scheduledJobsQ_) { if (filter == null || filter.passes(job)) { final long now = System.currentTimeMillis(); final long execTime = job.getTargetExecutionTime(); status.append(" ").append(count).append(") Job target execution time: ") .append(execTime).append(" (should start in ") .append((execTime - now) / 1000d).append("s)") .append(lineSeparator) .append(" job to string: ").append(job) .append(lineSeparator).append(" job id: ").append(job.getId()) .append(lineSeparator); if (job.isPeriodic()) { status.append(" period: ") .append(job.getPeriod().toString()) .append(lineSeparator); } count++; } } status.append("------------------------------------------") .append(lineSeparator); return status.toString(); } /** * {@inheritDoc} */ @Override public JavaScriptJob getEarliestJob() { return scheduledJobsQ_.peek(); } /** * {@inheritDoc} */ @Override public synchronized JavaScriptJob getEarliestJob(final JavaScriptJobFilter filter) { if (filter == null) { return scheduledJobsQ_.peek(); } for (final JavaScriptJob job : scheduledJobsQ_) { if (filter.passes(job)) { return job; } } return null; } /** * {@inheritDoc} */ @Override public boolean runSingleJob(final JavaScriptJob givenJob) { assert givenJob != null; final JavaScriptJob job = getEarliestJob(); if (job != givenJob) { return false; } final long currentTime = System.currentTimeMillis(); if (job.getTargetExecutionTime() > currentTime) { return false; } synchronized (this) { if (scheduledJobsQ_.remove(job)) { currentlyRunningJob_ = job; } // no need to notify if processing is started } final boolean debug = LOG.isDebugEnabled(); final boolean isPeriodicJob = job.isPeriodic(); if (isPeriodicJob) { final long jobPeriod = job.getPeriod().longValue(); // reference: http://ejohn.org/blog/how-javascript-timers-work/ long timeDifference = currentTime - job.getTargetExecutionTime(); timeDifference = (timeDifference / jobPeriod) * jobPeriod + jobPeriod; job.setTargetExecutionTime(job.getTargetExecutionTime() + timeDifference); // queue synchronized (this) { if (!cancelledJobs_.contains(job.getId())) { if (debug) { LOG.debug("Reschedulling job " + job); } scheduledJobsQ_.add(job); notify(); } } } if (debug) { final String periodicJob = isPeriodicJob ? "interval " : ""; LOG.debug("Starting " + periodicJob + "job " + job); } try { job.run(); } catch (final RuntimeException e) { LOG.error("Job run failed with unexpected RuntimeException: " + e.getMessage(), e); } finally { synchronized (this) { if (job == currentlyRunningJob_) { currentlyRunningJob_ = null; } notify(); } } if (debug) { final String periodicJob = isPeriodicJob ? "interval " : ""; LOG.debug("Finished " + periodicJob + "job " + job); } return true; } /** * Our own serialization (to handle the weak reference) * @param in the stream to read form * @throws IOException in case of error * @throws ClassNotFoundException in case of error */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // we do not store the jobs (at the moment) scheduledJobsQ_ = new PriorityQueue<>(); cancelledJobs_ = new ArrayList<>(); currentlyRunningJob_ = null; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy