Skip to content

Commit c7d2616

Browse files
Googlercopybara-github
Googler
authored andcommitted
Fix the case where if all strategies for one branch of dynamic execution fail to accept (that is, refuse to even take) the action given, the whole action fails. Instead of seeing whether the other branch can run and the action that that it succeeded.
NOTE: This is _not_ about the local/remote execution branch trying to run it and failing. Obviously that should fail the whole dynamic execution. This is about none of the strategies of one branch even stating that they can run it (based on what they return from `canExec`) ### Use cases #### Local execution form some tasks requires specific architecture and/or OS If the local execution branch requires a specific architecture/OS for some action, and users' bazel is being run a machine that isn't that combination (assuming the local strategy given is already setup to return `false` in `canExec` in this case). Previously the whole execution of the dynamic execution would fail without even trying to see if remote execution succeeded. Now it will gracefully fallback to just waiting on remote strategy. #### Conditional usage of the `worker` strategy in the face of multiple action definitions for a given mnemonic To conditionally use workers for supported actions but still simultaneously kick off a dynamic run, the options`--stragegy=TaskMnemonic=dynamic --dynamic_local_strategy=TaskMnemonic=worker --dynamic_remote_strategy=TaskMnemonic=remote` can be given. Then any action with with the mnemonic `TaskMnemonic` that can't support `worker` execution (xor `remote` execution) will wait for the other execution branch to complete. If neither set of strategies can run the action, then the task fails. Previously, this would have failed if, for example, the `worker` strategy cannot handle the action, even if `remote` could have. This at first glance seems silly as if you know TaskMnemonic doesn't have a worker enabled implementation, why specify `worker` as the local strategy? But keep in mind any action can declare most any mnemonic. In this example, say that first rule doing a TaskMnemonic is worker enabled, so you add the flags. But then someone makes a bazel/starlark rule with the same mnemonic but different implementation (for example, a "mimic" rule), and that new one doesn't support worker. Then this becomes a case of "partial" worker support for a mnemonic. RELNOTES: If all strategies of one branch (the local or remote execution branch) of the `dynamic` strategy fail to even accept (via the response they give from `canExec`) the action, `dynamic` will now try to see if the other branch can accept it. (Trying to run it and it failing will still cause a failure if it was the first result, this is about strategies claiming they can't even try the action) PiperOrigin-RevId: 374265582
1 parent f6a822f commit c7d2616

File tree

5 files changed

+384
-47
lines changed

5 files changed

+384
-47
lines changed

CONTRIBUTORS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,5 @@ Jonathan Dierksen <[email protected]>
108108
Tony Aiuto <[email protected]>
109109
Andy Scott <[email protected]>
110110
Jamie Snape <[email protected]>
111-
Irina Chernushina <[email protected]>
111+
Irina Chernushina <[email protected]>
112+
C. Sean Young <[email protected]>

src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java

Lines changed: 149 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,17 @@
3636
import com.google.devtools.build.lib.actions.SpawnResult;
3737
import com.google.devtools.build.lib.actions.SpawnResult.Status;
3838
import com.google.devtools.build.lib.actions.SpawnStrategy;
39+
import com.google.devtools.build.lib.actions.UserExecException;
3940
import com.google.devtools.build.lib.events.Event;
4041
import com.google.devtools.build.lib.exec.ExecutionPolicy;
42+
import com.google.devtools.build.lib.server.FailureDetails;
4143
import com.google.devtools.build.lib.server.FailureDetails.DynamicExecution;
4244
import com.google.devtools.build.lib.server.FailureDetails.DynamicExecution.Code;
4345
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
4446
import com.google.devtools.build.lib.util.io.FileOutErr;
4547
import com.google.devtools.build.lib.vfs.Path;
4648
import java.io.IOException;
49+
import java.util.List;
4750
import java.util.Optional;
4851
import java.util.concurrent.Callable;
4952
import java.util.concurrent.CancellationException;
@@ -276,11 +279,13 @@ static ImmutableList<SpawnResult> waitBranches(
276279
String.format(
277280
"Neither branch of %s cancelled the other one.",
278281
spawn.getResourceOwner().getPrimaryOutput().prettyPrint()));
279-
} else if (remoteResult != null) {
280-
return remoteResult;
281282
} else if (localResult != null) {
282283
return localResult;
284+
} else if (remoteResult != null) {
285+
return remoteResult;
283286
} else {
287+
// TODO(b/173153395): Sometimes gets thrown for currently unknown reasons.
288+
// (sometimes happens in relation to the whole dynamic execution being cancelled)
284289
throw new AssertionError(
285290
String.format(
286291
"Neither branch of %s completed. Local was %scancelled and remote was %scancelled",
@@ -331,18 +336,142 @@ static void verifyAvailabilityInfo(DynamicExecutionOptions options, Spawn spawn)
331336
}
332337
}
333338

339+
private static boolean canExecLocalSpawn(
340+
Spawn spawn,
341+
ExecutionPolicy executionPolicy,
342+
ActionContext.ActionContextRegistry actionContextRegistry,
343+
DynamicStrategyRegistry dynamicStrategyRegistry) {
344+
if (!executionPolicy.canRunLocally()) {
345+
return false;
346+
}
347+
List<SandboxedSpawnStrategy> localStrategies =
348+
dynamicStrategyRegistry.getDynamicSpawnActionContexts(
349+
spawn, DynamicStrategyRegistry.DynamicMode.LOCAL);
350+
return localStrategies.stream()
351+
.anyMatch(
352+
s ->
353+
(s.canExec(spawn, actionContextRegistry)
354+
|| s.canExecWithLegacyFallback(spawn, actionContextRegistry)));
355+
}
356+
357+
private boolean canExecLocal(
358+
Spawn spawn,
359+
ExecutionPolicy mainSpawnExecutionPolicy,
360+
ActionContext.ActionContextRegistry actionContextRegistry,
361+
DynamicStrategyRegistry dynamicStrategyRegistry) {
362+
if (!canExecLocalSpawn(
363+
spawn, mainSpawnExecutionPolicy, actionContextRegistry, dynamicStrategyRegistry)) {
364+
return false;
365+
}
366+
// Present if there is a extra local spawn. Unset if not.
367+
Optional<Boolean> canLocalSpawn =
368+
getExtraSpawnForLocalExecution
369+
.apply(spawn)
370+
.map(
371+
extraSpawn ->
372+
canExecLocalSpawn(
373+
extraSpawn,
374+
getExecutionPolicy.apply(extraSpawn),
375+
actionContextRegistry,
376+
dynamicStrategyRegistry));
377+
return canLocalSpawn.orElse(true);
378+
}
379+
380+
private static boolean canExecRemote(
381+
Spawn spawn,
382+
ExecutionPolicy executionPolicy,
383+
ActionContext.ActionContextRegistry actionContextRegistry,
384+
DynamicStrategyRegistry dynamicStrategyRegistry) {
385+
if (!executionPolicy.canRunRemotely()) {
386+
return false;
387+
}
388+
List<SandboxedSpawnStrategy> remoteStrategies =
389+
dynamicStrategyRegistry.getDynamicSpawnActionContexts(
390+
spawn, DynamicStrategyRegistry.DynamicMode.REMOTE);
391+
return remoteStrategies.stream().anyMatch(s -> s.canExec(spawn, actionContextRegistry));
392+
}
393+
394+
@Override
395+
public boolean canExec(Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) {
396+
ExecutionPolicy executionPolicy = getExecutionPolicy.apply(spawn);
397+
DynamicStrategyRegistry dynamicStrategyRegistry =
398+
actionContextRegistry.getContext(DynamicStrategyRegistry.class);
399+
400+
return canExecLocal(spawn, executionPolicy, actionContextRegistry, dynamicStrategyRegistry)
401+
|| canExecRemote(spawn, executionPolicy, actionContextRegistry, dynamicStrategyRegistry);
402+
}
403+
404+
/**
405+
* Returns an error string for being unable to execute locally and/or remotely the given execution
406+
* state.
407+
*
408+
* <p>Usage note, this method is only to be called after an impossible condition is already
409+
* detected by the caller, as all this does is give an error string to put in the exception.
410+
*
411+
* @param spawn The action that needs to be executed
412+
* @param localAllowedBySpawnExecutionPolicy whether the execution policy for this spawn allows
413+
* trying local execution.
414+
* @param remoteAllowedBySpawnExecutionPolicy whether the execution policy for this spawn allows
415+
* trying remote execution.
416+
*/
417+
private static String getNoCanExecFailureMessage(
418+
Spawn spawn,
419+
boolean localAllowedBySpawnExecutionPolicy,
420+
boolean remoteAllowedBySpawnExecutionPolicy) {
421+
// TODO(b/188387840): Can't use Spawn.toString() here because tests report FakeOwner instances
422+
// as the resource owner, and those cause toStrings to throw if no primary output.
423+
// TODO(b/188402092): Even if the above is fixed, we still don't want to use Spawn.toString()
424+
// until the mnemonic is included in the output unconditionally. Too useful for the error
425+
// message.
426+
if (!localAllowedBySpawnExecutionPolicy && !remoteAllowedBySpawnExecutionPolicy) {
427+
return "Neither local nor remote execution allowed for action " + spawn.getMnemonic();
428+
} else if (!remoteAllowedBySpawnExecutionPolicy) {
429+
return "No usable dynamic_local_strategy found (and remote execution disabled) for action "
430+
+ spawn.getMnemonic();
431+
} else if (!localAllowedBySpawnExecutionPolicy) {
432+
return "No usable dynamic_remote_strategy found (and local execution disabled) for action "
433+
+ spawn.getMnemonic();
434+
} else {
435+
return "No usable dynamic_local_strategy or dynamic_remote_strategy found for action "
436+
+ spawn.getMnemonic();
437+
}
438+
}
439+
334440
@Override
335441
public ImmutableList<SpawnResult> exec(
336442
final Spawn spawn, final ActionExecutionContext actionExecutionContext)
337443
throws ExecException, InterruptedException {
338444
DynamicSpawnStrategy.verifyAvailabilityInfo(options, spawn);
339445
ExecutionPolicy executionPolicy = getExecutionPolicy.apply(spawn);
340-
if (executionPolicy.canRunLocallyOnly()) {
341-
return runLocally(spawn, actionExecutionContext, null);
342-
}
343-
if (executionPolicy.canRunRemotelyOnly()) {
446+
447+
DynamicStrategyRegistry dynamicStrategyRegistry =
448+
actionExecutionContext.getContext(DynamicStrategyRegistry.class);
449+
boolean localCanExec =
450+
canExecLocal(spawn, executionPolicy, actionExecutionContext, dynamicStrategyRegistry);
451+
452+
boolean remoteCanExec =
453+
canExecRemote(spawn, executionPolicy, actionExecutionContext, dynamicStrategyRegistry);
454+
455+
if (!localCanExec && !remoteCanExec) {
456+
FailureDetail failure =
457+
FailureDetail.newBuilder()
458+
.setMessage(
459+
getNoCanExecFailureMessage(
460+
spawn, executionPolicy.canRunLocally(), executionPolicy.canRunRemotely()))
461+
.setDynamicExecution(
462+
DynamicExecution.newBuilder().setCode(Code.NO_USABLE_STRATEGY_FOUND).build())
463+
.setSpawn(
464+
FailureDetails.Spawn.newBuilder()
465+
.setCode(FailureDetails.Spawn.Code.NO_USABLE_STRATEGY_FOUND)
466+
.build())
467+
.build();
468+
throw new UserExecException(failure);
469+
} else if (!localCanExec && remoteCanExec) {
344470
return runRemotely(spawn, actionExecutionContext, null);
471+
} else if (localCanExec && !remoteCanExec) {
472+
return runLocally(spawn, actionExecutionContext, null);
345473
}
474+
// else both can exec. Fallthrough to below.
346475

347476
// Semaphores to track termination of each branch. These are necessary to wait for the branch to
348477
// finish its own cleanup (e.g. terminating subprocesses) once it has been cancelled.
@@ -458,28 +587,6 @@ public ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
458587
}
459588
}
460589

461-
@Override
462-
public boolean canExec(Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) {
463-
DynamicStrategyRegistry dynamicStrategyRegistry =
464-
actionContextRegistry.getContext(DynamicStrategyRegistry.class);
465-
for (SandboxedSpawnStrategy strategy :
466-
dynamicStrategyRegistry.getDynamicSpawnActionContexts(
467-
spawn, DynamicStrategyRegistry.DynamicMode.LOCAL)) {
468-
if (strategy.canExec(spawn, actionContextRegistry)
469-
|| strategy.canExecWithLegacyFallback(spawn, actionContextRegistry)) {
470-
return true;
471-
}
472-
}
473-
for (SandboxedSpawnStrategy strategy :
474-
dynamicStrategyRegistry.getDynamicSpawnActionContexts(
475-
spawn, DynamicStrategyRegistry.DynamicMode.REMOTE)) {
476-
if (strategy.canExec(spawn, actionContextRegistry)) {
477-
return true;
478-
}
479-
}
480-
return false;
481-
}
482-
483590
@Override
484591
public void usedContext(ActionContext.ActionContextRegistry actionContextRegistry) {
485592
actionContextRegistry
@@ -496,6 +603,12 @@ private static FileOutErr getSuffixedFileOutErr(FileOutErr fileOutErr, String su
496603
outDir.getChild(outBaseName + suffix), errDir.getChild(errBaseName + suffix));
497604
}
498605

606+
/**
607+
* Try to run the given spawn locally.
608+
*
609+
* <p>Precondition: At least one {@code dynamic_local_strategy} returns {@code true} from its
610+
* {@link SpawnStrategy#canExec canExec} method for the given {@code spawn}.
611+
*/
499612
private ImmutableList<SpawnResult> runLocally(
500613
Spawn spawn,
501614
ActionExecutionContext actionExecutionContext,
@@ -539,12 +652,15 @@ private static ImmutableList<SpawnResult> runSpawnLocally(
539652
return strategy.exec(spawn, actionExecutionContext, stopConcurrentSpawns);
540653
}
541654
}
542-
throw new RuntimeException(
543-
String.format(
544-
"executorCreated not yet called or no default dynamic_local_strategy set for %s",
545-
spawn.getMnemonic()));
655+
throw new AssertionError("canExec passed but no usable local strategy for action " + spawn);
546656
}
547657

658+
/**
659+
* Try to run the given spawn locally.
660+
*
661+
* <p>Precondition: At least one {@code dynamic_remote_strategy} returns {@code true} from its
662+
* {@link SpawnStrategy#canExec canExec} method for the given {@code spawn}.
663+
*/
548664
private static ImmutableList<SpawnResult> runRemotely(
549665
Spawn spawn,
550666
ActionExecutionContext actionExecutionContext,
@@ -571,10 +687,7 @@ private static ImmutableList<SpawnResult> runRemotely(
571687
return results;
572688
}
573689
}
574-
throw new RuntimeException(
575-
String.format(
576-
"executorCreated not yet called or no default dynamic_remote_strategy set for %s",
577-
spawn.getMnemonic()));
690+
throw new AssertionError("canExec passed but no usable remote strategy for action " + spawn);
578691
}
579692

580693
/**

src/main/protobuf/failure_details.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,7 @@ message DynamicExecution {
10441044
XCODE_RELATED_PREREQ_UNMET = 1 [(metadata) = { exit_code: 36 }];
10451045
ACTION_LOG_MOVE_FAILURE = 2 [(metadata) = { exit_code: 1 }];
10461046
RUN_FAILURE = 3 [(metadata) = { exit_code: 1 }];
1047+
NO_USABLE_STRATEGY_FOUND = 4 [(metadata) = { exit_code: 2 }];
10471048
}
10481049

10491050
Code code = 1;

src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,22 @@ private class MockSpawnStrategy implements SandboxedSpawnStrategy {
125125

126126
private final DoExec doExecAfterStop;
127127

128+
private final boolean canExec;
129+
128130
MockSpawnStrategy(String name) {
129131
this(name, DoExec.NOTHING, DoExec.NOTHING);
130132
}
131133

132134
MockSpawnStrategy(String name, DoExec doExecBeforeStop, DoExec doExecAfterStop) {
135+
this(name, doExecBeforeStop, doExecAfterStop, true);
136+
}
137+
138+
MockSpawnStrategy(
139+
String name, DoExec doExecBeforeStop, DoExec doExecAfterStop, boolean canExec) {
133140
this.name = name;
134141
this.doExecBeforeStop = doExecBeforeStop;
135142
this.doExecAfterStop = doExecAfterStop;
143+
this.canExec = canExec;
136144
}
137145

138146
/** Helper to record an execution failure from within {@link #doExecBeforeStop}. */
@@ -189,7 +197,7 @@ public ImmutableList<SpawnResult> exec(
189197

190198
@Override
191199
public boolean canExec(Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) {
192-
return true;
200+
return canExec;
193201
}
194202

195203
@Nullable
@@ -552,6 +560,48 @@ public void actionSucceedsIfRemoteExecutionSucceedsEvenIfLocalFailsLater() throw
552560
assertThat(outErr.outAsLatin1()).doesNotContain("MockLocalSpawnStrategy");
553561
}
554562

563+
@Test
564+
public void actionSucceedsIfLocalExecutionSucceedsEvenIfRemoteRunsNothing() throws Exception {
565+
MockSpawnStrategy localStrategy = new MockSpawnStrategy("MockLocalSpawnStrategy");
566+
567+
MockSpawnStrategy remoteStrategy =
568+
new MockSpawnStrategy("MockRemoteSpawnStrategy", DoExec.NOTHING, DoExec.NOTHING, false);
569+
570+
StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy);
571+
572+
Spawn spawn = newDynamicSpawn();
573+
strategyAndContext.exec(spawn);
574+
575+
assertThat(localStrategy.getExecutedSpawn()).isEqualTo(spawn);
576+
assertThat(localStrategy.succeeded()).isTrue();
577+
assertThat(remoteStrategy.getExecutedSpawn()).isNull();
578+
assertThat(remoteStrategy.succeeded()).isFalse();
579+
580+
assertThat(outErr.outAsLatin1()).contains("output files written with MockLocalSpawnStrategy");
581+
assertThat(outErr.outAsLatin1()).doesNotContain("MockRemoteSpawnStrategy");
582+
}
583+
584+
@Test
585+
public void actionSucceedsIfRemoteExecutionSucceedsEvenIfLocalRunsNothing() throws Exception {
586+
MockSpawnStrategy localStrategy =
587+
new MockSpawnStrategy("MockLocalSpawnStrategy", DoExec.NOTHING, DoExec.NOTHING, false);
588+
589+
MockSpawnStrategy remoteStrategy = new MockSpawnStrategy("MockRemoteSpawnStrategy");
590+
591+
StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy);
592+
593+
Spawn spawn = newDynamicSpawn();
594+
strategyAndContext.exec(spawn);
595+
596+
assertThat(localStrategy.getExecutedSpawn()).isNull();
597+
assertThat(localStrategy.succeeded()).isFalse();
598+
assertThat(remoteStrategy.getExecutedSpawn()).isEqualTo(spawn);
599+
assertThat(remoteStrategy.succeeded()).isTrue();
600+
601+
assertThat(outErr.outAsLatin1()).contains("output files written with MockRemoteSpawnStrategy");
602+
assertThat(outErr.outAsLatin1()).doesNotContain("MockLocalSpawnStrategy");
603+
}
604+
555605
@Test
556606
public void actionFailsIfLocalFailsImmediatelyEvenIfRemoteSucceedsLater() throws Exception {
557607
CountDownLatch countDownLatch = new CountDownLatch(2);
@@ -662,6 +712,34 @@ public void actionFailsIfLocalAndRemoteFail() throws Exception {
662712
assertThat(remoteStrategy.succeeded()).isFalse();
663713
}
664714

715+
@Test
716+
public void actionFailsIfLocalAndRemoteRunNothing() throws Exception {
717+
MockSpawnStrategy localStrategy =
718+
new MockSpawnStrategy("MockLocalSpawnStrategy", DoExec.NOTHING, DoExec.NOTHING, false);
719+
720+
MockSpawnStrategy remoteStrategy =
721+
new MockSpawnStrategy("MockRemoteSpawnStrategy", DoExec.NOTHING, DoExec.NOTHING, false);
722+
723+
StrategyAndContext strategyAndContext = createSpawnStrategy(localStrategy, remoteStrategy);
724+
725+
Spawn spawn = newDynamicSpawn();
726+
ExecException e = assertThrows(UserExecException.class, () -> strategyAndContext.exec(spawn));
727+
728+
// Has "No usable", followed by both dynamic_local_strategy and dynamic_remote_strategy in,
729+
// followed by the action's mnemonic.
730+
String regexMatch =
731+
"[nN]o usable\\b.*\\bdynamic_local_strategy\\b.*\\bdynamic_remote_strategy\\b.*\\b"
732+
+ spawn.getMnemonic()
733+
+ "\\b";
734+
735+
assertThat(e).hasMessageThat().containsMatch(regexMatch);
736+
737+
assertThat(localStrategy.getExecutedSpawn()).isNull();
738+
assertThat(localStrategy.succeeded()).isFalse();
739+
assertThat(remoteStrategy.getExecutedSpawn()).isNull();
740+
assertThat(remoteStrategy.succeeded()).isFalse();
741+
}
742+
665743
@Test
666744
public void stopConcurrentSpawnsWaitForCompletion() throws Exception {
667745
CountDownLatch countDownLatch = new CountDownLatch(2);

0 commit comments

Comments
 (0)