Skip to content

Commit 85e6cff

Browse files
fmeumcopybara-github
authored andcommitted
Strictify exception types in remote package to reduce wrapping
This makes it easier to understand what kinds of exceptions can be thrown by a particular function and avoids wrapping in `RuntimeException` that can turn regular failures into crashes. In particular, this resolves an issue seen on a PR in which `RemoteSpawnRunner` turned an `ExecException` raised by `RES.uploadInputsIfNotPresent` due to a lost input into a `RuntimeException`, thus resulting in a crash. It's not clear whether this behavior can also happen at HEAD or whether all callers unwrap the `RuntimeException`, so this change doesn't necessarily fix a bug. Work towards #21378 Closes #26650. PiperOrigin-RevId: 792329428 Change-Id: Iced921ce8633926e6488d38502c9b93c9fffa393
1 parent 757240f commit 85e6cff

File tree

6 files changed

+68
-74
lines changed

6 files changed

+68
-74
lines changed

src/main/java/com/google/devtools/build/lib/remote/BazelOutputService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ private FinalizeBuildResponse finalizeBuild(FinalizeBuildRequest request)
360360

361361
@Override
362362
public void finalizeAction(Action action, OutputMetadataStore outputMetadataStore)
363-
throws IOException, EnvironmentalExecException, InterruptedException {
363+
throws IOException, InterruptedException {
364364
var execRoot = execRootSupplier.get();
365365
var outputPath = outputPathSupplier.get();
366366

src/main/java/com/google/devtools/build/lib/remote/ReferenceCountedChannel.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ public ServerCapabilities getServerCapabilities() throws IOException {
7878
try (var s = Profiler.instance().profile("getServerCapabilities")) {
7979
return blockingGet(
8080
withChannelConnection(ChannelConnectionWithServerCapabilities::getServerCapabilities));
81-
} catch (ExecutionException e) {
82-
throw new IOException(e);
8381
} catch (InterruptedException e) {
8482
Thread.currentThread().interrupt();
8583
throw new IOException(e);
@@ -90,21 +88,30 @@ public boolean isShutdown() {
9088
return dynamicConnectionPool.isClosed();
9189
}
9290

91+
/**
92+
* A specialized {@link Function} that can only throw {@link IOException} and {@link
93+
* InterruptedException}.
94+
*/
95+
@FunctionalInterface
96+
public interface IOFunction<T, R> extends Function<T, R> {
97+
@Override
98+
R apply(T t) throws IOException, InterruptedException;
99+
}
100+
93101
@CheckReturnValue
94102
public <T> ListenableFuture<T> withChannelFuture(
95-
Function<Channel, ? extends ListenableFuture<T>> source) {
103+
IOFunction<Channel, ? extends ListenableFuture<T>> source) {
96104
return RxFutures.toListenableFuture(
97105
withChannel(channel -> RxFutures.toSingle(() -> source.apply(channel), directExecutor())));
98106
}
99107

100-
public <T> T withChannelBlocking(Function<Channel, T> source)
101-
throws ExecutionException, IOException, InterruptedException {
108+
public <T> T withChannelBlocking(IOFunction<Channel, T> source)
109+
throws IOException, InterruptedException {
102110
return blockingGet(withChannel(channel -> Single.just(source.apply(channel))));
103111
}
104112

105113
// prevents rxjava silent possible wrap of RuntimeException and misinterpretation
106-
private <T> T blockingGet(Single<T> single)
107-
throws ExecutionException, IOException, InterruptedException {
114+
private <T> T blockingGet(Single<T> single) throws IOException, InterruptedException {
108115
SettableFuture<T> future = SettableFuture.create();
109116
single.subscribe(
110117
new SingleObserver<T>() {
@@ -135,8 +142,9 @@ public void onSubscribe(Disposable d) {
135142
} catch (ExecutionException e) {
136143
Throwable cause = e.getCause();
137144
Throwables.throwIfInstanceOf(cause, IOException.class);
145+
Throwables.throwIfInstanceOf(cause, InterruptedException.class);
138146
Throwables.throwIfUnchecked(cause);
139-
throw e;
147+
throw new IllegalStateException("Unexpected exception type", cause);
140148
}
141149
}
142150

src/main/java/com/google/devtools/build/lib/remote/RemoteRetrier.java

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,13 @@
1818

1919
import com.google.common.annotations.VisibleForTesting;
2020
import com.google.common.base.Preconditions;
21-
import com.google.common.base.Throwables;
2221
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
2322
import com.google.devtools.build.lib.remote.Retrier.ResultClassifier.Result;
2423
import com.google.devtools.build.lib.remote.options.RemoteOptions;
2524
import io.grpc.Status;
2625
import io.grpc.StatusRuntimeException;
2726
import java.io.IOException;
2827
import java.time.Duration;
29-
import java.util.concurrent.Callable;
3028
import java.util.concurrent.ThreadLocalRandom;
3129
import java.util.function.Supplier;
3230
import javax.annotation.Nullable;
@@ -106,33 +104,13 @@ public RemoteRetrier(
106104
super(backoff, resultClassifier, retryScheduler, circuitBreaker, sleeper);
107105
}
108106

109-
/**
110-
* Execute a callable with retries. {@link IOException} and {@link InterruptedException} are
111-
* propagated directly to the caller. All other exceptions are wrapped in {@link
112-
* RuntimeException}.
113-
*/
107+
/** Execute a callable with retries. */
114108
@Override
115-
public <T> T execute(Callable<T> call) throws IOException, InterruptedException {
109+
public <T, E extends Exception> T execute(RetryableCallable<T, E> call)
110+
throws E, IOException, InterruptedException {
116111
return execute(call, newBackoff());
117112
}
118113

119-
/**
120-
* Execute a callable with retries and given {@link Backoff}. {@link IOException} and {@link
121-
* InterruptedException} are propagated directly to the caller. All other exceptions are wrapped
122-
* in {@link RuntimeException}.
123-
*/
124-
@Override
125-
public <T> T execute(Callable<T> call, Backoff backoff) throws IOException, InterruptedException {
126-
try {
127-
return super.execute(call, backoff);
128-
} catch (Exception e) {
129-
Throwables.throwIfInstanceOf(e, IOException.class);
130-
Throwables.throwIfInstanceOf(e, InterruptedException.class);
131-
Throwables.throwIfUnchecked(e);
132-
throw new RuntimeException(e);
133-
}
134-
}
135-
136114
/** Backoff strategy that backs off exponentially. */
137115
public static class ExponentialBackoff implements Backoff {
138116

@@ -154,8 +132,8 @@ public static class ExponentialBackoff implements Backoff {
154132
* jitter, and 1 providing a duration that is 0-200% of the non-jittered duration.
155133
* @param maxAttempts Maximal times to attempt a retry 0 means no retries.
156134
*/
157-
ExponentialBackoff(Duration initial, Duration max, double multiplier, double jitter,
158-
int maxAttempts) {
135+
ExponentialBackoff(
136+
Duration initial, Duration max, double multiplier, double jitter, int maxAttempts) {
159137
Preconditions.checkArgument(multiplier > 1, "multipler must be > 1");
160138
Preconditions.checkArgument(jitter >= 0 && jitter <= 1, "jitter must be in the range (0, 1)");
161139
Preconditions.checkArgument(maxAttempts >= 0, "maxAttempts must be >= 0");

src/main/java/com/google/devtools/build/lib/remote/Retrier.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,33 +224,38 @@ public Retrier(
224224
this.sleeper = sleeper;
225225
}
226226

227-
ListeningScheduledExecutorService getRetryService() {
228-
return retryService;
227+
/** A {@link Callable} that can be retried in case of transient failure. */
228+
@FunctionalInterface
229+
public interface RetryableCallable<T, E extends Exception> extends Callable<T> {
230+
@Override
231+
T call() throws IOException, InterruptedException, E;
229232
}
230233

231234
/**
232-
* Execute a {@link Callable}, retrying execution in case of transient failure and returning the
233-
* result in case of success.
235+
* Execute a {@link RetryableCallable}, retrying execution in case of transient failure and
236+
* returning the result in case of success.
234237
*/
235-
public <T> T execute(Callable<T> call) throws Exception {
238+
public <T, E extends Exception> T execute(RetryableCallable<T, E> call)
239+
throws E, IOException, InterruptedException {
236240
return execute(call, newBackoff());
237241
}
238242

239243
/**
240-
* Execute a {@link Callable}, retrying execution in case of transient failure and returning the
241-
* result in case of success with give {@link Backoff}.
244+
* Execute a {@link RetryableCallable}, retrying execution in case of transient failure and
245+
* returning the result in case of success with give {@link Backoff}.
242246
*
243247
* <p>{@link InterruptedException} is not retried.
244248
*
245249
* @param call the {@link Callable} to execute.
246-
* @throws Exception if the {@code call} didn't succeed within the framework specified by {@code
247-
* backoffSupplier} and {@code resultClassifier}.
250+
* @throws E or {@link IOException} if the {@code call} didn't succeed within the framework
251+
* specified by {@code backoffSupplier} and {@code resultClassifier}.
248252
* @throws CircuitBreakerException in case a call was rejected because the circuit breaker
249253
* tripped.
250254
* @throws InterruptedException if the {@code call} throws an {@link InterruptedException} or the
251255
* current thread's interrupted flag is set.
252256
*/
253-
public <T> T execute(Callable<T> call, Backoff backoff) throws Exception {
257+
public <T, E extends Exception> T execute(RetryableCallable<T, E> call, Backoff backoff)
258+
throws E, IOException, InterruptedException {
254259
while (true) {
255260
State circuitState = circuitBreaker.state();
256261
if (State.REJECT_CALLS.equals(circuitState)) {

src/main/java/com/google/devtools/build/lib/remote/downloader/GrpcRemoteDownloader.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,18 @@ public void download(
183183
}
184184
final Digest blobDigest = response.getBlobDigest();
185185

186-
retrier.execute(
187-
() -> {
188-
try (OutputStream out = newOutputStream(destination, checksum)) {
189-
Utils.getFromFuture(
190-
cacheClient.downloadBlob(remoteActionExecutionContext, blobDigest, out));
191-
} catch (OutputDigestMismatchException e) {
192-
e.setOutputPath(destination.getPathString());
193-
throw e;
194-
}
195-
return null;
196-
});
186+
var unused =
187+
retrier.execute(
188+
() -> {
189+
try (OutputStream out = newOutputStream(destination, checksum)) {
190+
Utils.getFromFuture(
191+
cacheClient.downloadBlob(remoteActionExecutionContext, blobDigest, out));
192+
} catch (OutputDigestMismatchException e) {
193+
e.setOutputPath(destination.getPathString());
194+
throw e;
195+
}
196+
return null;
197+
});
197198

198199
} catch (StatusRuntimeException | IOException e) {
199200
eventHandler.post(new FetchEvent(eventUri, FetchId.Downloader.GRPC, /* success= */ false));

src/test/java/com/google/devtools/build/lib/remote/RetrierTest.java

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -158,19 +158,20 @@ public void nestedRetriesShouldWork() throws Exception {
158158
AtomicInteger attemptsLvl1 = new AtomicInteger();
159159
AtomicInteger attemptsLvl2 = new AtomicInteger();
160160
try {
161-
r.execute(
162-
() -> {
163-
attemptsLvl0.incrementAndGet();
164-
return r.execute(
165-
() -> {
166-
attemptsLvl1.incrementAndGet();
167-
return r.execute(
168-
() -> {
169-
attemptsLvl2.incrementAndGet();
170-
throw new Exception("failure message");
171-
});
172-
});
173-
});
161+
var unused =
162+
r.execute(
163+
() -> {
164+
attemptsLvl0.incrementAndGet();
165+
return r.execute(
166+
() -> {
167+
attemptsLvl1.incrementAndGet();
168+
return r.execute(
169+
() -> {
170+
attemptsLvl2.incrementAndGet();
171+
throw new Exception("failure message");
172+
});
173+
});
174+
});
174175
} catch (Exception e) {
175176
assertThat(e).hasMessageThat().isEqualTo("failure message");
176177
assertThat(attemptsLvl0.get()).isEqualTo(2);
@@ -229,10 +230,11 @@ public void circuitBreakerHalfOpenIsNotRetried() throws Exception {
229230
cb.trialCall();
230231

231232
try {
232-
r.execute(
233-
() -> {
234-
throw new Exception("call failed");
235-
});
233+
var unused =
234+
r.execute(
235+
() -> {
236+
throw new Exception("call failed");
237+
});
236238
} catch (Exception expected) {
237239
// Intentionally left empty.
238240
}

0 commit comments

Comments
 (0)