/*
* 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.caching.internal;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import org.gradle.StartParameter;
import org.gradle.api.internal.file.TemporaryFileProvider;
import org.gradle.api.internal.tasks.GeneratedSubclasses;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.caching.BuildCacheService;
import org.gradle.caching.BuildCacheServiceFactory;
import org.gradle.caching.configuration.BuildCache;
import org.gradle.caching.configuration.internal.BuildCacheConfigurationInternal;
import org.gradle.internal.Cast;
import org.gradle.internal.operations.BuildOperationContext;
import org.gradle.internal.operations.BuildOperationExecutor;
import org.gradle.internal.operations.CallableBuildOperation;
import org.gradle.internal.progress.BuildOperationDescriptor;
import org.gradle.internal.reflect.Instantiator;
import org.gradle.util.Path;
import org.gradle.util.SingleMessageLogger;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class BuildCacheServiceProvider {
private static final Logger LOGGER = Logging.getLogger(BuildCacheServiceProvider.class);
private static final int MAX_ERROR_COUNT_FOR_BUILD_CACHE = 3;
private final BuildCacheConfigurationInternal buildCacheConfiguration;
private final BuildOperationExecutor buildOperationExecutor;
private final Instantiator instantiator;
private final StartParameter startParameter;
private final TemporaryFileProvider temporaryFileProvider;
@Inject
public BuildCacheServiceProvider(BuildCacheConfigurationInternal buildCacheConfiguration, StartParameter startParameter, Instantiator instantiator, BuildOperationExecutor buildOperationExecutor, TemporaryFileProvider temporaryFileProvider) {
this.buildCacheConfiguration = buildCacheConfiguration;
this.startParameter = startParameter;
this.instantiator = instantiator;
this.buildOperationExecutor = buildOperationExecutor;
this.temporaryFileProvider = temporaryFileProvider;
}
public BuildCacheService createBuildCacheService(final Path buildIdentityPath) {
return buildOperationExecutor.call(new CallableBuildOperation<BuildCacheService>() {
@Override
public BuildCacheService call(BuildOperationContext context) {
if (!startParameter.isBuildCacheEnabled()) {
context.setResult(FinalizeBuildCacheConfigurationBuildOperationType.ResultImpl.disabled());
return new NoOpBuildCacheService();
}
if (startParameter.isBuildCacheEnabled()) {
SingleMessageLogger.incubatingFeatureUsed("Build cache");
}
BuildCache local = buildCacheConfiguration.getLocal();
BuildCache remote = buildCacheConfiguration.getRemote();
boolean localEnabled = local != null && local.isEnabled();
boolean remoteEnabled = remote != null && remote.isEnabled();
if (remoteEnabled && startParameter.isOffline()) {
remoteEnabled = false;
LOGGER.warn("Remote build cache is disabled when running with --offline.");
}
DescribedBuildCacheService localDescribedService = localEnabled
? createRawBuildCacheService(local, "local", buildIdentityPath)
: null;
DescribedBuildCacheService remoteDescribedService = remoteEnabled
? createRawBuildCacheService(remote, "remote", buildIdentityPath)
: null;
context.setResult(new FinalizeBuildCacheConfigurationBuildOperationType.ResultImpl(
true,
local != null && local.isEnabled(),
remote != null && remote.isEnabled() && !startParameter.isOffline(),
localDescribedService == null ? null : localDescribedService.description,
remoteDescribedService == null ? null : remoteDescribedService.description
));
//noinspection ConstantConditions
RoleAwareBuildCacheService localRoleAware = localEnabled
? decorate(localDescribedService.service, "local")
: null;
//noinspection ConstantConditions
RoleAwareBuildCacheService remoteRoleAware = remoteEnabled
? decorate(remoteDescribedService.service, "remote")
: null;
if (localEnabled && remoteEnabled) {
return new DispatchingBuildCacheService(localRoleAware, local.isPush(), remoteRoleAware, remote.isPush(), temporaryFileProvider);
} else if (localEnabled) {
return preventPushIfNecessary(localRoleAware, local.isPush());
} else if (remoteEnabled) {
return preventPushIfNecessary(remoteRoleAware, remote.isPush());
} else if (!startParameter.isBuildCacheEnabled()) {
return new NoOpBuildCacheService();
} else {
LOGGER.warn("Task output caching is enabled, but no build caches are configured or enabled.");
return new NoOpBuildCacheService();
}
}
@Override
public BuildOperationDescriptor.Builder description() {
return BuildOperationDescriptor.displayName("Finalize build cache configuration")
.details(new FinalizeBuildCacheConfigurationBuildOperationType.DetailsImpl());
}
});
}
private static RoleAwareBuildCacheService preventPushIfNecessary(RoleAwareBuildCacheService buildCacheService, boolean pushEnabled) {
return pushEnabled ? buildCacheService : preventPush(buildCacheService);
}
private static PushOrPullPreventingBuildCacheServiceDecorator preventPush(RoleAwareBuildCacheService buildCacheService) {
return new PushOrPullPreventingBuildCacheServiceDecorator(true, false, buildCacheService);
}
private RoleAwareBuildCacheService decorate(BuildCacheService rawService, String role) {
RoleAwareBuildCacheService decoratedService = new BuildCacheServiceWithRole(role, rawService);
decoratedService = new BuildOperationFiringBuildCacheServiceDecorator(buildOperationExecutor, decoratedService);
decoratedService = new ShortCircuitingErrorHandlerBuildCacheServiceDecorator(MAX_ERROR_COUNT_FOR_BUILD_CACHE, startParameter, decoratedService);
return decoratedService;
}
private <T extends BuildCache> DescribedBuildCacheService createRawBuildCacheService(final T configuration, String role, Path buildIdentityPath) {
Class<? extends BuildCacheServiceFactory<T>> castFactoryType = Cast.uncheckedCast(
buildCacheConfiguration.getBuildCacheServiceFactoryType(configuration.getClass())
);
BuildCacheServiceFactory<T> factory = instantiator.newInstance(castFactoryType);
Describer describer = new Describer();
BuildCacheService service = factory.createBuildCacheService(configuration, describer);
ImmutableSortedMap<String, String> config = ImmutableSortedMap.copyOf(describer.configParams);
BuildCacheDescription description = new BuildCacheDescription(configuration, describer.type, config);
logConfig(buildIdentityPath, role, description);
return new DescribedBuildCacheService(service, description);
}
private static void logConfig(Path buildIdentityPath, String role, BuildCacheDescription description) {
if (LOGGER.isLifecycleEnabled()) {
StringBuilder config = new StringBuilder();
boolean pullOnly = !description.isPush();
if (!description.config.isEmpty() || pullOnly) {
Map<String, String> configMap;
if (pullOnly) {
configMap = new LinkedHashMap<String, String>();
// Pull-only always comes first
configMap.put("pull-only", null);
configMap.putAll(description.config);
} else {
configMap = description.config;
}
config.append(" (");
Joiner.on(", ").appendTo(config, Iterables.transform(configMap.entrySet(), new Function<Map.Entry<String, String>, String>() {
@Override
public String apply(Map.Entry<String, String> input) {
if (input.getValue() == null) {
return input.getKey();
} else {
return input.getKey() + " = " + input.getValue();
}
}
}));
config.append(")");
}
String buildDescription;
if (buildIdentityPath.equals(Path.ROOT)) {
buildDescription = "the root build";
} else {
buildDescription = "build '" + buildIdentityPath + "'";
}
LOGGER.lifecycle("Using {} {} build cache for {}{}.",
role,
description.type == null ? description.className : description.type,
buildDescription,
config
);
}
}
private static class BuildCacheServiceWithRole extends ForwardingBuildCacheService implements RoleAwareBuildCacheService {
private final String role;
private final BuildCacheService delegate;
private BuildCacheServiceWithRole(String role, BuildCacheService delegate) {
this.role = role;
this.delegate = delegate;
}
@Override
protected BuildCacheService delegate() {
return delegate;
}
@Override
public String getRole() {
return role;
}
}
private static final class BuildCacheDescription implements FinalizeBuildCacheConfigurationBuildOperationType.ResultImpl.BuildCacheDescription {
private final String className;
private final boolean push;
private final String type;
private final ImmutableSortedMap<String, String> config;
private BuildCacheDescription(BuildCache buildCache, String type, ImmutableSortedMap<String, String> config) {
this.className = GeneratedSubclasses.unpack(buildCache.getClass()).getName();
this.push = buildCache.isPush();
this.type = type;
this.config = config;
}
public String getClassName() {
return className;
}
public boolean isPush() {
return push;
}
public String getType() {
return type;
}
public Map<String, String> getConfig() {
return config;
}
}
private static class Describer implements BuildCacheServiceFactory.Describer {
private String type;
private Map<String, String> configParams = new HashMap<String, String>();
@Override
public BuildCacheServiceFactory.Describer type(String type) {
if (type == null) {
throw new IllegalArgumentException("'type' argument cannot be null");
}
this.type = type;
return this;
}
@Override
public BuildCacheServiceFactory.Describer config(String name, String value) {
if (name == null) {
throw new IllegalArgumentException("'name' argument cannot be null");
}
if (value == null) {
throw new IllegalArgumentException("'value' argument cannot be null");
}
configParams.put(name, value);
return this;
}
}
private static class DescribedBuildCacheService {
private final BuildCacheService service;
private final BuildCacheDescription description;
private DescribedBuildCacheService(BuildCacheService service, BuildCacheDescription description) {
this.service = service;
this.description = description;
}
}
}