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

org.eclipse.jdt.internal.core.search.processing.JobManager Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright (c) 2000, 2022 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.core.search.processing;

import static org.eclipse.jdt.internal.core.JavaModelManager.trace;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.util.Messages;
import org.eclipse.jdt.internal.core.util.Util;

public abstract class JobManager {

	/**
	 * queue of jobs to execute
	 * 
* synchronized by JobManager.this */ private final List awaitingJobs = new LinkedList<>(); private volatile boolean executing; /** * background processing *
* synchronized by JobManager.this */ private Thread processingThread; private volatile Job progressJob; /** * counter indicating whether job execution is enabled or not, disabled if <= 0 * it cannot go beyond 1 *
* synchronized by JobManager.this */ private int enableCount = 1; public static boolean VERBOSE = false; /** * flag indicating that the activation has completed *
* synchronized by JobManager.this */ private boolean activated; private final AtomicInteger awaitingClients = new AtomicInteger(); private final Object idleMonitor = new Object(); private synchronized Thread getProcessingThread() { return this.processingThread; } /** * Invoked exactly once, in background, before starting processing any job */ synchronized void activateProcessing() { this.activated = true; // someone may wait for awaitingJobsCount() that returned dummy job count // before indexer was started notifyAll(); } /** * Answer the amount of awaiting jobs. */ public synchronized int awaitingJobsCount() { // pretend busy in case concurrent job attempts performing before activated return this.activated ? this.awaitingJobs.size() : 1; } /** * Answers the first job in the queue, or null if there is no job available or * index manager is disabled * * Until the job has completed, the job manager will keep answering the same job. */ public synchronized IJob currentJob() { if (this.enableCount > 0 && !this.awaitingJobs.isEmpty()) { return this.awaitingJobs.get(0); } return null; } /** * Answers the first job in the queue, or null if there is no job available, * independently on job manager enablement state. * Until the job has completed, the job manager will keep answering the same job. */ public synchronized IJob currentJobForced() { if (!this.awaitingJobs.isEmpty()) { return this.awaitingJobs.get(0); } return null; } public synchronized void disable() { this.enableCount--; if (VERBOSE) { trace("DISABLING background indexing"); //$NON-NLS-1$ } } /** * @return {@code true} if the job manager is enabled */ public synchronized boolean isEnabled() { return this.enableCount > 0; } /** * Remove the index from cache for a given project. * Passing null as a job family discards them all. */ public void discardJobs(String jobFamily) { if (VERBOSE) { trace("DISCARD background job family - " + jobFamily); //$NON-NLS-1$ } try { IJob currentJob; // cancel current job if it belongs to the given family synchronized(this){ currentJob = currentJob(); disable(); } if (currentJob != null && (jobFamily == null || currentJob.belongsTo(jobFamily))) { currentJob.cancel(); synchronized (this) { // wait until current active job has finished while (getProcessingThread() != null && this.executing){ try { if (VERBOSE) { trace("-> waiting end of current background job - " + currentJob); //$NON-NLS-1$ } this.wait(50); } catch(InterruptedException e){ // ignore } } } } synchronized(this) { Iterator it = this.awaitingJobs.iterator(); boolean notify = false; while (it.hasNext()) { currentJob = it.next(); if (jobFamily == null || currentJob.belongsTo(jobFamily)) { if (VERBOSE) { trace("-> discarding background job - " + currentJob); //$NON-NLS-1$ } currentJob.cancel(); it.remove(); notify = true; } } if (notify){ notifyAll(); // notify waiters for awaitingJobsCount() } } } finally { enable(); } if (VERBOSE) { trace("DISCARD DONE with background job family - " + jobFamily); //$NON-NLS-1$ } } public synchronized void enable() { this.enableCount++; if (VERBOSE) { trace("ENABLING background indexing"); //$NON-NLS-1$ } notifyAll(); // wake up the background thread if it is waiting (context must be synchronized) } protected synchronized boolean isJobWaiting(IJob request) { if(this.awaitingJobs.size() <= 1) { return false; } return hasPendingJobMatching(request::equals); } protected synchronized boolean hasPendingJobMatching(Predicate request) { int awaitingJobsCount = awaitingJobsCount(); if (awaitingJobsCount <= 1) { return false; } // Start at the end and go backwards ListIterator iterator = this.awaitingJobs.listIterator(awaitingJobsCount); IJob first = this.awaitingJobs.get(0); while (iterator.hasPrevious()) { IJob job = iterator.previous(); // don't check first job, as it may have already started if(job == first) { break; } if (request.test(job)) { return true; } } return false; } /** * Advance to the next available job, once the current one has been completed. * Note: clients awaiting until the job count is zero are still waiting at this point. */ protected synchronized void moveToNextJob() { //if (!enabled) return; if (!this.awaitingJobs.isEmpty()) { this.awaitingJobs.remove(0); if (awaitingJobsCount() == 0) { synchronized (this) { this.notifyAll(); } } } } /** * When idle, give chance to do something */ protected abstract void notifyIdle(long idlingMilliSeconds); /** * This API is allowing to run one job in concurrence with background processing. * Indeed since other jobs are performed in background, resource sharing might be * an issue.Therefore, this functionality allows a given job to be run without * colliding with background ones. * Note: multiple thread might attempt to perform concurrent jobs at the same time, * and should synchronize (it is deliberately left to clients to decide whether * concurrent jobs might interfere or not. In general, multiple read jobs are ok). * * Waiting policy can be: * IJobConstants.ForceImmediateSearch * IJobConstants.CancelIfNotReadyToSearch * IJobConstants.WaitUntilReadyToSearch */ public boolean performConcurrentJob(IJob searchJob, int waitingPolicy, IProgressMonitor monitor) { if (VERBOSE) { trace("STARTING concurrent job - " + searchJob); //$NON-NLS-1$ } searchJob.ensureReadyToRun(); boolean status = IJob.FAILED; try { SubMonitor subMonitor = SubMonitor.convert(monitor); if (awaitingJobsCount() > 0) { if (VERBOSE) { trace("-> NOT READY - " + awaitingJobsCount() + " awaiting jobs - " + searchJob);//$NON-NLS-1$ //$NON-NLS-2$ } switch (waitingPolicy) { case IJob.ForceImmediate : if (VERBOSE) { trace("-> NOT READY - forcing immediate - " + searchJob);//$NON-NLS-1$ } try { disable(); // pause indexing status = searchJob.execute(subMonitor); } finally { enable(); } if (VERBOSE) { trace("FINISHED concurrent job - " + searchJob); //$NON-NLS-1$ } return status; case IJob.CancelIfNotReady : if (VERBOSE) { trace("-> NOT READY - cancelling - " + searchJob); //$NON-NLS-1$ trace("CANCELED concurrent job - " + searchJob); //$NON-NLS-1$ } throw new OperationCanceledException(); case IJob.WaitUntilReady : int totalWork = 1000; SubMonitor waitMonitor = subMonitor.setWorkRemaining(10).split(8).setWorkRemaining(totalWork); // use local variable to avoid potential NPE (see bug 20435 NPE when searching java method // and bug 42760 NullPointerException in JobManager when searching) Thread t = getProcessingThread(); int originalPriority = t == null ? -1 : t.getPriority(); try { if (t != null) t.setPriority(Thread.currentThread().getPriority()); this.awaitingClients.incrementAndGet(); IJob previousJob = null; int awaitingJobsCount; int lastJobsCount = totalWork; float lastWorked = 0; float totalWorked = 0; while ((awaitingJobsCount = awaitingJobsCount()) > 0) { if (waitMonitor.isCanceled() || getProcessingThread() == null) throw new OperationCanceledException(); boolean shouldDisable = false; IJob currentJob = currentJobForced(); if (currentJob != null) { if (!isEnabled()) { if (VERBOSE) { trace("-> NOT READY (" + this.enableCount //$NON-NLS-1$ + ") - enabling indexer to process " //$NON-NLS-1$ + awaitingJobsCount + " jobs - " + searchJob);//$NON-NLS-1$ } enable(); shouldDisable = true; } synchronized (this.idleMonitor) { this.idleMonitor.notifyAll(); // wake up idle sleepers } } if (currentJob != null && currentJob != previousJob) { if (VERBOSE) { trace("-> NOT READY - waiting until ready to process " + awaitingJobsCount + " awaiting jobs - " + searchJob);//$NON-NLS-1$ //$NON-NLS-2$ } String indexing = Messages.bind(Messages.jobmanager_filesToIndex, currentJob.getJobFamily(), Integer.toString(awaitingJobsCount)); waitMonitor.subTask(indexing); // ratio of the amount of work relative to the total work float ratio = awaitingJobsCount < totalWork ? 1 : ((float) totalWork) / awaitingJobsCount; if (lastJobsCount > awaitingJobsCount) { totalWorked += (lastJobsCount - awaitingJobsCount) * ratio; } else { // more jobs were added, just increment by the ratio totalWorked += ratio; } if (totalWorked - lastWorked >= 1) { waitMonitor.worked((int) (totalWorked - lastWorked)); lastWorked = totalWorked; } lastJobsCount = awaitingJobsCount; previousJob = currentJob; } synchronized (this) { if (awaitingJobsCount() > 0) { try { this.wait(50); // avoid Thread.sleep! wait is informed by notifyAll } catch (InterruptedException e) { // ignore } } } if (shouldDisable) { if (VERBOSE) { trace("-> NOT READY (" + this.enableCount + ") - disabling indexer again, still awaiting jobs: " //$NON-NLS-1$ //$NON-NLS-2$ + awaitingJobsCount + " - " + searchJob);//$NON-NLS-1$ } disable(); } } } finally { this.awaitingClients.decrementAndGet(); if (t != null && originalPriority > -1 && t.isAlive()) t.setPriority(originalPriority); } } } status = searchJob.execute(subMonitor); } finally { SubMonitor.done(monitor); if (VERBOSE) { trace("FINISHED concurrent job - " + searchJob); //$NON-NLS-1$ } } return status; } public abstract String processName(); /** * Schedules given job for execution is there is no equal jobs waiting in the queue already * * @see JobManager#isJobWaiting(IJob) * @param job * a job to schedule (or not) */ public synchronized void requestIfNotWaiting(IJob job) { if(!isJobWaiting(job)) { request(job); } } public synchronized void request(IJob job) { job.ensureReadyToRun(); // append the job to the list of ones to process later on this.awaitingJobs.add(job); if (VERBOSE) { trace("REQUEST background job - " + job); //$NON-NLS-1$ trace("AWAITING JOBS count: " + awaitingJobsCount()); //$NON-NLS-1$ } notifyAll(); // wake up the background thread if it is waiting } /** * Flush current state */ public void reset() { if (VERBOSE) { trace("Reset"); //$NON-NLS-1$ } Thread thread = getProcessingThread(); if (thread != null) { discardJobs(null); // discard all jobs } else { synchronized (this) { /* initiate background processing */ Thread t = new Thread(this::indexerLoop, processName()); t.setDaemon(true); // less prioritary by default, priority is raised if clients are actively waiting on it t.setPriority(Thread.NORM_PRIORITY-1); // https://bugs.eclipse.org/bugs/show_bug.cgi?id=296343 // set the context loader to avoid leaking the current context loader t.setContextClassLoader(this.getClass().getClassLoader()); t.start(); this.processingThread = t; } } } /** * Infinite loop performing resource indexing */ void indexerLoop() { boolean cacheZipFiles = false; Long idlingStart = null; activateProcessing(); try { class ProgressJob extends Job { ProgressJob(String name) { super(name); } @Override protected IStatus run(IProgressMonitor monitor) { IJob job = currentJob(); while (!monitor.isCanceled() && job != null) { String taskName = new StringBuilder(Messages.jobmanager_indexing) .append(Messages.bind(Messages.jobmanager_filesToIndex, job.getJobFamily(), Integer.toString(awaitingJobsCount()))) .toString(); monitor.subTask(taskName); setName(taskName); synchronized (JobManager.this) { if (currentJob() != null) { try { JobManager.this.wait(500); } catch (InterruptedException e) { // ignore } } } job = currentJob(); } //make sure next index job will schedule new ProgressJob: JobManager.this.progressJob = null; return Status.OK_STATUS; } } this.progressJob = null; while (getProcessingThread() != null) { try { IJob job; synchronized (this) { // handle shutdown case when notifyAll came before the wait but after the while loop was entered if (getProcessingThread() == null) continue; // must check for new job inside this sync block to avoid timing hole if ((job = currentJob()) == null) { Job pJob = this.progressJob; if (pJob != null) { pJob.cancel(); this.progressJob = null; } if (idlingStart == null) { idlingStart = System.nanoTime(); } else { this.wait(); // wait until a new job is posted or disabled indexer is enabled again } } } if (job == null) { // don't call notifyIdle() within synchronized block or it may deadlock: notifyIdle((System.nanoTime() - idlingStart) / 1_000_000); if (currentJob() != null) { // notifyIdle() may have requested new job continue; } if (cacheZipFiles) { JavaModelManager.getJavaModelManager().flushZipFiles(this); cacheZipFiles = false; } // just woke up, delay before processing any new jobs, allow some time for the active thread to finish synchronized (this.idleMonitor) { this.idleMonitor.wait(500); // avoid sleep fixed time } continue; } idlingStart = null; if (VERBOSE) { trace(awaitingJobsCount() + " awaiting jobs"); //$NON-NLS-1$ trace("STARTING background job - " + job); //$NON-NLS-1$ } try { this.executing = true; if (this.progressJob == null) { ProgressJob pJob = new ProgressJob(Messages.bind(Messages.jobmanager_indexing, "", "")); //$NON-NLS-1$ //$NON-NLS-2$ pJob.setPriority(Job.LONG); pJob.setSystem(true); pJob.schedule(); this.progressJob = pJob; } if (!cacheZipFiles) { JavaModelManager.getJavaModelManager().cacheZipFiles(this); cacheZipFiles = true; } job.execute(null); // may enqueue a new job } finally { this.executing = false; if (VERBOSE) { trace("FINISHED background job - " + job); //$NON-NLS-1$ } moveToNextJob(); if (this.awaitingClients.get() == 0 && job.waitNeeded()) { if (VERBOSE) { trace("WAITING after job - " + job); //$NON-NLS-1$ } synchronized (this.idleMonitor) { this.idleMonitor.wait(5); // avoid sleep fixed time } } } } catch (InterruptedException e) { // background indexing was interrupted } } } catch (ThreadDeath e) { // do not restart throw e; } catch (RuntimeException|Error e) { if (getProcessingThread() != null) { // if not shutting down // log exception Util.log(e, "Background Indexer Crash Recovery"); //$NON-NLS-1$ // keep job manager alive discardJobs(null); synchronized (this) { this.processingThread = null; } reset(); // this will fork a new thread with no waiting jobs, some indexes will be inconsistent } throw e; } finally { if (cacheZipFiles) { JavaModelManager.getJavaModelManager().flushZipFiles(this); cacheZipFiles = false; } } } /** * Stop background processing, and wait until the current job is completed before returning */ public void shutdown() { if (VERBOSE) { trace("Shutdown"); //$NON-NLS-1$ } disable(); discardJobs(null); // will wait until current executing job has completed Thread thread = getProcessingThread(); try { if (thread != null) { // see http://bugs.eclipse.org/bugs/show_bug.cgi?id=31858 synchronized (this) { this.processingThread = null; // mark the job manager as shutting down so that the thread will stop by itself notifyAll(); // ensure its awake so it can be shutdown } // in case processing thread is handling a job thread.join(); } Job job = this.progressJob; if (job != null) { job.cancel(); job.join(); } } catch (InterruptedException e) { // ignore } } @Override public synchronized String toString() { StringBuilder buffer = new StringBuilder(10); buffer.append("Enable count:").append(this.enableCount).append('\n'); //$NON-NLS-1$ int numJobs = this.awaitingJobs.size(); buffer.append("Jobs in queue:").append(numJobs).append('\n'); //$NON-NLS-1$ for (int i = 0; i < numJobs && i < 15; i++) { buffer.append(i).append(" - job["+i+"]: ").append(this.awaitingJobs.get(i)).append('\n'); //$NON-NLS-1$ //$NON-NLS-2$ } return buffer.toString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy