Remote: handle early return of compressed blobs uploads (#14885)

This is an implementation of this REAPI spec update:
https://github.com/bazelbuild/remote-apis/pull/213

Here's a bazel-remote build that can be used to test this change:
https://github.com/buchgr/bazel-remote/pull/527

Fixes #14654

Closes #14870.

PiperOrigin-RevId: 430167812
(cherry picked from commit d184e4883bb7fc21de2f7aeea4304994de27e9ea)

Co-authored-by: Mostyn Bramley-Moore <mostyn@antipode.se>
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index a95cecd..6936c81 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -20,6 +20,7 @@
 Kevin Bierhoff <kmb@google.com>
 Klaas Boesche <klaasb@google.com>
 Phil Bordelon <sunfall@google.com>
+Mostyn Bramley-Moore <mostyn@antipode.se>
 Jon Brandvein <brandjon@google.com>
 Volker Braun <vbraun.name@gmail.com>
 Thomas Broyer <t.broyer@ltgt.net>
diff --git a/src/main/java/com/google/devtools/build/lib/remote/ByteStreamUploader.java b/src/main/java/com/google/devtools/build/lib/remote/ByteStreamUploader.java
index d4aa190..220a304 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/ByteStreamUploader.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/ByteStreamUploader.java
@@ -440,14 +440,33 @@
               // level/algorithm, so we cannot know the expected committed offset
               long committedSize = committedOffset.get();
               long expected = chunker.getOffset();
-              if (!chunker.hasNext() && committedSize != expected) {
+
+              if (committedSize == expected) {
+                // Both compressed and uncompressed uploads can succeed
+                // with this result.
+                return immediateVoidFuture();
+              }
+
+              if (chunker.isCompressed()) {
+                if (committedSize == -1) {
+                  // Returned early, blob already available.
+                  return immediateVoidFuture();
+                }
+
                 String message =
                     format(
-                        "write incomplete: committed_size %d for %d total",
+                        "compressed write incomplete: committed_size %d is neither -1 nor total %d",
                         committedSize, expected);
                 return Futures.immediateFailedFuture(new IOException(message));
               }
+
+              // Uncompressed upload failed.
+              String message =
+                  format(
+                      "write incomplete: committed_size %d for %d total", committedSize, expected);
+              return Futures.immediateFailedFuture(new IOException(message));
             }
+
             return immediateVoidFuture();
           },
           MoreExecutors.directExecutor());