com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJobManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJobManagerImpl.java

Source

/*
 * Copyright (c) 2002-2016 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
 * http://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.io.ObjectOutputStream;
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;

/**
 * <p>Default implementation of {@link JavaScriptJobManager}.</p>
 *
 * <p>This job manager class is guaranteed not to keep old windows in memory (no window memory leaks).</p>
 *
 * <p>This job manager is serializable, but any running jobs are transient and are not serialized.</p>
 *
 * @author Daniel Gredler
 * @author Katharina Probst
 * @author Amit Manjhi
 * @author Ronald Brill
 * @author Carsten Steul
 * @see MemoryLeakTest
 */
class JavaScriptJobManagerImpl implements JavaScriptJobManager {

    private static final String LINE_SEPARARATOR = System.getProperty("line.separator");

    /**
     * The window to which this job manager belongs (weakly referenced, so as not
     * to leak memory).
     */
    private final transient WeakReference<WebWindow> window_;

    /**
     * Queue of jobs that are scheduled to run. This is a priority queue, sorted
     * by closest target execution time.
     */
    private transient PriorityQueue<JavaScriptJob> scheduledJobsQ_ = new PriorityQueue<>();

    private transient ArrayList<Integer> cancelledJobs_ = new ArrayList<>();

    private transient JavaScriptJob currentlyRunningJob_ = null;

    /** 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 (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.toString());
                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.toString());
                LOG.debug("      job id: " + job.getId());
                if (job.isPeriodic()) {
                    LOG.debug("      period: " + job.getPeriod().intValue());
                }
                count++;
            }
            LOG.debug("------------------------------------------");
        }
    }

    /**
     * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
     */
    @Override
    public synchronized String jobStatusDump(final JavaScriptJobFilter filter) {
        final StringBuilder status = new StringBuilder();
        status.append("------ JavaScript job status -----");
        status.append(LINE_SEPARARATOR);
        if (null != currentlyRunningJob_ && (filter == null || filter.passes(currentlyRunningJob_))) {
            status.append("  current running job: ").append(currentlyRunningJob_.toString());
            status.append("      job id: " + currentlyRunningJob_.getId());
            status.append(LINE_SEPARARATOR);
            status.append(LINE_SEPARARATOR);
            status.append(LINE_SEPARARATOR);
        }
        status.append("  number of jobs on the queue: " + scheduledJobsQ_.size());
        status.append(LINE_SEPARARATOR);
        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("  " + count);
                status.append(")  Job target execution time: " + execTime);
                status.append(" (should start in " + ((execTime - now) / 1000d) + "s)");
                status.append(LINE_SEPARARATOR);
                status.append("      job to string: ").append(job.toString());
                status.append(LINE_SEPARARATOR);
                status.append("      job id: " + job.getId());
                status.append(LINE_SEPARARATOR);
                if (job.isPeriodic()) {
                    status.append("      period: " + job.getPeriod().intValue());
                    status.append(LINE_SEPARARATOR);
                }
                count++;
            }
        }
        status.append("------------------------------------------");
        status.append(LINE_SEPARARATOR);

        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 (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 out the stream to write to
     * @throws IOException in case of error
     */
    private void writeObject(final ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
    }

    /**
     * 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;
    }
}