Wrap StatusRuntimeExceptions from GrpcRemoteCache

Exceptions that occur during remote interactions are expected to be
wrapped in IOException for observation by the RemoteSpawn{Runner,Cache}
layers.

Fixes #7856

Closes #7860.

PiperOrigin-RevId: 240793745
diff --git a/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java b/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java
index e052071..523a414 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java
@@ -193,7 +193,7 @@
 
             @Override
             public void onFailure(Throwable t) {
-              dirDownload.setException(t);
+              dirDownload.setException(new IOException(t));
             }
           },
           MoreExecutors.directExecutor());
@@ -310,7 +310,16 @@
     private final boolean isExecutable;
 
     public FuturePathBooleanTuple(ListenableFuture<?> future, Path path, boolean isExecutable) {
-      this.future = future;
+      this.future = Futures.catchingAsync(
+          future,
+          Throwable.class,
+          (t) -> {
+            if (t instanceof IOException) {
+              return Futures.immediateFailedFuture(t);
+            }
+            return Futures.immediateFailedFuture(new IOException(t));
+          },
+          MoreExecutors.directExecutor());
       this.path = path;
       this.isExecutable = isExecutable;
     }
diff --git a/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java b/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java
index 7d459b5..3b185ff 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/GrpcRemoteCache.java
@@ -151,7 +151,11 @@
   private ListenableFuture<FindMissingBlobsResponse> getMissingDigests(
       FindMissingBlobsRequest request) throws IOException, InterruptedException {
     Context ctx = Context.current();
-    return retrier.executeAsync(() -> ctx.call(() -> casFutureStub().findMissingBlobs(request)));
+    try {
+      return retrier.executeAsync(() -> ctx.call(() -> casFutureStub().findMissingBlobs(request)));
+    } catch (StatusRuntimeException e) {
+      throw new IOException(e);
+    }
   }
 
   private ImmutableSet<Digest> getMissingDigests(Iterable<Digest> digests)