/*
* Copyright 2017 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.work;
import com.google.common.collect.Lists;
import org.gradle.api.Action;
import org.gradle.api.Describable;
import org.gradle.api.Transformer;
import org.gradle.api.specs.Spec;
import org.gradle.internal.resources.AbstractResourceLockRegistry;
import org.gradle.internal.resources.AbstractTrackedResourceLock;
import org.gradle.internal.resources.DefaultResourceLockCoordinationService;
import org.gradle.internal.resources.ExclusiveAccessResourceLock;
import org.gradle.internal.resources.ResourceLock;
import org.gradle.internal.resources.ResourceLockCoordinationService;
import org.gradle.internal.resources.ResourceLockState;
import org.gradle.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.gradle.internal.resources.DefaultResourceLockCoordinationService.*;
import static org.gradle.internal.resources.ResourceLockState.Disposition.*;
public class DefaultWorkerLeaseService implements WorkerLeaseService {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultWorkerLeaseService.class);
private final int maxWorkerCount;
private int counter = 1;
private final Root root = new Root();
private final ResourceLockCoordinationService coordinationService;
private final ProjectLockRegistry projectLockRegistry;
private final WorkerLeaseLockRegistry workerLeaseLockRegistry;
public DefaultWorkerLeaseService(ResourceLockCoordinationService coordinationService, boolean parallelEnabled, int maxWorkerCount) {
this.maxWorkerCount = maxWorkerCount;
this.coordinationService = coordinationService;
this.projectLockRegistry = new ProjectLockRegistry(coordinationService, parallelEnabled);
this.workerLeaseLockRegistry = new WorkerLeaseLockRegistry(coordinationService);
LOGGER.info("Using {} worker leases.", maxWorkerCount);
}
@Override
public int getMaxWorkerCount() {
return maxWorkerCount;
}
@Override
public WorkerLease getCurrentWorkerLease() {
Collection<? extends ResourceLock> operations = workerLeaseLockRegistry.getResourceLocksByCurrentThread();
if (operations.isEmpty()) {
throw new IllegalStateException("No worker lease associated with the current thread");
}
return (DefaultWorkerLease) operations.toArray()[operations.size() - 1];
}
private synchronized DefaultWorkerLease getWorkerLease(LeaseHolder parent) {
int workerId = counter++;
Thread ownerThread = Thread.currentThread();
return workerLeaseLockRegistry.getResourceLock(parent, workerId, ownerThread);
}
@Override
public DefaultWorkerLease getWorkerLease() {
Collection<? extends ResourceLock> operations = workerLeaseLockRegistry.getResourceLocksByCurrentThread();
LeaseHolder parent = operations.isEmpty() ? root : (DefaultWorkerLease) operations.toArray()[operations.size() - 1];
return getWorkerLease(parent);
}
@Override
public void stop() {
coordinationService.withStateLock(new Transformer<ResourceLockState.Disposition, ResourceLockState>() {
@Override
public ResourceLockState.Disposition transform(ResourceLockState resourceLockState) {
if (workerLeaseLockRegistry.hasOpenLocks()) {
throw new IllegalStateException("Some worker leases have not been marked as completed.");
}
if (projectLockRegistry.hasOpenLocks()) {
throw new IllegalStateException("Some project locks have not been unlocked.");
}
return FINISHED;
}
});
}
@Override
public ResourceLock getProjectLock(String gradlePath, String projectPath) {
return projectLockRegistry.getResourceLock(gradlePath, projectPath);
}
@Override
public void withoutProjectLock(Runnable runnable) {
final Collection<? extends ResourceLock> projectLocks = projectLockRegistry.getResourceLocksByCurrentThread();
withoutLocks(projectLocks.toArray(new ResourceLock[projectLocks.size()])).execute(runnable);
}
@Override
public Executor withLocks(final ResourceLock... locks) {
return new Executor() {
@Override
public void execute(Runnable runnable) {
coordinationService.withStateLock(lock(locks));
try {
runnable.run();
} finally {
coordinationService.withStateLock(unlock(locks));
}
}
};
}
@Override
public Executor withoutLocks(final ResourceLock... locks) {
return new Executor() {
@Override
public void execute(Runnable runnable) {
if (!allLockedByCurrentThread(locks)) {
throw new IllegalStateException("Not all of the locks specified are currently held by the current thread. This could lead to orphaned locks.");
}
coordinationService.withStateLock(unlock(locks));
try {
runnable.run();
} finally {
if (!coordinationService.withStateLock(tryLock(locks))) {
WorkerLease workerLease = getCurrentWorkerLease();
List<ResourceLock> allLocks = Lists.newArrayList();
allLocks.add(workerLease);
allLocks.addAll(Arrays.asList(locks));
coordinationService.withStateLock(unlock(workerLease));
coordinationService.withStateLock(lock(allLocks));
}
}
}
};
}
private boolean allLockedByCurrentThread(final ResourceLock... locks) {
final AtomicBoolean allLocked = new AtomicBoolean();
coordinationService.withStateLock(new Transformer<ResourceLockState.Disposition, ResourceLockState>() {
@Override
public ResourceLockState.Disposition transform(ResourceLockState resourceLockState) {
allLocked.set(CollectionUtils.every(Arrays.asList(locks), new Spec<ResourceLock>() {
@Override
public boolean isSatisfiedBy(ResourceLock lock) {
return lock.isLockedByCurrentThread();
}
}));
return FINISHED;
}
});
return allLocked.get();
}
private static class ProjectLockRegistry extends AbstractResourceLockRegistry<ExclusiveAccessResourceLock> {
private final boolean parallelEnabled;
ProjectLockRegistry(ResourceLockCoordinationService coordinationService, boolean parallelEnabled) {
super(coordinationService);
this.parallelEnabled = parallelEnabled;
}
ResourceLock getResourceLock(String gradlePath, String projectPath) {
String displayName = projectPath;
if (!parallelEnabled) {
displayName = gradlePath;
}
return getOrRegisterResourceLock(displayName, new ResourceLockProducer<ExclusiveAccessResourceLock>() {
@Override
public ExclusiveAccessResourceLock create(String displayName, ResourceLockCoordinationService coordinationService, Action<ResourceLock> lockAction, Action<ResourceLock> unlockAction) {
return new ExclusiveAccessResourceLock(displayName, coordinationService, lockAction, unlockAction);
}
});
}
}
private class WorkerLeaseLockRegistry extends AbstractResourceLockRegistry<DefaultWorkerLease> {
WorkerLeaseLockRegistry(ResourceLockCoordinationService coordinationService) {
super(coordinationService);
}
DefaultWorkerLease getResourceLock(final LeaseHolder parent, int workerId, final Thread ownerThread) {
String displayName = parent.getDisplayName() + '.' + workerId;
return getOrRegisterResourceLock(displayName, new ResourceLockProducer<DefaultWorkerLease>() {
@Override
public DefaultWorkerLease create(String displayName, ResourceLockCoordinationService coordinationService, Action<ResourceLock> lockAction, Action<ResourceLock> unlockAction) {
return new DefaultWorkerLease(displayName, coordinationService, lockAction, unlockAction, parent, ownerThread);
}
});
}
}
private interface LeaseHolder extends Describable {
boolean grantLease();
void releaseLease();
}
private class Root implements LeaseHolder {
int leasesInUse;
public String getDisplayName() {
return "root";
}
@Override
public boolean grantLease() {
if (leasesInUse >= maxWorkerCount) {
return false;
}
leasesInUse++;
return true;
}
@Override
public void releaseLease() {
leasesInUse--;
}
}
private class DefaultWorkerLease extends AbstractTrackedResourceLock implements LeaseHolder, WorkerLeaseCompletion, WorkerLease {
private final LeaseHolder parent;
private final Thread ownerThread;
int children;
boolean active;
public DefaultWorkerLease(String displayName, ResourceLockCoordinationService coordinationService, Action<ResourceLock> lockAction, Action<ResourceLock> unlockAction, LeaseHolder parent, Thread ownerThread) {
super(displayName, coordinationService, lockAction, unlockAction);
this.parent = parent;
this.ownerThread = ownerThread;
}
@Override
protected boolean doIsLocked() {
return active;
}
@Override
protected boolean doIsLockedByCurrentThread() {
return active && Thread.currentThread() == ownerThread;
}
@Override
protected boolean acquireLock() {
if (parent.grantLease()) {
active = true;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Worker lease {} started ({} worker(s) in use).", getDisplayName(), root.leasesInUse);
}
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Build operation {} could not be started ({} worker(s) in use).", getDisplayName(), root.leasesInUse);
}
}
return active;
}
@Override
protected void releaseLock() {
if (Thread.currentThread() != ownerThread) {
// Not implemented - not yet required. Please implement if required
throw new UnsupportedOperationException("Must complete operation from owner thread.");
}
parent.releaseLease();
active = false;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Worker lease {} completed ({} worker(s) in use)", getDisplayName(), root.leasesInUse);
}
if (children != 0) {
throw new IllegalStateException("Some child operations have not yet completed.");
}
}
@Override
public boolean grantLease() {
if (children == 0 || root.grantLease()) {
children++;
return true;
}
return false;
}
@Override
public void releaseLease() {
children--;
if (children > 0) {
root.releaseLease();
}
}
WorkerLeaseCompletion start() {
coordinationService.withStateLock(lock(this));
return this;
}
@Override
public WorkerLease createChild() {
return getWorkerLease(this);
}
@Override
public WorkerLeaseCompletion startChild() {
return getWorkerLease(this).start();
}
@Override
public void leaseFinish() {
coordinationService.withStateLock(DefaultResourceLockCoordinationService.unlock(this));
}
}
}