| // 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.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Sets; |
| import com.google.common.graph.ImmutableGraph; |
| 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.ActionKeyContext; |
| import com.google.devtools.build.lib.actions.ActionLookupData; |
| import com.google.devtools.build.lib.actions.ActionResult; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.Executor; |
| import com.google.devtools.build.lib.actions.FileStateValue; |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; |
| import com.google.devtools.build.lib.actions.util.DummyExecutor; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; |
| import com.google.devtools.build.lib.collect.nestedset.Order; |
| import com.google.devtools.build.lib.testutil.TimestampGranularityUtils; |
| 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.Root; |
| 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.SkyKey; |
| import com.google.devtools.build.skyframe.SkyValue; |
| import com.google.devtools.build.skyframe.ValueOrException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.function.Supplier; |
| 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(fileSystem, 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); |
| } |
| } |
| |
| private static final class EvaluatedEntry { |
| public final SkyKey skyKey; |
| final EvaluationSuccessState successState; |
| public final EvaluationState state; |
| |
| EvaluatedEntry(SkyKey skyKey, EvaluationSuccessState successState, EvaluationState state) { |
| this.skyKey = skyKey; |
| this.successState = successState; |
| this.state = state; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| return obj instanceof EvaluatedEntry |
| && this.skyKey.equals(((EvaluatedEntry) obj).skyKey) |
| && this.successState.equals(((EvaluatedEntry) obj).successState) |
| && this.state.equals(((EvaluatedEntry) obj).state); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(skyKey, successState, 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, |
| @Nullable SkyValue value, |
| Supplier<EvaluationSuccessState> evaluationSuccessState, |
| EvaluationState state) { |
| evaluated.add(new EvaluatedEntry(skyKey, evaluationSuccessState.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, |
| NestedSetBuilder.create(Order.STABLE_ORDER, input), |
| ImmutableSet.of(output)); |
| this.executionCounter = executionCounter; |
| } |
| |
| @Override |
| public ActionResult 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 = getInputs().getSingleton().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); |
| } |
| return ActionResult.EMPTY; |
| } |
| |
| @Override |
| public String getMnemonic() { |
| return null; |
| } |
| |
| @Override |
| protected void computeKey(ActionKeyContext actionKeyContext, Fingerprint fp) { |
| fp.addString(getPrimaryOutput().getExecPathString()); |
| fp.addInt(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<IOException> { |
| private final SkyKey actionDepKey; |
| |
| SkyframeAwareExecutionCountingAction( |
| Artifact input, Artifact output, AtomicInteger executionCounter, SkyKey actionDepKey) { |
| super(input, output, executionCounter); |
| this.actionDepKey = actionDepKey; |
| } |
| |
| @Override |
| public Object processSkyframeValues( |
| ImmutableList<? extends SkyKey> keys, |
| Map<SkyKey, ValueOrException<IOException>> values, |
| boolean valuesMissing) { |
| assertThat(keys).containsExactly(actionDepKey); |
| assertThat(values.keySet()).containsExactly(actionDepKey); |
| return null; |
| } |
| |
| @Override |
| public ImmutableList<SkyKey> getDirectSkyframeDependencies() { |
| return ImmutableList.of(actionDepKey); |
| } |
| |
| @Override |
| public Class<IOException> getExceptionType() { |
| return IOException.class; |
| } |
| |
| @Override |
| public ImmutableGraph<SkyKey> getSkyframeDependenciesForRewinding(SkyKey self) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| /** Sanity check: ensure that a file's ctime was updated from an older value. */ |
| private void checkCtimeUpdated(Path path, long oldCtime) throws IOException { |
| if (oldCtime >= path.stat().getLastChangeTime()) { |
| throw new IllegalStateException(String.format("path=(%s), ctime=(%d)", path, oldCtime)); |
| } |
| } |
| |
| private void maybeChangeFile(Artifact file, ChangeArtifact changeRequest) throws Exception { |
| if (changeRequest == ChangeArtifact.DONT_CHANGE) { |
| return; |
| } |
| |
| Path path = file.getPath(); |
| |
| if (changeRequest.changeMtime()) { |
| long ctime = path.stat().getLastChangeTime(); |
| // Ensure enough time elapsed for file updates to have a visible effect on the file's ctime. |
| TimestampGranularityUtils.waitForTimestampGranularity(ctime, reporter.getOutErr()); |
| // waitForTimestampGranularity waits long enough for System.currentTimeMillis() to be greater |
| // than the time at the setCommandStartTime() call. Therefore setting |
| // System.currentTimeMillis() is guaranteed to advance the file's ctime. |
| path.setLastModifiedTime(System.currentTimeMillis()); |
| // Sanity check: ensure that updating the file's mtime indeed advanced its ctime. |
| checkCtimeUpdated(path, ctime); |
| } |
| |
| if (changeRequest.changeContent()) { |
| long ctime = path.stat().getLastChangeTime(); |
| // Ensure enough time elapsed for file updates to have a visible effect on the file's ctime. |
| TimestampGranularityUtils.waitForTimestampGranularity(ctime, reporter.getOutErr()); |
| appendToFile(path); |
| // Sanity check: ensure that appending to the file indeed advanced its ctime. |
| checkCtimeUpdated(path, ctime); |
| } |
| |
| // 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().getRoot(), 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, |
| null, |
| executor, |
| null, |
| null, |
| options, |
| null, |
| null); |
| |
| // Sanity check that our invalidation receiver is working correctly. We'll rely on it again. |
| SkyKey actionKey = ActionLookupData.create(ACTION_LOOKUP_KEY, 0); |
| TrackingEvaluationProgressReceiver.EvaluatedEntry evaluatedAction = |
| progressReceiver.getEvalutedEntry(actionKey); |
| assertThat(evaluatedAction).isNotNull(); |
| |
| // 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, |
| null, |
| executor, |
| null, |
| null, |
| options, |
| 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); |
| } 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); |
| } |
| } 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(Root.fromPath(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), |
| unconditionalExecution |
| ? ExpectActionIs.REEXECUTED |
| : ExpectActionIs.DIRTIED_BUT_VERIFIED_CLEAN); |
| } |
| |
| @Test |
| public void testCacheCheckingActionWithNonChangingInput() throws Exception { |
| assertActionWithNonChangingInput(/* unconditionalExecution */ false); |
| } |
| |
| @Test |
| public void testCacheBypassingActionWithNonChangingInput() throws Exception { |
| assertActionWithNonChangingInput(/* unconditionalExecution */ true); |
| } |
| |
| private void assertActionWithNonChangingInput(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 |
| ? NestedSetBuilder.emptySet(Order.STABLE_ORDER) |
| : NestedSetBuilder.create(Order.STABLE_ORDER, input), |
| ImmutableSet.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 void computeKey(ActionKeyContext actionKeyContext, Fingerprint fp) { |
| fp.addInt(42); |
| } |
| } |
| |
| private abstract static class SingleOutputSkyframeAwareAction extends SingleOutputAction |
| implements SkyframeAwareAction<IOException> { |
| SingleOutputSkyframeAwareAction(@Nullable Artifact input, Artifact output) { |
| super(input, output); |
| } |
| |
| @Override |
| public boolean executeUnconditionally() { |
| return true; |
| } |
| |
| @Override |
| public boolean isVolatile() { |
| return true; |
| } |
| |
| @Override |
| public Class<IOException> getExceptionType() { |
| return IOException.class; |
| } |
| } |
| |
| /** |
| * 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 ActionResult execute(ActionExecutionContext actionExecutionContext) |
| throws ActionExecutionException, InterruptedException { |
| writeOutput(null, "gen1"); |
| return ActionResult.EMPTY; |
| } |
| }); |
| |
| registerAction( |
| new SingleOutputSkyframeAwareAction(genFile1, genFile2) { |
| |
| @Override |
| public ImmutableList<SkyKey> getDirectSkyframeDependencies() { |
| return ImmutableList.of(); |
| } |
| |
| @Override |
| public Object processSkyframeValues( |
| ImmutableList<? extends SkyKey> keys, |
| Map<SkyKey, ValueOrException<IOException>> values, |
| boolean valuesMissing) { |
| assertThat(keys).isEmpty(); |
| assertThat(values).isEmpty(); |
| assertThat(valuesMissing).isFalse(); |
| return null; |
| } |
| |
| @Override |
| public ImmutableGraph<SkyKey> getSkyframeDependenciesForRewinding(SkyKey self) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public ActionResult execute(ActionExecutionContext actionExecutionContext) |
| throws ActionExecutionException { |
| writeOutput(readInput(), "gen2"); |
| return ActionResult.EMPTY; |
| } |
| }); |
| |
| builder.buildArtifacts( |
| reporter, |
| ImmutableSet.of(genFile2), |
| null, |
| null, |
| null, |
| null, |
| null, |
| executor, |
| null, |
| null, |
| options, |
| null, |
| null); |
| } |
| } |