package jenkins.util; import com.google.common.util.concurrent.SettableFuture; import hudson.remoting.AtmostOneThreadExecutor; import hudson.util.DaemonThreadFactory; import hudson.util.NamingThreadFactory; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; /** * {@link Executor}-like class that executes a single task repeatedly, in such a way that a single execution * can cover multiple pending queued requests. * * <p> * This is akin to doing laundry — when you put a dirty cloth into the laundry box, you mentally "schedule" * a laundry task, regardless of whether there already is some cloths in the box or not. When you later actually get around * doing laundry, you wash all the dirty cloths in the box, not just your cloths. And if someone brings * more dirty cloths while a washer and dryer are in operation, the person has to mentally "schedule" the task * and run the machines another time later, as the current batch is already in progress. * * <p> * Since this class collapses multiple submitted tasks into just one run, it only makes sense when everyone * submits the same task. Thus {@link #submit()} method does not take {@link Callable} as a parameter, * instead you pass that in the constructor. * * * <h2>Implementation</h2> * <p> * This instance has two independent states. One is {@link #pending}, which indicates that * the task execution is requested but not yet scheduled. The other is {@link #inprogress}, * which indicates that the task execution is scheduled but not yet completed. * * <p> * All the internal state transition is guarded by the monitor of 'this'. {@link #pending} * is non-null only if {@link #inprogress} is non-null. * * @author Kohsuke Kawaguchi * @see AtmostOneThreadExecutor */ public class AtmostOneTaskExecutor<V> { /** * The actual executor that executes {@link #task} */ private final ExecutorService base; /** * Task to be executed. */ private final Callable<V> task; /** * If a task is already submitted and pending execution, non-null. * Guarded by "synchronized(this)" */ private SettableFuture<V> pending; private SettableFuture<V> inprogress; public AtmostOneTaskExecutor(ExecutorService base, Callable<V> task) { this.base = base; this.task = task; } public AtmostOneTaskExecutor(Callable<V> task) { this(new AtmostOneThreadExecutor(new NamingThreadFactory( new DaemonThreadFactory(), String.format("AtmostOneTaskExecutor[%s]", task) )), task ); } public synchronized Future<V> submit() { if (pending==null) { pending = SettableFuture.create(); maybeRun(); } return pending; } /** * If {@link #pending} is non-null (meaning someone requested the task to be kicked), * but {@link #inprogress} is null (meaning none is executing right now), * get one going. */ private synchronized void maybeRun() { if (inprogress==null && pending!=null) { base.submit(new Callable<Void>() { @Override public Void call() throws Exception { synchronized (AtmostOneTaskExecutor.this) { // everyone who submits after this should form a next batch inprogress = pending; pending = null; } try { inprogress.set(task.call()); } catch (Throwable t) { inprogress.setException(t); } finally { synchronized (AtmostOneTaskExecutor.this) { // if next one is pending, get that scheduled inprogress = null; maybeRun(); } } return null; } }); } } }