/* * Copyright 2015 the original author or authors. * * 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 org.gradle.internal.operations; import org.gradle.internal.UncheckedException; import org.gradle.internal.work.WorkerLeaseRegistry; import org.gradle.internal.work.WorkerLeaseService; import java.util.Collection; import java.util.Deque; import java.util.LinkedList; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; class DefaultBuildOperationQueue<T extends BuildOperation> implements BuildOperationQueue<T> { private enum State { Working, Finishing, Cancelled, Done } private final WorkerLeaseService workerLeases; private final WorkerLeaseRegistry.WorkerLease parentWorkerLease; private final Executor executor; private final QueueWorker<T> queueWorker; private String logLocation; // Lock protects the following state, using an intentionally simple locking strategy private final ReentrantLock lock = new ReentrantLock(); private final Condition workAvailable = lock.newCondition(); private final Condition workDone = lock.newCondition(); private State state = State.Working; private int workers; private final Deque<T> workQueue = new LinkedList<T>(); private final LinkedList<Throwable> failures = new LinkedList<Throwable>(); DefaultBuildOperationQueue(WorkerLeaseService workerLeases, ExecutorService executor, QueueWorker<T> queueWorker) { this.workerLeases = workerLeases; this.parentWorkerLease = workerLeases.getWorkerLease(); this.executor = executor; this.queueWorker = queueWorker; } @Override public void add(final T operation) { lock.lock(); try { if (state == State.Done) { throw new IllegalStateException("BuildOperationQueue cannot be reused once it has completed."); } if (state == State.Cancelled) { return; } workQueue.add(operation); workAvailable.signalAll(); if (workers == 0 || workers < workerLeases.getMaxWorkerCount()) { // TODO This could be more efficient, so that we only start a worker when there are none idle _and_ there is a worker lease available workers++; executor.execute(new WorkerRunnable()); } } finally { lock.unlock(); } } @Override public void cancel() { lock.lock(); try { if (state == State.Cancelled || state == State.Done) { return; } state = State.Cancelled; workQueue.clear(); workAvailable.signalAll(); } finally { lock.unlock(); } } public void waitForCompletion() throws MultipleBuildOperationFailures { lock.lock(); try { if (state == State.Done) { throw new IllegalStateException("Cannot wait for completion more than once."); } state = State.Finishing; workAvailable.signalAll(); while (workers > 0) { try { workDone.await(); } catch (InterruptedException e) { throw UncheckedException.throwAsUncheckedException(e); } } state = State.Done; if (!failures.isEmpty()) { throw new MultipleBuildOperationFailures(getFailureMessage(failures), failures, logLocation); } } finally { lock.unlock(); } } @Override public void setLogLocation(String logLocation) { this.logLocation = logLocation; } private static String getFailureMessage(Collection<? extends Throwable> failures) { if (failures.size() == 1) { return "A build operation failed."; } return "Multiple build operations failed."; } private class WorkerRunnable implements Runnable { @Override public void run() { T operation; while ((operation = waitForNextOperation()) != null) { runBatch(operation); } shutDown(); } private T waitForNextOperation() { lock.lock(); try { while (state == State.Working && workQueue.isEmpty()) { try { workAvailable.await(); } catch (InterruptedException e) { throw new UncheckedException(e); } } return getNextOperation(); } finally { lock.unlock(); } } private void runBatch(final T firstOperation) { workerLeases.withLocks(parentWorkerLease.createChild()).execute(new Runnable() { @Override public void run() { T operation = firstOperation; while (operation != null) { runOperation(operation); operation = getNextOperation(); } } }); } private T getNextOperation() { lock.lock(); try { return workQueue.pollFirst(); } finally { lock.unlock(); } } private void runOperation(T operation) { try { queueWorker.execute(operation); } catch (Throwable t) { addFailure(t); } } private void addFailure(Throwable failure) { lock.lock(); try { failures.add(failure); } finally { lock.unlock(); } } private void shutDown() { lock.lock(); try { workers--; workDone.signalAll(); } finally { lock.unlock(); } } } }