blob: a6dfdfd1e65f150349af620590f9b053490415ef [file] [log] [blame]
larsrceaa38f52022-01-11 13:05:32 -08001// Copyright 2021 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.dynamic;
16
17import static com.google.common.base.Preconditions.checkState;
18import static com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode.LOCAL;
19
20import com.google.common.annotations.VisibleForTesting;
21import com.google.common.collect.ImmutableList;
22import com.google.common.flogger.GoogleLogger;
23import com.google.common.util.concurrent.MoreExecutors;
larsrceaa38f52022-01-11 13:05:32 -080024import com.google.devtools.build.lib.actions.ActionExecutionContext;
25import com.google.devtools.build.lib.actions.DynamicStrategyRegistry;
26import com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode;
27import com.google.devtools.build.lib.actions.ExecException;
28import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy;
29import com.google.devtools.build.lib.actions.Spawn;
30import com.google.devtools.build.lib.actions.SpawnResult;
31import com.google.devtools.build.lib.actions.SpawnResult.Status;
32import com.google.devtools.build.lib.actions.SpawnStrategy;
33import com.google.devtools.build.lib.dynamic.DynamicExecutionModule.IgnoreFailureCheck;
34import com.google.devtools.build.lib.util.io.FileOutErr;
larsrc88f605c2022-01-17 17:27:08 -080035import java.time.Duration;
36import java.time.Instant;
larsrceaa38f52022-01-11 13:05:32 -080037import java.util.Optional;
38import java.util.concurrent.Future;
39import java.util.concurrent.atomic.AtomicBoolean;
40import java.util.concurrent.atomic.AtomicReference;
41import java.util.function.Function;
42import javax.annotation.Nullable;
43
44/**
45 * The local version of a Branch. On top of normal Branch things, this handles delaying after remote
46 * cache hits and passing the extra-spawn function.
47 */
48@VisibleForTesting
49class LocalBranch extends Branch {
50 private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
51
52 private RemoteBranch remoteBranch;
53 private final IgnoreFailureCheck ignoreFailureCheck;
54 private final Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution;
55 private final AtomicBoolean delayLocalExecution;
larsrc88f605c2022-01-17 17:27:08 -080056 private final Instant creationTime = Instant.now();
larsrceaa38f52022-01-11 13:05:32 -080057
58 public LocalBranch(
59 ActionExecutionContext actionExecutionContext,
60 Spawn spawn,
61 AtomicReference<DynamicMode> strategyThatCancelled,
62 DynamicExecutionOptions options,
63 IgnoreFailureCheck ignoreFailureCheck,
64 Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution,
65 AtomicBoolean delayLocalExecution) {
larsrcf7ded2c2022-01-12 02:06:53 -080066 super(actionExecutionContext, spawn, strategyThatCancelled, options);
larsrceaa38f52022-01-11 13:05:32 -080067 this.ignoreFailureCheck = ignoreFailureCheck;
68 this.getExtraSpawnForLocalExecution = getExtraSpawnForLocalExecution;
69 this.delayLocalExecution = delayLocalExecution;
70 }
71
larsrcf7ded2c2022-01-12 02:06:53 -080072 @Override
73 public DynamicMode getMode() {
74 return LOCAL;
75 }
76
larsrc88f605c2022-01-17 17:27:08 -080077 public Duration getAge() {
78 return Duration.between(creationTime, Instant.now());
79 }
80
larsrceaa38f52022-01-11 13:05:32 -080081 /**
82 * Try to run the given spawn locally.
83 *
84 * <p>Precondition: At least one {@code dynamic_local_strategy} returns {@code true} from its
85 * {@link SpawnStrategy#canExec canExec} method for the given {@code spawn}.
86 */
87 static ImmutableList<SpawnResult> runLocally(
88 Spawn spawn,
89 ActionExecutionContext actionExecutionContext,
90 @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns,
91 Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution)
92 throws ExecException, InterruptedException {
93 ImmutableList<SpawnResult> spawnResult =
94 runSpawnLocally(spawn, actionExecutionContext, stopConcurrentSpawns);
95 if (spawnResult.stream().anyMatch(result -> result.status() != Status.SUCCESS)) {
96 return spawnResult;
97 }
98
99 Optional<Spawn> extraSpawn = getExtraSpawnForLocalExecution.apply(spawn);
100 if (!extraSpawn.isPresent()) {
101 return spawnResult;
102 }
103
104 // The remote branch was already cancelled -- we are holding the output lock during the
105 // execution of the extra spawn.
106 ImmutableList<SpawnResult> extraSpawnResult =
107 runSpawnLocally(extraSpawn.get(), actionExecutionContext, null);
108 return ImmutableList.<SpawnResult>builderWithExpectedSize(
109 spawnResult.size() + extraSpawnResult.size())
110 .addAll(spawnResult)
111 .addAll(extraSpawnResult)
112 .build();
113 }
114
115 private static ImmutableList<SpawnResult> runSpawnLocally(
116 Spawn spawn,
117 ActionExecutionContext actionExecutionContext,
118 @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns)
119 throws ExecException, InterruptedException {
120 DynamicStrategyRegistry dynamicStrategyRegistry =
121 actionExecutionContext.getContext(DynamicStrategyRegistry.class);
122
123 for (SandboxedSpawnStrategy strategy :
124 dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, LOCAL)) {
125 if (strategy.canExec(spawn, actionExecutionContext)
126 || strategy.canExecWithLegacyFallback(spawn, actionExecutionContext)) {
127 ImmutableList<SpawnResult> results =
128 strategy.exec(spawn, actionExecutionContext, stopConcurrentSpawns);
129 if (results == null) {
130 logger.atWarning().log(
131 "Local strategy %s for %s target %s returned null, which it shouldn't do.",
132 strategy, spawn.getMnemonic(), spawn.getResourceOwner().prettyPrint());
133 }
134 return results;
135 }
136 }
137 throw new AssertionError("canExec passed but no usable local strategy for action " + spawn);
138 }
139
140 /** Sets up the {@link Future} used in the local branch to know what remote branch to cancel. */
larsrc6f892882022-01-12 09:22:15 -0800141 protected void prepareFuture(RemoteBranch remoteBranch) {
larsrceaa38f52022-01-11 13:05:32 -0800142 // TODO(b/203094728): Maybe generify this method and move it up.
143 this.remoteBranch = remoteBranch;
144 future.addListener(
145 () -> {
146 if (starting.compareAndSet(true, false)) {
147 // If the local branch got cancelled before even starting, we release its semaphore
148 // for it.
149 done.release();
150 }
151 if (!future.isCancelled()) {
152 remoteBranch.cancel();
153 }
154 },
155 MoreExecutors.directExecutor());
larsrceaa38f52022-01-11 13:05:32 -0800156 }
157
158 @Override
159 ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
160 throws InterruptedException, ExecException {
161 try {
162 if (!starting.compareAndSet(true, false)) {
163 // If we ever get here, it's because we were cancelled early and the listener
164 // ran first. Just make sure that's the case.
165 checkState(Thread.interrupted());
166 throw new InterruptedException();
167 }
168 if (delayLocalExecution.get()) {
169 Thread.sleep(options.localExecutionDelay);
170 }
171 return runLocally(
172 spawn,
173 context,
174 (exitCode, errorMessage, outErr) -> {
175 maybeIgnoreFailure(exitCode, errorMessage, outErr);
176 DynamicSpawnStrategy.stopBranch(
larsrcf7ded2c2022-01-12 02:06:53 -0800177 remoteBranch, this, strategyThatCancelled, options, this.context);
larsrceaa38f52022-01-11 13:05:32 -0800178 },
179 getExtraSpawnForLocalExecution);
180 } catch (DynamicInterruptedException e) {
181 if (options.debugSpawnScheduler) {
182 logger.atInfo().log(
183 "Local branch of %s self-cancelling with %s: '%s'",
184 spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
185 }
186 // This exception can be thrown due to races in stopBranch(), in which case
187 // the branch that lost the race may not have been cancelled yet. Cancel it here
188 // to prevent the listener from cross-cancelling.
189 cancel();
190 throw e;
191 } catch (
192 @SuppressWarnings("InterruptedExceptionSwallowed")
193 Throwable e) {
194 if (options.debugSpawnScheduler) {
195 logger.atInfo().log(
196 "Local branch of %s failed with %s: '%s'",
197 spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
198 }
199 throw e;
200 } finally {
201 done.release();
202 }
203 }
204
205 /**
206 * Called when execution failed, to check if we should allow the other branch to continue instead
207 * of failing.
208 *
209 * @throws DynamicInterruptedException if this failure can be ignored in favor of the result of
210 * the other branch.
211 */
212 protected void maybeIgnoreFailure(int exitCode, String errorMessage, FileOutErr outErr)
213 throws DynamicInterruptedException {
214 if (exitCode == 0 || ignoreFailureCheck == null) {
215 return;
216 }
217 synchronized (spawn) {
218 if (ignoreFailureCheck.canIgnoreFailure(spawn, exitCode, errorMessage, outErr, true)) {
219 throw new DynamicInterruptedException(
220 String.format(
221 "Local branch of %s cancelling self in favor of remote.",
222 spawn.getResourceOwner().prettyPrint()));
223 }
224 }
225 }
226}