/* * Copyright 2015-present Facebook, Inc. * * 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 com.facebook.buck.artifact_cache; import com.facebook.buck.cli.BuckConfig; import com.facebook.buck.config.ConfigView; import com.facebook.buck.slb.SlbBuckConfig; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.immutables.BuckStyleImmutable; import com.facebook.buck.util.unit.SizeUnit; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; import org.immutables.value.Value; /** Represents configuration specific to the {@link ArtifactCache}s. */ public class ArtifactCacheBuckConfig implements ConfigView<BuckConfig> { private static final String CACHE_SECTION_NAME = "cache"; private static final String DEFAULT_DIR_CACHE_MODE = CacheReadMode.READWRITE.name(); // Names of the fields in a [cache*] section that describe a single HTTP cache. private static final String HTTP_URL_FIELD_NAME = "http_url"; private static final String HTTP_BLACKLISTED_WIFI_SSIDS_FIELD_NAME = "blacklisted_wifi_ssids"; private static final String HTTP_MODE_FIELD_NAME = "http_mode"; private static final String HTTP_TIMEOUT_SECONDS_FIELD_NAME = "http_timeout_seconds"; private static final String HTTP_READ_HEADERS_FIELD_NAME = "http_read_headers"; private static final String HTTP_WRITE_HEADERS_FIELD_NAME = "http_write_headers"; private static final String HTTP_CACHE_ERROR_MESSAGE_NAME = "http_error_message_format"; private static final String HTTP_MAX_STORE_SIZE = "http_max_store_size"; private static final String HTTP_THREAD_POOL_SIZE = "http_thread_pool_size"; private static final String HTTP_THREAD_POOL_KEEP_ALIVE_DURATION_MILLIS = "http_thread_pool_keep_alive_duration_millis"; private static final ImmutableSet<String> HTTP_CACHE_DESCRIPTION_FIELDS = ImmutableSet.of( HTTP_URL_FIELD_NAME, HTTP_BLACKLISTED_WIFI_SSIDS_FIELD_NAME, HTTP_MODE_FIELD_NAME, HTTP_TIMEOUT_SECONDS_FIELD_NAME, HTTP_READ_HEADERS_FIELD_NAME, HTTP_WRITE_HEADERS_FIELD_NAME, HTTP_CACHE_ERROR_MESSAGE_NAME, HTTP_MAX_STORE_SIZE); private static final String HTTP_MAX_FETCH_RETRIES = "http_max_fetch_retries"; private static final String DIR_FIELD = "dir"; private static final String DIR_MODE_FIELD = "dir_mode"; private static final String DIR_MAX_SIZE_FIELD = "dir_max_size"; private static final String DIR_CACHE_NAMES_FIELD_NAME = "dir_cache_names"; private static final ImmutableSet<String> DIR_CACHE_DESCRIPTION_FIELDS = ImmutableSet.of(DIR_FIELD, DIR_MODE_FIELD, DIR_MAX_SIZE_FIELD); private static final URI DEFAULT_HTTP_URL = URI.create("http://localhost:8080/"); private static final String DEFAULT_HTTP_CACHE_MODE = CacheReadMode.READWRITE.name(); private static final long DEFAULT_HTTP_CACHE_TIMEOUT_SECONDS = 3L; private static final String DEFAULT_HTTP_MAX_CONCURRENT_WRITES = "1"; private static final String DEFAULT_HTTP_WRITE_SHUTDOWN_TIMEOUT_SECONDS = "1800"; // 30 minutes private static final String DEFAULT_HTTP_CACHE_ERROR_MESSAGE = "{cache_name} cache encountered an error: {error_message}"; private static final int DEFAULT_HTTP_MAX_FETCH_RETRIES = 2; private static final String SERVED_CACHE_ENABLED_FIELD_NAME = "serve_local_cache"; private static final String DEFAULT_SERVED_CACHE_MODE = CacheReadMode.READONLY.name(); private static final String SERVED_CACHE_READ_MODE_FIELD_NAME = "served_local_cache_mode"; private static final String LOAD_BALANCING_TYPE = "load_balancing_type"; private static final LoadBalancingType DEFAULT_LOAD_BALANCING_TYPE = LoadBalancingType.SINGLE_SERVER; private static final long DEFAULT_HTTP_THREAD_POOL_SIZE = 5; private static final long DEFAULT_HTTP_THREAD_POOL_KEEP_ALIVE_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(1); private static final String TWO_LEVEL_CACHING_ENABLED_FIELD_NAME = "two_level_cache_enabled"; // Old name for "two_level_cache_minimum_size", remove eventually. private static final String TWO_LEVEL_CACHING_THRESHOLD_FIELD_NAME = "two_level_cache_threshold"; private static final String TWO_LEVEL_CACHING_MIN_SIZE_FIELD_NAME = "two_level_cache_minimum_size"; private static final String TWO_LEVEL_CACHING_MAX_SIZE_FIELD_NAME = "two_level_cache_maximum_size"; private static final long TWO_LEVEL_CACHING_MIN_SIZE_DEFAULT = 20 * 1024L; private static final String HYBRID_THRIFT_ENDPOINT = "hybrid_thrift_endpoint"; private static final String REPOSITORY = "repository"; private static final String DEFAULT_REPOSITORY = ""; private static final String SCHEDULE_TYPE = "schedule_type"; private static final String DEFAULT_SCHEDULE_TYPE = "none"; public enum LoadBalancingType { SINGLE_SERVER, CLIENT_SLB, } private final BuckConfig buckConfig; private final SlbBuckConfig slbConfig; public static ArtifactCacheBuckConfig of(BuckConfig delegate) { return new ArtifactCacheBuckConfig(delegate); } public ArtifactCacheBuckConfig(BuckConfig buckConfig) { this.buckConfig = buckConfig; this.slbConfig = new SlbBuckConfig(buckConfig, CACHE_SECTION_NAME); } @Override public BuckConfig getDelegate() { return buckConfig; } public String getRepository() { return buckConfig.getValue(CACHE_SECTION_NAME, REPOSITORY).orElse(DEFAULT_REPOSITORY); } public String getScheduleType() { return buckConfig.getValue(CACHE_SECTION_NAME, SCHEDULE_TYPE).orElse(DEFAULT_SCHEDULE_TYPE); } SlbBuckConfig getSlbConfig() { return slbConfig; } Optional<String> getHybridThriftEndpoint() { return buckConfig.getValue(CACHE_SECTION_NAME, HYBRID_THRIFT_ENDPOINT); } LoadBalancingType getLoadBalancingType() { return buckConfig .getEnum(CACHE_SECTION_NAME, LOAD_BALANCING_TYPE, LoadBalancingType.class) .orElse(DEFAULT_LOAD_BALANCING_TYPE); } public int getHttpMaxConcurrentWrites() { return Integer.valueOf( buckConfig .getValue(CACHE_SECTION_NAME, "http_max_concurrent_writes") .orElse(DEFAULT_HTTP_MAX_CONCURRENT_WRITES)); } public int getHttpWriterShutdownTimeout() { return Integer.valueOf( buckConfig .getValue(CACHE_SECTION_NAME, "http_writer_shutdown_timeout_seconds") .orElse(DEFAULT_HTTP_WRITE_SHUTDOWN_TIMEOUT_SECONDS)); } int getMaxFetchRetries() { return buckConfig .getInteger(CACHE_SECTION_NAME, HTTP_MAX_FETCH_RETRIES) .orElse(DEFAULT_HTTP_MAX_FETCH_RETRIES); } public boolean hasAtLeastOneWriteableCache() { return getHttpCacheEntries() .stream() .anyMatch(entry -> entry.getCacheReadMode().equals(CacheReadMode.READWRITE)); } String getHostToReportToRemoteCacheServer() { return buckConfig.getLocalhost(); } public ImmutableList<String> getArtifactCacheModesRaw() { // If there is a user-set value, even if it is `mode =`, use it. if (buckConfig.hasUserDefinedValue(CACHE_SECTION_NAME, "mode")) { return buckConfig.getListWithoutComments(CACHE_SECTION_NAME, "mode"); } // Otherwise, we default to using the directory cache. return ImmutableList.of("dir"); } ImmutableSet<ArtifactCacheMode> getArtifactCacheModes() { return getArtifactCacheModesRaw() .stream() .map( input -> { try { return ArtifactCacheMode.valueOf(input); } catch (IllegalArgumentException e) { throw new HumanReadableException( "Unusable %s.mode: '%s'", CACHE_SECTION_NAME, input); } }) .collect(MoreCollectors.toImmutableSet()); } Optional<DirCacheEntry> getServedLocalCache() { if (!getServingLocalCacheEnabled()) { return Optional.empty(); } return Optional.of( obtainDirEntryForName(Optional.empty()).withCacheReadMode(getServedLocalCacheReadMode())); } public ArtifactCacheEntries getCacheEntries() { ImmutableSet<DirCacheEntry> dirCacheEntries = getDirCacheEntries(); ImmutableSet<HttpCacheEntry> httpCacheEntries = getHttpCacheEntries(); Predicate<DirCacheEntry> isDirCacheEntryWriteable = dirCache -> dirCache.getCacheReadMode().isWritable(); // Enforce some sanity checks on the config: // - we don't want multiple writeable dir caches pointing to the same directory dirCacheEntries .stream() .filter(isDirCacheEntryWriteable) .collect(Collectors.groupingBy(DirCacheEntry::getCacheDir)) .forEach( (path, dirCachesPerPath) -> { if (dirCachesPerPath.size() > 1) { throw new HumanReadableException( "Multiple writeable dir caches defined for path %s. This is not supported.", path); } }); return ArtifactCacheEntries.builder() .setDirCacheEntries(dirCacheEntries) .setHttpCacheEntries(httpCacheEntries) .build(); } private ImmutableSet<HttpCacheEntry> getHttpCacheEntries() { if (getArtifactCacheModes().contains(ArtifactCacheMode.http) || legacyHttpCacheConfigurationFieldsPresent()) { return ImmutableSet.of(obtainHttpEntry()); } return ImmutableSet.of(); } private ImmutableSet<DirCacheEntry> getDirCacheEntries() { ImmutableSet.Builder<DirCacheEntry> result = ImmutableSet.builder(); ImmutableList<String> names = getDirCacheNames(); boolean implicitLegacyCache = names.isEmpty() && getArtifactCacheModes().contains(ArtifactCacheMode.dir); if (implicitLegacyCache || legacyDirCacheConfigurationFieldsPresent()) { result.add(obtainDirEntryForName(Optional.empty())); } for (String cacheName : names) { result.add(obtainDirEntryForName(Optional.of(cacheName))); } return result.build(); } // It's important that this number is greater than the `-j` parallelism, // as if it's too small, we'll overflow the reusable connection pool and // start spamming new connections. While this isn't the best location, // the other current option is setting this wherever we construct a `Build` // object and have access to the `-j` argument. However, since that is // created in several places leave it here for now. public long getThreadPoolSize() { return buckConfig .getLong(CACHE_SECTION_NAME, HTTP_THREAD_POOL_SIZE) .orElse(DEFAULT_HTTP_THREAD_POOL_SIZE); } long getThreadPoolKeepAliveDurationMillis() { return buckConfig .getLong(CACHE_SECTION_NAME, HTTP_THREAD_POOL_KEEP_ALIVE_DURATION_MILLIS) .orElse(DEFAULT_HTTP_THREAD_POOL_KEEP_ALIVE_DURATION_MILLIS); } boolean getTwoLevelCachingEnabled() { return buckConfig.getBooleanValue( CACHE_SECTION_NAME, TWO_LEVEL_CACHING_ENABLED_FIELD_NAME, false); } long getTwoLevelCachingMinimumSize() { return buckConfig .getValue(CACHE_SECTION_NAME, TWO_LEVEL_CACHING_MIN_SIZE_FIELD_NAME) .map(Optional::of) .orElse(buckConfig.getValue(CACHE_SECTION_NAME, TWO_LEVEL_CACHING_THRESHOLD_FIELD_NAME)) .map(SizeUnit::parseBytes) .orElse(TWO_LEVEL_CACHING_MIN_SIZE_DEFAULT); } Optional<Long> getTwoLevelCachingMaximumSize() { return buckConfig .getValue(CACHE_SECTION_NAME, TWO_LEVEL_CACHING_MAX_SIZE_FIELD_NAME) .map(SizeUnit::parseBytes); } private boolean getServingLocalCacheEnabled() { return buckConfig.getBooleanValue(CACHE_SECTION_NAME, SERVED_CACHE_ENABLED_FIELD_NAME, false); } private CacheReadMode getServedLocalCacheReadMode() { return getCacheReadMode( CACHE_SECTION_NAME, SERVED_CACHE_READ_MODE_FIELD_NAME, DEFAULT_SERVED_CACHE_MODE); } private CacheReadMode getCacheReadMode(String section, String fieldName, String defaultValue) { String cacheMode = buckConfig.getValue(section, fieldName).orElse(defaultValue); final CacheReadMode result; try { result = CacheReadMode.valueOf(cacheMode.toUpperCase()); } catch (IllegalArgumentException e) { throw new HumanReadableException("Unusable cache.%s: '%s'", fieldName, cacheMode); } return result; } private ImmutableMap<String, String> getCacheHeaders(String section, String fieldName) { ImmutableMap.Builder<String, String> headerBuilder = ImmutableMap.builder(); ImmutableList<String> rawHeaders = buckConfig.getListWithoutComments(section, fieldName, ';'); for (String rawHeader : rawHeaders) { List<String> splitHeader = Splitter.on(':').omitEmptyStrings().trimResults().splitToList(rawHeader); headerBuilder.put(splitHeader.get(0), splitHeader.get(1)); } return headerBuilder.build(); } private ImmutableList<String> getDirCacheNames() { return buckConfig.getListWithoutComments(CACHE_SECTION_NAME, DIR_CACHE_NAMES_FIELD_NAME); } private String getCacheErrorFormatMessage(String section, String fieldName, String defaultValue) { return buckConfig.getValue(section, fieldName).orElse(defaultValue); } private DirCacheEntry obtainDirEntryForName(Optional<String> cacheName) { final String section = Joiner.on('#').skipNulls().join(CACHE_SECTION_NAME, cacheName.orElse(null)); CacheReadMode readMode = getCacheReadMode(section, DIR_MODE_FIELD, DEFAULT_DIR_CACHE_MODE); String cacheDir = buckConfig.getLocalCacheDirectory(section); Path pathToCacheDir = buckConfig.resolvePathThatMayBeOutsideTheProjectFilesystem(Paths.get(cacheDir)); Preconditions.checkNotNull(pathToCacheDir); Optional<Long> maxSizeBytes = buckConfig.getValue(section, DIR_MAX_SIZE_FIELD).map(SizeUnit::parseBytes); return DirCacheEntry.builder() .setName(cacheName) .setCacheDir(pathToCacheDir) .setCacheReadMode(readMode) .setMaxSizeBytes(maxSizeBytes) .build(); } private HttpCacheEntry obtainHttpEntry() { HttpCacheEntry.Builder builder = HttpCacheEntry.builder(); builder.setUrl( buckConfig.getUrl(CACHE_SECTION_NAME, HTTP_URL_FIELD_NAME).orElse(DEFAULT_HTTP_URL)); builder.setTimeoutSeconds( buckConfig .getLong(CACHE_SECTION_NAME, HTTP_TIMEOUT_SECONDS_FIELD_NAME) .orElse(DEFAULT_HTTP_CACHE_TIMEOUT_SECONDS) .intValue()); builder.setReadHeaders(getCacheHeaders(CACHE_SECTION_NAME, HTTP_READ_HEADERS_FIELD_NAME)); builder.setWriteHeaders(getCacheHeaders(CACHE_SECTION_NAME, HTTP_WRITE_HEADERS_FIELD_NAME)); builder.setBlacklistedWifiSsids(getBlacklistedWifiSsids()); builder.setCacheReadMode( getCacheReadMode(CACHE_SECTION_NAME, HTTP_MODE_FIELD_NAME, DEFAULT_HTTP_CACHE_MODE)); builder.setErrorMessageFormat( getCacheErrorFormatMessage( CACHE_SECTION_NAME, HTTP_CACHE_ERROR_MESSAGE_NAME, DEFAULT_HTTP_CACHE_ERROR_MESSAGE)); builder.setMaxStoreSize(buckConfig.getLong(CACHE_SECTION_NAME, HTTP_MAX_STORE_SIZE)); return builder.build(); } public ImmutableSet<String> getBlacklistedWifiSsids() { return ImmutableSet.copyOf( buckConfig.getListWithoutComments( CACHE_SECTION_NAME, HTTP_BLACKLISTED_WIFI_SSIDS_FIELD_NAME)); } private boolean legacyHttpCacheConfigurationFieldsPresent() { for (String field : HTTP_CACHE_DESCRIPTION_FIELDS) { if (buckConfig.getValue(CACHE_SECTION_NAME, field).isPresent()) { return true; } } return false; } private boolean legacyDirCacheConfigurationFieldsPresent() { for (String field : DIR_CACHE_DESCRIPTION_FIELDS) { if (buckConfig.getValue(CACHE_SECTION_NAME, field).isPresent()) { return true; } } return false; } boolean getDirCacheRunsPropagationExperiment() { return buckConfig.getBooleanValue(CACHE_SECTION_NAME, "_exp_propagation", false); } boolean getDirCachePropagationExperimentRandomizedTrialForcedToBeControlGroup() { return buckConfig.getBooleanValue( CACHE_SECTION_NAME, "_exp_propagation_force_control_group", false); } public enum ArtifactCacheMode { dir, http, thrift_over_http, } @Value.Immutable @BuckStyleImmutable abstract static class AbstractArtifactCacheEntries { public abstract ImmutableSet<HttpCacheEntry> getHttpCacheEntries(); public abstract ImmutableSet<DirCacheEntry> getDirCacheEntries(); } @Value.Immutable @BuckStyleImmutable abstract static class AbstractDirCacheEntry { public abstract Optional<String> getName(); public abstract Path getCacheDir(); public abstract Optional<Long> getMaxSizeBytes(); public abstract CacheReadMode getCacheReadMode(); } @Value.Immutable @BuckStyleImmutable abstract static class AbstractHttpCacheEntry { public abstract URI getUrl(); public abstract int getTimeoutSeconds(); public abstract ImmutableMap<String, String> getReadHeaders(); public abstract ImmutableMap<String, String> getWriteHeaders(); public abstract CacheReadMode getCacheReadMode(); protected abstract ImmutableSet<String> getBlacklistedWifiSsids(); public abstract String getErrorMessageFormat(); public abstract Optional<Long> getMaxStoreSize(); // We're connected to a wifi hotspot that has been explicitly blacklisted from connecting to // a distributed cache. boolean isWifiUsableForDistributedCache(Optional<String> currentWifiSsid) { return !(currentWifiSsid.isPresent() && getBlacklistedWifiSsids().contains(currentWifiSsid.get())); } } }