larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 1 | // 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 | |
| 15 | package com.google.devtools.build.lib.dynamic; |
| 16 | |
| 17 | import static com.google.common.base.Preconditions.checkState; |
| 18 | import static com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode.LOCAL; |
| 19 | |
| 20 | import com.google.common.annotations.VisibleForTesting; |
| 21 | import com.google.common.collect.ImmutableList; |
| 22 | import com.google.common.flogger.GoogleLogger; |
| 23 | import com.google.common.util.concurrent.MoreExecutors; |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 24 | import com.google.devtools.build.lib.actions.ActionExecutionContext; |
| 25 | import com.google.devtools.build.lib.actions.DynamicStrategyRegistry; |
| 26 | import com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode; |
| 27 | import com.google.devtools.build.lib.actions.ExecException; |
| 28 | import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy; |
| 29 | import com.google.devtools.build.lib.actions.Spawn; |
| 30 | import com.google.devtools.build.lib.actions.SpawnResult; |
| 31 | import com.google.devtools.build.lib.actions.SpawnResult.Status; |
| 32 | import com.google.devtools.build.lib.actions.SpawnStrategy; |
| 33 | import com.google.devtools.build.lib.dynamic.DynamicExecutionModule.IgnoreFailureCheck; |
| 34 | import com.google.devtools.build.lib.util.io.FileOutErr; |
larsrc | 88f605c | 2022-01-17 17:27:08 -0800 | [diff] [blame^] | 35 | import java.time.Duration; |
| 36 | import java.time.Instant; |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 37 | import java.util.Optional; |
| 38 | import java.util.concurrent.Future; |
| 39 | import java.util.concurrent.atomic.AtomicBoolean; |
| 40 | import java.util.concurrent.atomic.AtomicReference; |
| 41 | import java.util.function.Function; |
| 42 | import 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 |
| 49 | class 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; |
larsrc | 88f605c | 2022-01-17 17:27:08 -0800 | [diff] [blame^] | 56 | private final Instant creationTime = Instant.now(); |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 57 | |
| 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) { |
larsrc | f7ded2c | 2022-01-12 02:06:53 -0800 | [diff] [blame] | 66 | super(actionExecutionContext, spawn, strategyThatCancelled, options); |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 67 | this.ignoreFailureCheck = ignoreFailureCheck; |
| 68 | this.getExtraSpawnForLocalExecution = getExtraSpawnForLocalExecution; |
| 69 | this.delayLocalExecution = delayLocalExecution; |
| 70 | } |
| 71 | |
larsrc | f7ded2c | 2022-01-12 02:06:53 -0800 | [diff] [blame] | 72 | @Override |
| 73 | public DynamicMode getMode() { |
| 74 | return LOCAL; |
| 75 | } |
| 76 | |
larsrc | 88f605c | 2022-01-17 17:27:08 -0800 | [diff] [blame^] | 77 | public Duration getAge() { |
| 78 | return Duration.between(creationTime, Instant.now()); |
| 79 | } |
| 80 | |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 81 | /** |
| 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. */ |
larsrc | 6f89288 | 2022-01-12 09:22:15 -0800 | [diff] [blame] | 141 | protected void prepareFuture(RemoteBranch remoteBranch) { |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 142 | // 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()); |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 156 | } |
| 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( |
larsrc | f7ded2c | 2022-01-12 02:06:53 -0800 | [diff] [blame] | 177 | remoteBranch, this, strategyThatCancelled, options, this.context); |
larsrc | eaa38f5 | 2022-01-11 13:05:32 -0800 | [diff] [blame] | 178 | }, |
| 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 | } |