// Copyright 2015 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.skyframe; import static com.google.common.truth.Truth.assertThat; import com.google.common.base.Objects; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Callables; import com.google.devtools.build.lib.actions.AbstractAction; import com.google.devtools.build.lib.actions.Action; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionExecutionException; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.Executor; import com.google.devtools.build.lib.actions.util.ActionsTestUtil; import com.google.devtools.build.lib.actions.util.DummyExecutor; import com.google.devtools.build.lib.util.Fingerprint; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.RootedPath; import com.google.devtools.build.skyframe.EvaluationProgressReceiver; import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState; import com.google.devtools.build.skyframe.SkyFunction.Environment; import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link SkyframeAwareAction}. */ @RunWith(JUnit4.class) public class SkyframeAwareActionTest extends TimestampBuilderTestCase { private Builder builder; private Executor executor; private TrackingEvaluationProgressReceiver progressReceiver; @Before public final void createBuilder() throws Exception { progressReceiver = new TrackingEvaluationProgressReceiver(); builder = createBuilder(inMemoryCache, 1, /*keepGoing=*/ false, progressReceiver); } @Before public final void createExecutor() throws Exception { executor = new DummyExecutor(rootDirectory); } private static final class TrackingEvaluationProgressReceiver extends EvaluationProgressReceiver.NullEvaluationProgressReceiver { public static final class InvalidatedKey { public final SkyKey skyKey; public final InvalidationState state; InvalidatedKey(SkyKey skyKey, InvalidationState state) { this.skyKey = skyKey; this.state = state; } @Override public boolean equals(Object obj) { return obj instanceof InvalidatedKey && this.skyKey.equals(((InvalidatedKey) obj).skyKey) && this.state.equals(((InvalidatedKey) obj).state); } @Override public int hashCode() { return Objects.hashCode(skyKey, state); } } public static final class EvaluatedEntry { public final SkyKey skyKey; public final SkyValue value; public final EvaluationState state; EvaluatedEntry(SkyKey skyKey, SkyValue value, EvaluationState state) { this.skyKey = skyKey; this.value = value; this.state = state; } @Override public boolean equals(Object obj) { return obj instanceof EvaluatedEntry && this.skyKey.equals(((EvaluatedEntry) obj).skyKey) && this.value.equals(((EvaluatedEntry) obj).value) && this.state.equals(((EvaluatedEntry) obj).state); } @Override public int hashCode() { return Objects.hashCode(skyKey, value, state); } } public final Set<InvalidatedKey> invalidated = Sets.newConcurrentHashSet(); public final Set<SkyKey> enqueued = Sets.newConcurrentHashSet(); public final Set<EvaluatedEntry> evaluated = Sets.newConcurrentHashSet(); public void reset() { invalidated.clear(); enqueued.clear(); evaluated.clear(); } public boolean wasInvalidated(SkyKey skyKey) { for (InvalidatedKey e : invalidated) { if (e.skyKey.equals(skyKey)) { return true; } } return false; } public EvaluatedEntry getEvalutedEntry(SkyKey forKey) { for (EvaluatedEntry e : evaluated) { if (e.skyKey.equals(forKey)) { return e; } } return null; } @Override public void invalidated(SkyKey skyKey, InvalidationState state) { invalidated.add(new InvalidatedKey(skyKey, state)); } @Override public void enqueueing(SkyKey skyKey) { enqueued.add(skyKey); } @Override public void evaluated( SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, EvaluationState state) { evaluated.add(new EvaluatedEntry(skyKey, skyValueSupplier.get(), state)); } } /** A mock action that counts how many times it was executed. */ private static class ExecutionCountingAction extends AbstractAction { private final AtomicInteger executionCounter; ExecutionCountingAction(Artifact input, Artifact output, AtomicInteger executionCounter) { super(ActionsTestUtil.NULL_ACTION_OWNER, ImmutableList.of(input), ImmutableList.of(output)); this.executionCounter = executionCounter; } @Override public void execute(ActionExecutionContext actionExecutionContext) throws ActionExecutionException, InterruptedException { executionCounter.incrementAndGet(); // This action first reads its input file (there can be only one). For the purpose of these // tests we assume that the input file is short, maybe just 10 bytes long. byte[] input = new byte[10]; int inputLen = 0; try (InputStream in = Iterables.getOnlyElement(getInputs()).getPath().getInputStream()) { inputLen = in.read(input); } catch (IOException e) { throw new ActionExecutionException(e, this, false); } // This action then writes the contents of the input to the (only) output file, and appends an // extra "x" character too. try (OutputStream out = getPrimaryOutput().getPath().getOutputStream()) { out.write(input, 0, inputLen); out.write('x'); } catch (IOException e) { throw new ActionExecutionException(e, this, false); } } @Override public String getMnemonic() { return null; } @Override protected String computeKey() { return getPrimaryOutput().getExecPathString() + executionCounter.get(); } } private static class ExecutionCountingCacheBypassingAction extends ExecutionCountingAction { ExecutionCountingCacheBypassingAction( Artifact input, Artifact output, AtomicInteger executionCounter) { super(input, output, executionCounter); } @Override public boolean executeUnconditionally() { return true; } @Override public boolean isVolatile() { return true; } } /** A mock skyframe-aware action that counts how many times it was executed. */ private static class SkyframeAwareExecutionCountingAction extends ExecutionCountingCacheBypassingAction implements SkyframeAwareAction { private final SkyKey actionDepKey; SkyframeAwareExecutionCountingAction( Artifact input, Artifact output, AtomicInteger executionCounter, SkyKey actionDepKey) { super(input, output, executionCounter); this.actionDepKey = actionDepKey; } @Override public void establishSkyframeDependencies(Environment env) throws ExceptionBase, InterruptedException { // Establish some Skyframe dependency. A real action would then use this to compute and // cache data for the execute(...) method. env.getValue(actionDepKey); } } private interface ExecutionCountingActionFactory { ExecutionCountingAction create(Artifact input, Artifact output, AtomicInteger executionCounter); } private enum ChangeArtifact { DONT_CHANGE, CHANGE_MTIME { @Override boolean changeMtime() { return true; } }, CHANGE_MTIME_AND_CONTENT { @Override boolean changeMtime() { return true; } @Override boolean changeContent() { return true; } }; boolean changeMtime() { return false; } boolean changeContent() { return false; } } private enum ExpectActionIs { NOT_DIRTIED { @Override boolean actuallyClean() { return true; } }, DIRTIED_BUT_VERIFIED_CLEAN { @Override boolean dirtied() { return true; } @Override boolean actuallyClean() { return true; } }, // REBUILT_BUT_ACTION_CACHE_HIT, // This would be a bug, symptom of a skyframe-aware action that doesn't bypass the action cache // and is incorrectly regarded as an action cache hit when its inputs stayed the same but its // "skyframe dependencies" changed. REEXECUTED { @Override boolean dirtied() { return true; } @Override boolean reexecuted() { return true; } }; boolean dirtied() { return false; } boolean actuallyClean() { return false; } boolean reexecuted() { return false; } } private void maybeChangeFile(Artifact file, ChangeArtifact changeRequest) throws Exception { if (changeRequest == ChangeArtifact.DONT_CHANGE) { return; } if (changeRequest.changeMtime()) { // 1000000 should be larger than the filesystem timestamp granularity. file.getPath().setLastModifiedTime(file.getPath().getLastModifiedTime() + 1000000); tsgm.waitForTimestampGranularity(reporter.getOutErr()); } if (changeRequest.changeContent()) { appendToFile(file.getPath()); } // Invalidate the file state value to inform Skyframe that the file may have changed. // This will also invalidate the action execution value. differencer.invalidate( ImmutableList.of( FileStateValue.key( RootedPath.toRootedPath(file.getRoot().getPath(), file.getRootRelativePath())))); } private void assertActionExecutions( ExecutionCountingActionFactory actionFactory, ChangeArtifact changeActionInput, Callable<Void> betweenBuilds, ExpectActionIs expectActionIs) throws Exception { // Set up the action's input, output, owner and most importantly the execution counter. Artifact actionInput = createSourceArtifact("foo/action-input.txt"); Artifact actionOutput = createDerivedArtifact("foo/action-output.txt"); AtomicInteger executionCounter = new AtomicInteger(0); scratch.file(actionInput.getPath().getPathString(), "foo"); // Generating actions of artifacts are found by looking them up in the graph. The lookup value // must be present in the graph before execution. Action action = actionFactory.create(actionInput, actionOutput, executionCounter); registerAction(action); // Build the output for the first time. builder.buildArtifacts( reporter, ImmutableSet.of(actionOutput), null, null, null, null, executor, null, false, null, null); // Sanity check that our invalidation receiver is working correctly. We'll rely on it again. SkyKey actionKey = ActionExecutionValue.key(OWNER_KEY, 0); TrackingEvaluationProgressReceiver.EvaluatedEntry evaluatedAction = progressReceiver.getEvalutedEntry(actionKey); assertThat(evaluatedAction).isNotNull(); SkyValue actionValue = evaluatedAction.value; // Mutate the action input if requested. maybeChangeFile(actionInput, changeActionInput); // Execute user code before next build. betweenBuilds.call(); // Rebuild the output. progressReceiver.reset(); builder.buildArtifacts( reporter, ImmutableSet.of(actionOutput), null, null, null, null, executor, null, false, null, null); if (expectActionIs.dirtied()) { assertThat(progressReceiver.wasInvalidated(actionKey)).isTrue(); TrackingEvaluationProgressReceiver.EvaluatedEntry newEntry = progressReceiver.getEvalutedEntry(actionKey); assertThat(newEntry).isNotNull(); if (expectActionIs.actuallyClean()) { // Action was dirtied but verified clean. assertThat(newEntry.state).isEqualTo(EvaluationState.CLEAN); assertThat(newEntry.value).isEqualTo(actionValue); } else { // Action was dirtied and rebuilt. It was either reexecuted or was an action cache hit, // doesn't matter here. assertThat(newEntry.state).isEqualTo(EvaluationState.BUILT); assertThat(newEntry.value).isNotEqualTo(actionValue); } } else { // Action was not dirtied. assertThat(progressReceiver.wasInvalidated(actionKey)).isFalse(); } // Assert that the action was executed the right number of times. Whether the action execution // function was called again is up for the test method to verify. assertThat(executionCounter.get()).isEqualTo(expectActionIs.reexecuted() ? 2 : 1); } private RootedPath createSkyframeDepOfAction() throws Exception { scratch.file(rootDirectory.getRelative("action.dep").getPathString(), "blah"); return RootedPath.toRootedPath(rootDirectory, PathFragment.create("action.dep")); } private void appendToFile(Path path) throws Exception { try (OutputStream stm = path.getOutputStream(/*append=*/ true)) { stm.write("blah".getBytes(StandardCharsets.UTF_8)); } } @Test public void testCacheCheckingActionWithContentChangingInput() throws Exception { assertActionWithContentChangingInput(/* unconditionalExecution */ false); } @Test public void testCacheBypassingActionWithContentChangingInput() throws Exception { assertActionWithContentChangingInput(/* unconditionalExecution */ true); } private void assertActionWithContentChangingInput(final boolean unconditionalExecution) throws Exception { // Assert that a simple, non-skyframe-aware action is executed twice // if its input's content changes between builds. assertActionExecutions( new ExecutionCountingActionFactory() { @Override public ExecutionCountingAction create( Artifact input, Artifact output, AtomicInteger executionCounter) { return unconditionalExecution ? new ExecutionCountingCacheBypassingAction(input, output, executionCounter) : new ExecutionCountingAction(input, output, executionCounter); } }, ChangeArtifact.CHANGE_MTIME_AND_CONTENT, Callables.<Void>returning(null), ExpectActionIs.REEXECUTED); } @Test public void testCacheCheckingActionWithMtimeChangingInput() throws Exception { assertActionWithMtimeChangingInput(/* unconditionalExecution */ false); } @Test public void testCacheBypassingActionWithMtimeChangingInput() throws Exception { assertActionWithMtimeChangingInput(/* unconditionalExecution */ true); } private void assertActionWithMtimeChangingInput(final boolean unconditionalExecution) throws Exception { // Assert that a simple, non-skyframe-aware action is executed only once // if its input's mtime changes but its contents stay the same between builds. assertActionExecutions( new ExecutionCountingActionFactory() { @Override public ExecutionCountingAction create( Artifact input, Artifact output, AtomicInteger executionCounter) { return unconditionalExecution ? new ExecutionCountingCacheBypassingAction(input, output, executionCounter) : new ExecutionCountingAction(input, output, executionCounter); } }, ChangeArtifact.CHANGE_MTIME, Callables.<Void>returning(null), ExpectActionIs.DIRTIED_BUT_VERIFIED_CLEAN); } public void testActionWithNonChangingInput(final boolean unconditionalExecution) throws Exception { // Assert that a simple, non-skyframe-aware action is executed only once // if its input does not change at all between builds. assertActionExecutions( new ExecutionCountingActionFactory() { @Override public ExecutionCountingAction create( Artifact input, Artifact output, AtomicInteger executionCounter) { return unconditionalExecution ? new ExecutionCountingCacheBypassingAction(input, output, executionCounter) : new ExecutionCountingAction(input, output, executionCounter); } }, ChangeArtifact.DONT_CHANGE, Callables.<Void>returning(null), ExpectActionIs.NOT_DIRTIED); } private void assertActionWithMaybeChangingInputAndChangingSkyframeDeps( ChangeArtifact changeInputFile) throws Exception { final RootedPath depPath = createSkyframeDepOfAction(); final SkyKey skyframeDep = FileStateValue.key(depPath); // Assert that an action-cache-check-bypassing action is executed twice if its skyframe deps // change while its input does not. The skyframe dependency is established by making the action // skyframe-aware and updating the value between builds. assertActionExecutions( new ExecutionCountingActionFactory() { @Override public ExecutionCountingAction create( Artifact input, Artifact output, AtomicInteger executionCounter) { return new SkyframeAwareExecutionCountingAction( input, output, executionCounter, skyframeDep); } }, changeInputFile, new Callable<Void>() { @Override public Void call() throws Exception { // Invalidate the dependency and change what its value will be in the next build. This // should enforce rebuilding of the action. appendToFile(depPath.asPath()); differencer.invalidate(ImmutableList.of(skyframeDep)); return null; } }, ExpectActionIs.REEXECUTED); } @Test public void testActionWithNonChangingInputButChangingSkyframeDeps() throws Exception { assertActionWithMaybeChangingInputAndChangingSkyframeDeps(ChangeArtifact.DONT_CHANGE); } @Test public void testActionWithChangingInputMtimeAndChangingSkyframeDeps() throws Exception { assertActionWithMaybeChangingInputAndChangingSkyframeDeps(ChangeArtifact.CHANGE_MTIME); } @Test public void testActionWithChangingInputAndChangingSkyframeDeps() throws Exception { assertActionWithMaybeChangingInputAndChangingSkyframeDeps( ChangeArtifact.CHANGE_MTIME_AND_CONTENT); } @Test public void testActionWithNonChangingInputAndNonChangingSkyframeDeps() throws Exception { final SkyKey skyframeDep = FileStateValue.key(createSkyframeDepOfAction()); // Assert that an action-cache-check-bypassing action is executed only once if neither its input // nor its Skyframe dependency changes between builds. assertActionExecutions( new ExecutionCountingActionFactory() { @Override public ExecutionCountingAction create( Artifact input, Artifact output, AtomicInteger executionCounter) { return new SkyframeAwareExecutionCountingAction( input, output, executionCounter, skyframeDep); } }, ChangeArtifact.DONT_CHANGE, new Callable<Void>() { @Override public Void call() throws Exception { // Invalidate the dependency but leave its value up-to-date, so the action should not // be rebuilt. differencer.invalidate(ImmutableList.of(skyframeDep)); return null; } }, ExpectActionIs.DIRTIED_BUT_VERIFIED_CLEAN); } private abstract static class SingleOutputAction extends AbstractAction { SingleOutputAction(@Nullable Artifact input, Artifact output) { super( ActionsTestUtil.NULL_ACTION_OWNER, input == null ? ImmutableList.<Artifact>of() : ImmutableList.of(input), ImmutableList.of(output)); } protected static final class Buffer { final int size; final byte[] data; Buffer(byte[] data, int size) { this.data = data; this.size = size; } } protected Buffer readInput() throws ActionExecutionException { byte[] input = new byte[100]; int inputLen = 0; try (InputStream in = getPrimaryInput().getPath().getInputStream()) { inputLen = in.read(input, 0, input.length); } catch (IOException e) { throw new ActionExecutionException(e, this, false); } return new Buffer(input, inputLen); } protected void writeOutput(@Nullable Buffer buf, String data) throws ActionExecutionException { try (OutputStream out = getPrimaryOutput().getPath().getOutputStream()) { if (buf != null) { out.write(buf.data, 0, buf.size); } out.write(data.getBytes(StandardCharsets.UTF_8), 0, data.length()); } catch (IOException e) { throw new ActionExecutionException(e, this, false); } } @Override public String getMnemonic() { return "MockActionMnemonic"; } @Override protected String computeKey() { return new Fingerprint().addInt(42).hexDigestAndReset(); } } private abstract static class SingleOutputSkyframeAwareAction extends SingleOutputAction implements SkyframeAwareAction { SingleOutputSkyframeAwareAction(@Nullable Artifact input, Artifact output) { super(input, output); } @Override public boolean executeUnconditionally() { return true; } @Override public boolean isVolatile() { return true; } } /** * Regression test to avoid a potential race condition in {@link ActionExecutionFunction}. * * <p>The test ensures that when ActionExecutionFunction executes a Skyframe-aware action * (implementor of {@link SkyframeAwareAction}), ActionExecutionFunction first requests the inputs * of the action and ensures they are built before requesting any of its Skyframe dependencies. * * <p>This strict ordering is very important to avoid the race condition, which could arise if the * compute method were too eager to request all dependencies: request input files but even if some * are missing, request also the skyframe-dependencies. The race is described in this method's * body. */ @Test public void testRaceConditionBetweenInputAcquisitionAndSkyframeDeps() throws Exception { // Sequence of events on threads A and B, showing SkyFunctions and requested SkyKeys, leading // to an InconsistentFilesystemException: // // _______________[Thread A]_________________|_______________[Thread B]_________________ // ActionExecutionFunction(gen2_action: | idle // genfiles/gen1 -> genfiles/foo/bar/gen2) | // ARTIFACT:genfiles/gen1 | // MOCK_VALUE:dummy_argument | // env.valuesMissing():yes ==> return | // | // ArtifactFunction(genfiles/gen1) | MockFunction() // CONFIGURED_TARGET://foo:gen1 | FILE:genfiles/foo // ACTION_EXECUTION:gen1_action | env.valuesMissing():yes ==> return // env.valuesMissing():yes ==> return | // | FileFunction(genfiles/foo) // ActionExecutionFunction(gen1_action) | FILE:genfiles // ARTIFACT:genfiles/gen0 | env.valuesMissing():yes ==> return // env.valuesMissing():yes ==> return | // | FileFunction(genfiles) // ArtifactFunction(genfiles/gen0) | FILE_STATE:genfiles // CONFIGURED_TARGET://foo:gen0 | env.valuesMissing():yes ==> return // ACTION_EXECUTION:gen0_action | // env.valuesMissing():yes ==> return | FileStateFunction(genfiles) // | stat genfiles // ActionExecutionFunction(gen0_action) | return FileStateValue:non-existent // create output directory: genfiles | // working | FileFunction(genfiles/foo) // | FILE:genfiles // | FILE_STATE:genfiles/foo // | env.valuesMissing():yes ==> return // | // | FileStateFunction(genfiles/foo) // | stat genfiles/foo // | return FileStateValue:non-existent // | // done, created genfiles/gen0 | FileFunction(genfiles/foo) // return ActionExecutionValue(gen0_action) | FILE:genfiles // | FILE_STATE:genfiles/foo // ArtifactFunction(genfiles/gen0) | return FileValue(genfiles/foo:non-existent) // CONFIGURED_TARGET://foo:gen0 | // ACTION_EXECUTION:gen0_action | MockFunction() // return ArtifactSkyKey(genfiles/gen0) | FILE:genfiles/foo // | FILE:genfiles/foo/bar/gen1 // ActionExecutionFunction(gen1_action) | env.valuesMissing():yes ==> return // ARTIFACT:genfiles/gen0 | // create output directory: genfiles/foo/bar | FileFunction(genfiles/foo/bar/gen1) // done, created genfiles/foo/bar/gen1 | FILE:genfiles/foo/bar // return ActionExecutionValue(gen1_action) | env.valuesMissing():yes ==> return // | // idle | FileFunction(genfiles/foo/bar) // | FILE:genfiles/foo // | FILE_STATE:genfiles/foo/bar // | env.valuesMissing():yes ==> return // | // | FileStateFunction(genfiles/foo/bar) // | stat genfiles/foo/bar // | return FileStateValue:directory // | // | FileFunction(genfiles/foo/bar) // | FILE:genfiles/foo // | FILE_STATE:genfiles/foo/bar // | throw InconsistentFilesystemException: // | genfiles/foo doesn't exist but // | genfiles/foo/bar does! Artifact genFile1 = createDerivedArtifact("foo/bar/gen1.txt"); Artifact genFile2 = createDerivedArtifact("gen2.txt"); registerAction( new SingleOutputAction(null, genFile1) { @Override public void execute(ActionExecutionContext actionExecutionContext) throws ActionExecutionException, InterruptedException { writeOutput(null, "gen1"); } }); registerAction( new SingleOutputSkyframeAwareAction(genFile1, genFile2) { @Override public void establishSkyframeDependencies(Environment env) throws ExceptionBase { assertThat(env.valuesMissing()).isFalse(); } @Override public void execute(ActionExecutionContext actionExecutionContext) throws ActionExecutionException, InterruptedException { writeOutput(readInput(), "gen2"); } }); builder.buildArtifacts( reporter, ImmutableSet.of(genFile2), null, null, null, null, executor, null, false, null, null); } }