blob: 909de727ae082ca5348cd42b32a3883882f7f05d [file] [log] [blame]
// Copyright 2023 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.remote;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.RequestMetadata;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.GoogleLogger;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionCacheUtils;
import com.google.devtools.build.lib.actions.ActionLookupData;
import com.google.devtools.build.lib.actions.ActionLookupValue;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue;
import com.google.devtools.build.lib.actions.cache.ActionCache;
import com.google.devtools.build.lib.actions.cache.ActionCache.Entry;
import com.google.devtools.build.lib.actions.cache.ActionCache.Entry.SerializableTreeArtifactValue;
import com.google.devtools.build.lib.concurrent.ExecutorUtil;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.remote.LeaseService.LeaseExtension;
import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
import com.google.devtools.build.lib.remote.util.DigestUtil;
import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
import com.google.devtools.build.lib.skyframe.ActionExecutionValue;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.skyframe.MemoizingEvaluator;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import javax.annotation.Nullable;
/** A {@link LeaseExtension} implementation that uses REAPI. */
public class RemoteLeaseExtension implements LeaseExtension {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final Predicate<SkyKey> ACTION_FILTER =
SkyFunctionName.functionIs(SkyFunctions.ACTION_EXECUTION);
private final ScheduledExecutorService scheduledExecutor =
Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder().setNameFormat("lease-extension-%d").build());
private final ReentrantLock lock = new ReentrantLock();
private final MemoizingEvaluator memoizingEvaluator;
@Nullable private final ActionCache actionCache;
private final RemoteCache remoteCache;
private final Duration remoteCacheTtl;
private final RemoteActionExecutionContext context;
public RemoteLeaseExtension(
MemoizingEvaluator memoizingEvaluator,
@Nullable ActionCache actionCache,
String buildRequestId,
String commandId,
RemoteCache remoteCache,
Duration remoteCacheTtl) {
this.memoizingEvaluator = memoizingEvaluator;
this.actionCache = actionCache;
this.remoteCache = remoteCache;
this.remoteCacheTtl = remoteCacheTtl;
RequestMetadata requestMetadata =
TracingMetadataUtils.buildMetadata(buildRequestId, commandId, "lease-extension", null);
this.context = RemoteActionExecutionContext.create(requestMetadata);
}
@Override
public void start() {
// Immediately extend leases for outputs that are already known to skyframe. For clean build,
// the set of outputs is empty. For incremental build, it contains outputs that were not
// invalidated after skyframe's dirtiness check.
var unused = scheduledExecutor.schedule(this::extendLeases, 0, MILLISECONDS);
}
private void extendLeases() {
// Acquire the lock to prevent multiple doExtendLeases() running.
lock.lock();
try (var silentCloseable = Profiler.instance().profile("doExtendLeases")) {
doExtendLeases();
} finally {
lock.unlock();
}
}
private void doExtendLeases() {
var valuesMap = memoizingEvaluator.getValues();
// We will extend leases for all known outputs so the earliest time when one output could be
// expired is (now + ttl).
var earliestExpiration = Instant.now().plus(remoteCacheTtl);
try {
for (var entry : valuesMap.entrySet()) {
SkyKey key = entry.getKey();
SkyValue value = entry.getValue();
if (value != null && ACTION_FILTER.test(key)) {
var action = getActionFromSkyKey(key);
var actionExecutionValue = (ActionExecutionValue) value;
var remoteFiles = collectRemoteFiles(actionExecutionValue);
if (!remoteFiles.isEmpty()) {
// Lease extensions are performed on action basis, not by collecting all outputs and
// issue one giant `FindMissingBlobs` call to avoid increasing memory footprint. Since
// this happens in the background, increased network calls are acceptable.
try (var silentCloseable1 = Profiler.instance().profile(action.describe())) {
extendLeaseForAction(action, remoteFiles, earliestExpiration.toEpochMilli());
}
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (Throwable e) {
logger.atWarning().withCause(e).log("Failed to extend the lease");
}
// Only extend the leases again when one of the outputs is about to expire.
var now = Instant.now();
Duration delay;
if (earliestExpiration.isAfter(now)) {
delay = Duration.between(now, earliestExpiration);
} else {
delay = Duration.ZERO;
}
var unused = scheduledExecutor.schedule(this::extendLeases, delay.toMillis(), MILLISECONDS);
}
private static boolean isRemoteMetadataWithTtl(FileArtifactValue metadata) {
return metadata.isRemote() && ((RemoteFileArtifactValue) metadata).getExpireAtEpochMilli() >= 0;
}
private ImmutableList<Map.Entry<? extends Artifact, FileArtifactValue>> collectRemoteFiles(
ActionExecutionValue actionExecutionValue) {
var result = ImmutableList.<Map.Entry<? extends Artifact, FileArtifactValue>>builder();
for (var entry : actionExecutionValue.getAllFileValues().entrySet()) {
if (isRemoteMetadataWithTtl(entry.getValue())) {
result.add(entry);
}
}
for (var treeMetadata : actionExecutionValue.getAllTreeArtifactValues().values()) {
for (var entry : treeMetadata.getChildValues().entrySet()) {
if (isRemoteMetadataWithTtl(entry.getValue())) {
result.add(entry);
}
}
}
return result.build();
}
/** Returns {@code true} iff the outputs of the action */
private void extendLeaseForAction(
Action action,
ImmutableList<Map.Entry<? extends Artifact, FileArtifactValue>> remoteFiles,
long expireAtEpochMilli)
throws IOException, InterruptedException {
ImmutableSet<Digest> missingDigests;
try (var silentCloseable = Profiler.instance().profile("findMissingDigests")) {
// We assume remote server will extend the leases for all referenced blobs by a
// FindMissingBlobs call.
missingDigests =
getFromFuture(
remoteCache.findMissingDigests(
context,
Iterables.transform(
remoteFiles, remoteFile -> buildDigest(remoteFile.getValue()))));
}
var token = getActionCacheToken(action);
for (var remoteFile : remoteFiles) {
var artifact = remoteFile.getKey();
var metadata = (RemoteFileArtifactValue) remoteFile.getValue();
// Only extend the lease for the remote output if it is still alive remotely.
if (!missingDigests.contains(buildDigest(metadata))) {
metadata.extendExpireAtEpochMilli(expireAtEpochMilli);
if (token != null) {
if (artifact instanceof TreeFileArtifact treeFileArtifact) {
token.extendOutputTreeFile(treeFileArtifact, expireAtEpochMilli);
} else {
token.extendOutputFile(artifact, expireAtEpochMilli);
}
}
}
}
if (actionCache != null && token != null && token.dirty) {
// Only update the action cache entry if the token was updated because it usually involves
// serialization.
actionCache.put(token.key, token.entry);
}
}
@Override
public void stop() {
if (ExecutorUtil.uninterruptibleShutdownNow(scheduledExecutor)) {
Thread.currentThread().interrupt();
}
}
private static Digest buildDigest(FileArtifactValue metadata) {
return DigestUtil.buildDigest(metadata.getDigest(), metadata.getSize());
}
private Action getActionFromSkyKey(SkyKey key) throws InterruptedException {
var actionLookupData = (ActionLookupData) key.argument();
var actionLookupValue =
(ActionLookupValue)
checkNotNull(
memoizingEvaluator.getExistingValue(actionLookupData.getActionLookupKey()));
return actionLookupValue.getAction(actionLookupData.getActionIndex());
}
@Nullable
private ActionCacheToken getActionCacheToken(Action action) {
if (actionCache != null) {
var actionCacheEntryWithKey = ActionCacheUtils.getCacheEntryWithKey(actionCache, action);
if (actionCacheEntryWithKey != null) {
return new ActionCacheToken(
actionCacheEntryWithKey.getKey(), actionCacheEntryWithKey.getValue());
}
}
return null;
}
private static class ActionCacheToken {
final String key;
final ActionCache.Entry entry;
private boolean dirty;
ActionCacheToken(String key, Entry entry) {
this.key = key;
this.entry = entry;
}
void extendOutputFile(Artifact artifact, long expireAtEpochMilli) {
var metadata = entry.getOutputFile(artifact);
if (metadata != null) {
metadata.extendExpireAtEpochMilli(expireAtEpochMilli);
dirty = true;
}
}
void extendOutputTreeFile(TreeFileArtifact treeFile, long expireAtEpochMilli) {
SerializableTreeArtifactValue treeMetadata = entry.getOutputTree(treeFile.getParent());
if (treeMetadata != null) {
var metadata = treeMetadata.childValues().get(treeFile.getTreeRelativePathString());
if (metadata != null) {
metadata.extendExpireAtEpochMilli(expireAtEpochMilli);
dirty = true;
}
}
}
}
}