diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelActionContextConsumer.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelActionContextConsumer.java index c995f78fe57cf7..9bf4f6376efbe5 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelActionContextConsumer.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelActionContextConsumer.java @@ -21,6 +21,7 @@ import com.google.devtools.build.lib.analysis.actions.FileWriteActionContext; import com.google.devtools.build.lib.bazel.rules.BazelStrategyModule.BazelExecutionOptions; import com.google.devtools.build.lib.exec.ActionContextConsumer; +import com.google.devtools.build.lib.exec.SpawnCache; import com.google.devtools.build.lib.rules.android.WriteAdbArgsActionContext; import com.google.devtools.build.lib.rules.cpp.CppCompileActionContext; import com.google.devtools.build.lib.rules.cpp.IncludeScanningContext; @@ -75,6 +76,7 @@ public Multimap, String> getActionContexts() { .put(IncludeScanningContext.class, "") .put(FileWriteActionContext.class, "") .put(WriteAdbArgsActionContext.class, "") + .put(SpawnCache.class, "") .build(); } } diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java index 25f4d0a99369f6..f0d17413c76660 100644 --- a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java +++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java @@ -137,6 +137,7 @@ private StrategyConverter(Iterable contextProviders) { for (ActionContext strategy : provider.getActionContexts()) { ExecutionStrategy annotation = strategy.getClass().getAnnotation(ExecutionStrategy.class); + // TODO(ulfjack): Don't silently ignore action contexts without annotation. if (annotation != null) { defaultClassMap.put(annotation.contextType(), strategy); diff --git a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java index 9ad2d468f3fedd..45322e471c4e82 100644 --- a/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategy.java @@ -14,6 +14,7 @@ package com.google.devtools.build.lib.exec; +import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.eventbus.EventBus; @@ -28,15 +29,19 @@ import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.Spawns; +import com.google.devtools.build.lib.exec.SpawnCache.CacheHandle; import com.google.devtools.build.lib.exec.SpawnResult.Status; import com.google.devtools.build.lib.exec.SpawnRunner.ProgressStatus; import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; import com.google.devtools.build.lib.rules.fileset.FilesetActionContext; import com.google.devtools.build.lib.util.CommandFailureUtils; import com.google.devtools.build.lib.util.io.FileOutErr; +import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.SortedMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -71,9 +76,29 @@ public void exec( SpawnExecutionPolicy policy = new SpawnExecutionPolicyImpl( spawn, actionExecutionContext, writeOutputFiles, timeout); + // TODO(ulfjack): Provide a way to disable the cache. We don't want the RemoteSpawnStrategy to + // check the cache twice. Right now that can't happen because this is hidden behind an + // experimental flag. + SpawnCache cache = actionExecutionContext.getContext(SpawnCache.class); + // In production, the getContext method guarantees that we never get null back. However, our + // integration tests don't set it up correctly, so cache may be null in testing. + if (cache == null || !Spawns.mayBeCached(spawn)) { + cache = SpawnCache.NO_CACHE; + } SpawnResult result; try { - result = spawnRunner.exec(spawn, policy); + try (CacheHandle cacheHandle = cache.lookup(spawn, policy)) { + if (cacheHandle.hasResult()) { + result = Preconditions.checkNotNull(cacheHandle.getResult()); + } else { + // Actual execution. + result = spawnRunner.exec(spawn, policy); + if (cacheHandle.willStore()) { + cacheHandle.store( + result, listExistingOutputFiles(spawn, actionExecutionContext.getExecRoot())); + } + } + } } catch (IOException e) { throw new EnvironmentalExecException("Unexpected IO error.", e); } @@ -91,6 +116,19 @@ public void exec( } } + private List listExistingOutputFiles(Spawn spawn, Path execRoot) { + ArrayList outputFiles = new ArrayList<>(); + for (ActionInput output : spawn.getOutputFiles()) { + Path outputPath = execRoot.getRelative(output.getExecPathString()); + // TODO(ulfjack): Store the actual list of output files in SpawnResult and use that instead + // of statting the files here again. + if (outputPath.exists()) { + outputFiles.add(outputPath); + } + } + return outputFiles; + } + private final class SpawnExecutionPolicyImpl implements SpawnExecutionPolicy { private final Spawn spawn; private final ActionExecutionContext actionExecutionContext; diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnCache.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnCache.java new file mode 100644 index 00000000000000..20ea4211a631ed --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnCache.java @@ -0,0 +1,175 @@ +// Copyright 2017 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.exec; + +import com.google.devtools.build.lib.actions.ActionContext; +import com.google.devtools.build.lib.actions.ExecException; +import com.google.devtools.build.lib.actions.ExecutionStrategy; +import com.google.devtools.build.lib.actions.Spawn; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; +import com.google.devtools.build.lib.vfs.Path; +import java.io.Closeable; +import java.io.IOException; +import java.util.Collection; +import java.util.NoSuchElementException; + +/** + * A cache that can lookup a {@link SpawnResult} given a {@link Spawn}, and can also upload the + * results of an executed spawn to the cache. + * + *

This is an experimental interface to implement caching with sandboxed local execution. + */ +public interface SpawnCache extends ActionContext { + /** A no-op implementation that has no result, and performs no upload. */ + public static CacheHandle NO_RESULT_NO_STORE = new CacheHandle() { + @Override + public boolean hasResult() { + return false; + } + + @Override + public SpawnResult getResult() { + throw new NoSuchElementException(); + } + + @Override + public boolean willStore() { + return false; + } + + @Override + public void store(SpawnResult result, Collection files) + throws InterruptedException, IOException { + // Do nothing. + } + + @Override + public void close() { + } + }; + + /** + * Helper method to create a {@link CacheHandle} from a successful {@link SpawnResult} instance. + */ + public static CacheHandle success(final SpawnResult result) { + return new CacheHandle() { + @Override + public boolean hasResult() { + return true; + } + + @Override + public SpawnResult getResult() { + return result; + } + + @Override + public boolean willStore() { + return false; + } + + @Override + public void store(SpawnResult result, Collection files) + throws InterruptedException, IOException { + throw new IllegalStateException(); + } + + @Override + public void close() { + } + }; + } + + /** A no-op spawn cache. */ + @ExecutionStrategy( + name = {"no-cache"}, + contextType = SpawnCache.class + ) + public static class NoSpawnCache implements SpawnCache { + @Override + public CacheHandle lookup(Spawn spawn, SpawnExecutionPolicy context) { + return SpawnCache.NO_RESULT_NO_STORE; + } + } + + /** A no-op implementation that has no results and performs no stores. */ + public static SpawnCache NO_CACHE = new NoSpawnCache(); + + /** + * This object represents both a successful and an unsuccessful cache lookup. If + * {@link #hasResult} returns true, then {@link #getResult} must successfully return a non-null + * instance (use the {@link #success} helper method). Otherwise {@link #getResult} should throw an + * {@link IllegalStateException}. + * + *

If {@link #hasResult} returns false, then {@link #store} may upload the result to the cache + * after successful execution. + * + *

Note that this interface extends {@link Closeable}, and callers must guarantee that + * {@link #close} is called on this entry (e.g., by using try-with-resources) to free up any + * acquired resources. + */ + interface CacheHandle extends Closeable { + /** Returns whether the cache lookup was successful. */ + boolean hasResult(); + + /** + * Returns the cached result. + * + * @throws NoSuchElementException if there is no result in this cache entry + */ + SpawnResult getResult(); + + /** + * Returns true if the store call will actually do work. Use this to avoid unnecessary work + * before store if it won't do anything. + */ + boolean willStore(); + + /** + * Called after successful {@link Spawn} execution, which may or may not store the result in the + * cache. + * + *

A cache may silently return from a failed store operation. We recommend to err on the side + * of raising an exception rather than returning silently, and to offer command-line flags to + * tweak this default policy as needed. + * + *

If the current thread is interrupted, then this method should return as quickly as + * possible with an {@link InterruptedException}. + */ + void store(SpawnResult result, Collection files) + throws InterruptedException, IOException; + } + + /** + * Perform a spawn lookup. This method is similar to {@link SpawnRunner#exec}, taking the same + * parameters and being allowed to throw the same exceptions. The intent for this method is to + * compute a cache lookup key for the given spawn, looking it up in an implementation-dependent + * cache (can be either on the local or remote machine), and returning a non-null + * {@link CacheHandle} instance. + * + *

If the lookup was successful, this method should write the cached outputs to their + * corresponding output locations in the output tree, as well as stdout and stderr, after + * notifying {@link SpawnExecutionPolicy#lockOutputFiles}. + * + *

If the lookup was unsuccessful, this method can return a {@link CacheHandle} instance that + * has no result, but uploads the results of the execution to the cache. The reason for a callback + * object is for the cache to store expensive intermediate values (such as the cache key) that are + * needed both for the lookup and the subsequent store operation. + * + *

Note that cache stores may be disabled, in which case the returned {@link CacheHandle} + * instance's {@link CacheHandle#store} is a no-op. + */ + CacheHandle lookup(Spawn spawn, SpawnExecutionPolicy context) + throws ExecException, IOException, InterruptedException; +} diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java index 8424fca0c2a53d..826de367c86328 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteActionContextProvider.java @@ -37,8 +37,6 @@ final class RemoteActionContextProvider extends ActionContextProvider { private final RemoteActionCache cache; private final GrpcRemoteExecutor executor; - private RemoteSpawnRunner spawnRunner; - RemoteActionContextProvider(CommandEnvironment env, @Nullable RemoteActionCache cache, @Nullable GrpcRemoteExecutor executor) { this.env = env; @@ -52,14 +50,19 @@ public Iterable getActionContexts() { checkNotNull(env.getOptions().getOptions(ExecutionOptions.class)); RemoteOptions remoteOptions = checkNotNull(env.getOptions().getOptions(RemoteOptions.class)); - spawnRunner = new RemoteSpawnRunner( - env.getExecRoot(), - remoteOptions, - createFallbackRunner(env), - executionOptions.verboseFailures, - cache, - executor); - return ImmutableList.of(new RemoteSpawnStrategy(spawnRunner)); + if (remoteOptions.experimentalRemoteSpawnCache) { + RemoteSpawnCache spawnCache = new RemoteSpawnCache(env.getExecRoot(), remoteOptions, cache); + return ImmutableList.of(spawnCache); + } else { + RemoteSpawnRunner spawnRunner = new RemoteSpawnRunner( + env.getExecRoot(), + remoteOptions, + createFallbackRunner(env), + executionOptions.verboseFailures, + cache, + executor); + return ImmutableList.of(new RemoteSpawnStrategy(spawnRunner)); + } } private static SpawnRunner createFallbackRunner(CommandEnvironment env) { @@ -79,9 +82,8 @@ private static SpawnRunner createFallbackRunner(CommandEnvironment env) { @Override public void executionPhaseEnding() { - if (spawnRunner != null) { - spawnRunner.close(); + if (cache != null) { + cache.close(); } - spawnRunner = null; } } diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java index 175b7b834d2ef2..8b6975257d06b7 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java @@ -218,6 +218,17 @@ public final class RemoteOptions extends OptionsBase { ) public double experimentalRemoteRetryJitter; + @Option( + name = "experimental_remote_spawn_cache", + defaultValue = "false", + category = "remote", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Whether to use the experimental spawn cache infrastructure for remote caching. " + + "Enabling this flag makes Bazel ignore any setting for remote_executor." + ) + public boolean experimentalRemoteSpawnCache; + public Platform parseRemotePlatformOverride() { if (experimentalRemotePlatformOverride != null) { Platform.Builder platformBuilder = Platform.newBuilder(); diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java new file mode 100644 index 00000000000000..5d5c3e8d16d42f --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java @@ -0,0 +1,132 @@ +// Copyright 2017 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 com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.ExecException; +import com.google.devtools.build.lib.actions.ExecutionStrategy; +import com.google.devtools.build.lib.actions.Spawn; +import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; +import com.google.devtools.build.lib.exec.SpawnCache; +import com.google.devtools.build.lib.exec.SpawnResult; +import com.google.devtools.build.lib.exec.SpawnResult.Status; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; +import com.google.devtools.build.lib.remote.Digests.ActionKey; +import com.google.devtools.build.lib.remote.TreeNodeRepository.TreeNode; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.remoteexecution.v1test.Action; +import com.google.devtools.remoteexecution.v1test.ActionResult; +import com.google.devtools.remoteexecution.v1test.Command; +import com.google.devtools.remoteexecution.v1test.Platform; +import java.io.IOException; +import java.util.Collection; +import java.util.NoSuchElementException; +import java.util.SortedMap; + +/** + * A remote {@link SpawnCache} implementation. + */ +@ThreadSafe // If the RemoteActionCache implementation is thread-safe. +@ExecutionStrategy( + name = {"remote-cache"}, + contextType = SpawnCache.class +) +final class RemoteSpawnCache implements SpawnCache { + private final Path execRoot; + private final RemoteOptions options; + // TODO(olaola): This will be set on a per-action basis instead. + private final Platform platform; + + private final RemoteActionCache remoteCache; + + RemoteSpawnCache(Path execRoot, RemoteOptions options, RemoteActionCache remoteCache) { + this.execRoot = execRoot; + this.options = options; + this.platform = options.parseRemotePlatformOverride(); + this.remoteCache = remoteCache; + } + + @Override + public CacheHandle lookup(Spawn spawn, SpawnExecutionPolicy policy) + throws InterruptedException, IOException, ExecException { + // Temporary hack: the TreeNodeRepository should be created and maintained upstream! + TreeNodeRepository repository = + new TreeNodeRepository(execRoot, policy.getActionInputFileCache()); + SortedMap inputMap = policy.getInputMapping(); + TreeNode inputRoot = repository.buildFromActionInputs(inputMap); + repository.computeMerkleDigests(inputRoot); + Command command = RemoteSpawnRunner.buildCommand(spawn.getArguments(), spawn.getEnvironment()); + Action action = + RemoteSpawnRunner.buildAction( + spawn.getOutputFiles(), + Digests.computeDigest(command), + repository.getMerkleDigest(inputRoot), + platform, + policy.getTimeout()); + + // Look up action cache, and reuse the action output if it is found. + final ActionKey actionKey = Digests.computeActionKey(action); + ActionResult result = + this.options.remoteAcceptCached ? remoteCache.getCachedActionResult(actionKey) : null; + if (result != null) { + // We don't cache failed actions, so we know the outputs exist. + // For now, download all outputs locally; in the future, we can reuse the digests to + // just update the TreeNodeRepository and continue the build. + try { + remoteCache.download(result, execRoot, policy.getFileOutErr()); + SpawnResult spawnResult = new SpawnResult.Builder() + .setStatus(Status.SUCCESS) + .setExitCode(result.getExitCode()) + .build(); + return SpawnCache.success(spawnResult); + } catch (CacheNotFoundException e) { + // There's a cache miss. Fall back to local execution. + } + } + if (options.remoteUploadLocalResults) { + return new CacheHandle() { + @Override + public boolean hasResult() { + return false; + } + + @Override + public SpawnResult getResult() { + throw new NoSuchElementException(); + } + + @Override + public boolean willStore() { + return true; + } + + @Override + public void store(SpawnResult result, Collection files) + throws InterruptedException, IOException { + if (result.status() != Status.SUCCESS || result.exitCode() != 0) { + return; + } + remoteCache.upload(actionKey, execRoot, files, policy.getFileOutErr()); + } + + @Override + public void close() { + } + }; + } else { + return SpawnCache.NO_RESULT_NO_STORE; + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java index 9fe489a7c452db..0657f2bc24729f 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java @@ -330,11 +330,4 @@ static List listExistingOutputFiles(Path execRoot, Spawn spawn) { } return outputFiles; } - - /** Release resources associated with this spawn runner. */ - public void close() { - if (remoteCache != null) { - remoteCache.close(); - } - } } diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java index 1cc587ab43ba91..d7bd5941d80d34 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java @@ -964,6 +964,8 @@ private static BlazeRuntime newRuntime(Iterable blazeModules, List< } runtimeBuilder.addBlazeModule(new BuiltinCommandModule()); + // This module needs to be registered before any module providing a SpawnCache implementation. + runtimeBuilder.addBlazeModule(new NoSpawnCacheModule()); runtimeBuilder.addBlazeModule(new CommandLogModule()); for (BlazeModule blazeModule : blazeModules) { runtimeBuilder.addBlazeModule(blazeModule); diff --git a/src/main/java/com/google/devtools/build/lib/runtime/NoSpawnCacheModule.java b/src/main/java/com/google/devtools/build/lib/runtime/NoSpawnCacheModule.java new file mode 100644 index 00000000000000..ffc20764908d78 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/NoSpawnCacheModule.java @@ -0,0 +1,29 @@ +// Copyright 2017 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.runtime; + +import com.google.devtools.build.lib.buildtool.BuildRequest; +import com.google.devtools.build.lib.exec.ExecutorBuilder; +import com.google.devtools.build.lib.exec.SpawnCache; + +/** + * Module providing a default no-op spawn cache. + */ +public final class NoSpawnCacheModule extends BlazeModule { + + @Override + public void executorInit(CommandEnvironment env, BuildRequest request, ExecutorBuilder builder) { + builder.addActionContext(SpawnCache.NO_CACHE); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java new file mode 100644 index 00000000000000..10ca68d638caa2 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/exec/AbstractSpawnStrategyTest.java @@ -0,0 +1,166 @@ +// Copyright 2017 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.exec; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.devtools.build.lib.actions.ActionExecutionContext; +import com.google.devtools.build.lib.actions.Spawn; +import com.google.devtools.build.lib.exec.SpawnCache.CacheHandle; +import com.google.devtools.build.lib.exec.SpawnResult.Status; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; +import com.google.devtools.build.lib.exec.util.SpawnBuilder; +import com.google.devtools.build.lib.testutil.Suite; +import com.google.devtools.build.lib.testutil.TestSpec; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; +import java.util.Collection; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link BlazeExecutor}. */ +@RunWith(JUnit4.class) +@TestSpec(size = Suite.SMALL_TESTS) +public class AbstractSpawnStrategyTest { + private static class TestedSpawnStrategy extends AbstractSpawnStrategy { + public TestedSpawnStrategy(SpawnRunner spawnRunner) { + super(spawnRunner); + } + } + + private static final Spawn SIMPLE_SPAWN = + new SpawnBuilder("/bin/echo", "Hi!").withEnvironment("VARIABLE", "value").build(); + + private final FileSystem fs = new InMemoryFileSystem(); + @Mock private SpawnRunner spawnRunner; + @Mock private ActionExecutionContext actionExecutionContext; + + @Before + public final void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testZeroExit() throws Exception { + when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(SpawnCache.NO_CACHE); + when(actionExecutionContext.getExecRoot()).thenReturn(fs.getPath("/execroot")); + when(spawnRunner.exec(any(Spawn.class), any(SpawnExecutionPolicy.class))) + .thenReturn(new SpawnResult.Builder().setStatus(Status.SUCCESS).build()); + + new TestedSpawnStrategy(spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); + // Must only be called exactly once. + verify(spawnRunner).exec(any(Spawn.class), any(SpawnExecutionPolicy.class)); + } + + @Test + public void testNonZeroExit() throws Exception { + when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(SpawnCache.NO_CACHE); + when(actionExecutionContext.getExecRoot()).thenReturn(fs.getPath("/execroot")); + SpawnResult result = new SpawnResult.Builder().setStatus(Status.SUCCESS).setExitCode(1).build(); + when(spawnRunner.exec(any(Spawn.class), any(SpawnExecutionPolicy.class))) + .thenReturn(result); + + try { + new TestedSpawnStrategy(spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); + } catch (SpawnExecException e) { + assertThat(e.getSpawnResult()).isSameAs(result); + } + // Must only be called exactly once. + verify(spawnRunner).exec(any(Spawn.class), any(SpawnExecutionPolicy.class)); + } + + @Test + public void testCacheHit() throws Exception { + SpawnCache cache = mock(SpawnCache.class); + SpawnResult result = new SpawnResult.Builder().setStatus(Status.SUCCESS).build(); + when(cache.lookup(any(Spawn.class), any(SpawnExecutionPolicy.class))) + .thenReturn(SpawnCache.success(result)); + when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(cache); + when(actionExecutionContext.getExecRoot()).thenReturn(fs.getPath("/execroot")); + + new TestedSpawnStrategy(spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); + verify(spawnRunner, never()).exec(any(Spawn.class), any(SpawnExecutionPolicy.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void testCacheMiss() throws Exception { + SpawnCache cache = mock(SpawnCache.class); + CacheHandle entry = mock(CacheHandle.class); + when(cache.lookup(any(Spawn.class), any(SpawnExecutionPolicy.class))).thenReturn(entry); + when(entry.hasResult()).thenReturn(false); + when(entry.willStore()).thenReturn(true); + + when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(cache); + when(actionExecutionContext.getExecRoot()).thenReturn(fs.getPath("/execroot")); + SpawnResult result = new SpawnResult.Builder().setStatus(Status.SUCCESS).build(); + when(spawnRunner.exec(any(Spawn.class), any(SpawnExecutionPolicy.class))).thenReturn(result); + + new TestedSpawnStrategy(spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); + // Must only be called exactly once. + verify(spawnRunner).exec(any(Spawn.class), any(SpawnExecutionPolicy.class)); + verify(entry).store(eq(result), any(Collection.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void testCacheMissWithNonZeroExit() throws Exception { + SpawnCache cache = mock(SpawnCache.class); + CacheHandle entry = mock(CacheHandle.class); + when(cache.lookup(any(Spawn.class), any(SpawnExecutionPolicy.class))).thenReturn(entry); + when(entry.hasResult()).thenReturn(false); + when(entry.willStore()).thenReturn(true); + + when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(cache); + when(actionExecutionContext.getExecRoot()).thenReturn(fs.getPath("/execroot")); + SpawnResult result = new SpawnResult.Builder().setStatus(Status.SUCCESS).setExitCode(1).build(); + when(spawnRunner.exec(any(Spawn.class), any(SpawnExecutionPolicy.class))).thenReturn(result); + + try { + new TestedSpawnStrategy(spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); + } catch (SpawnExecException e) { + assertThat(e.getSpawnResult()).isSameAs(result); + } + // Must only be called exactly once. + verify(spawnRunner).exec(any(Spawn.class), any(SpawnExecutionPolicy.class)); + verify(entry).store(eq(result), any(Collection.class)); + } + + @Test + public void testTagNoCache() throws Exception { + SpawnCache cache = mock(SpawnCache.class); + when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(cache); + when(actionExecutionContext.getExecRoot()).thenReturn(fs.getPath("/execroot")); + when(spawnRunner.exec(any(Spawn.class), any(SpawnExecutionPolicy.class))) + .thenReturn(new SpawnResult.Builder().setStatus(Status.SUCCESS).build()); + + Spawn uncacheableSpawn = + new SpawnBuilder("/bin/echo", "Hi").withExecutionInfo("no-cache", "").build(); + new TestedSpawnStrategy(spawnRunner).exec(uncacheableSpawn, actionExecutionContext); + // Must only be called exactly once. + verify(spawnRunner).exec(any(Spawn.class), any(SpawnExecutionPolicy.class)); + // Must not be called. + verify(cache, never()).lookup(any(Spawn.class), any(SpawnExecutionPolicy.class)); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnCacheTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnCacheTest.java new file mode 100644 index 00000000000000..930815bcf57ab1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnCacheTest.java @@ -0,0 +1,203 @@ +// Copyright 2017 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.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.actions.ActionInput; +import com.google.devtools.build.lib.actions.ActionInputFileCache; +import com.google.devtools.build.lib.actions.ActionInputHelper; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; +import com.google.devtools.build.lib.actions.ResourceSet; +import com.google.devtools.build.lib.actions.SimpleSpawn; +import com.google.devtools.build.lib.exec.SpawnCache.CacheHandle; +import com.google.devtools.build.lib.exec.SpawnInputExpander; +import com.google.devtools.build.lib.exec.SpawnResult; +import com.google.devtools.build.lib.exec.SpawnResult.Status; +import com.google.devtools.build.lib.exec.SpawnRunner.ProgressStatus; +import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy; +import com.google.devtools.build.lib.exec.util.FakeOwner; +import com.google.devtools.build.lib.remote.Digests.ActionKey; +import com.google.devtools.build.lib.remote.TreeNodeRepository.TreeNode; +import com.google.devtools.build.lib.util.io.FileOutErr; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; +import com.google.devtools.common.options.Options; +import com.google.devtools.remoteexecution.v1test.ActionResult; +import com.google.devtools.remoteexecution.v1test.Command; +import java.io.IOException; +import java.time.Duration; +import java.util.Collection; +import java.util.SortedMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link RemoteSpawnCache}. */ +@RunWith(JUnit4.class) +public class RemoteSpawnCacheTest { + private static final ArtifactExpander SIMPLE_ARTIFACT_EXPANDER = + new ArtifactExpander() { + @Override + public void expand(Artifact artifact, Collection output) { + output.add(artifact); + } + }; + + private FileSystem fs; + private Path execRoot; + private SimpleSpawn simpleSpawn; + private FakeActionInputFileCache fakeFileCache; + @Mock private RemoteActionCache remoteCache; + private RemoteSpawnCache cache; + private FileOutErr outErr; + + private final SpawnExecutionPolicy simplePolicy = + new SpawnExecutionPolicy() { + @Override + public int getId() { + return 0; + } + + @Override + public void prefetchInputs() { + // CachedLocalSpawnRunner should never prefetch itself, though the nested SpawnRunner may. + throw new UnsupportedOperationException(); + } + + @Override + public void lockOutputFiles() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean speculating() { + return false; + } + + @Override + public ActionInputFileCache getActionInputFileCache() { + return fakeFileCache; + } + + @Override + public ArtifactExpander getArtifactExpander() { + throw new UnsupportedOperationException(); + } + + @Override + public Duration getTimeout() { + return Duration.ZERO; + } + + @Override + public FileOutErr getFileOutErr() { + return outErr; + } + + @Override + public SortedMap getInputMapping() throws IOException { + return new SpawnInputExpander(/*strict*/ false) + .getInputMapping(simpleSpawn, SIMPLE_ARTIFACT_EXPANDER, fakeFileCache, "workspace"); + } + + @Override + public void report(ProgressStatus state, String name) { + // TODO(ulfjack): Test that the right calls are made. + } + }; + + @Before + public final void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + fs = new InMemoryFileSystem(); + execRoot = fs.getPath("/exec/root"); + FileSystemUtils.createDirectoryAndParents(execRoot); + fakeFileCache = new FakeActionInputFileCache(execRoot); + simpleSpawn = + new SimpleSpawn( + new FakeOwner("Mnemonic", "Progress Message"), + ImmutableList.of("/bin/echo", "Hi!"), + ImmutableMap.of("VARIABLE", "value"), + /*executionInfo=*/ ImmutableMap.of(), + /*inputs=*/ ImmutableList.of(ActionInputHelper.fromPath("input")), + /*outputs=*/ ImmutableList.of(), + ResourceSet.ZERO); + + Path stdout = fs.getPath("/tmp/stdout"); + Path stderr = fs.getPath("/tmp/stderr"); + FileSystemUtils.createDirectoryAndParents(stdout.getParentDirectory()); + FileSystemUtils.createDirectoryAndParents(stderr.getParentDirectory()); + outErr = new FileOutErr(stdout, stderr); + RemoteOptions options = Options.getDefaults(RemoteOptions.class); + cache = new RemoteSpawnCache(execRoot, options, remoteCache); + fakeFileCache.createScratchInput(simpleSpawn.getInputFiles().get(0), "xyz"); + } + + @SuppressWarnings("unchecked") + @Test + public void cacheHit() throws Exception { + ActionResult actionResult = ActionResult.getDefaultInstance(); + when(remoteCache.getCachedActionResult(any(ActionKey.class))).thenReturn(actionResult); + + CacheHandle entry = cache.lookup(simpleSpawn, simplePolicy); + assertThat(entry.hasResult()).isTrue(); + SpawnResult result = entry.getResult(); + // All other methods on RemoteActionCache have side effects, so we verify all of them. + verify(remoteCache).download(actionResult, execRoot, outErr); + verify(remoteCache, never()) + .ensureInputsPresent( + any(TreeNodeRepository.class), + any(Path.class), + any(TreeNode.class), + any(Command.class)); + verify(remoteCache, never()) + .upload( + any(ActionKey.class), any(Path.class), any(Collection.class), any(FileOutErr.class)); + assertThat(result.setupSuccess()).isTrue(); + assertThat(result.exitCode()).isEqualTo(0); + // We expect the CachedLocalSpawnRunner to _not_ write to outErr at all. + assertThat(outErr.hasRecordedOutput()).isFalse(); + assertThat(outErr.hasRecordedStderr()).isFalse(); + } + + @Test + public void cacheMiss() throws Exception { + CacheHandle entry = cache.lookup(simpleSpawn, simplePolicy); + assertThat(entry.hasResult()).isFalse(); + SpawnResult result = new SpawnResult.Builder().setExitCode(0).setStatus(Status.SUCCESS).build(); + ImmutableList outputFiles = ImmutableList.of(fs.getPath("/random/file")); + entry.store(result, outputFiles); + verify(remoteCache) + .upload( + any(ActionKey.class), + any(Path.class), + eq(outputFiles), + eq(outErr)); + } +}