3535import com.google.api.core.ApiFutureCallback;
3636import com.google.api.core.ApiFutures;
3737import com.google.api.core.InternalApi;
38+ import com.google.api.core.SettableApiFuture;
3839import com.google.api.gax.resumable.ChunkUploadRequest;
3940import com.google.api.gax.resumable.ChunkUploadResponse;
4041import com.google.api.gax.resumable.ResumableUploadStatus;
4142import com.google.common.io.ByteStreams;
4243import com.google.common.util.concurrent.MoreExecutors;
43- import java.io.IOException;
4444import java.io.InputStream;
4545import java.util.Arrays;
4646import java.util.concurrent.CancellationException;
4747import org.jspecify.annotations.NullMarked;
48+ import org.jspecify.annotations.Nullable;
4849
4950/**
5051 * Coordinates chunk transmission steps of a resumable upload session.
@@ -64,84 +65,90 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
6465 private final byte[] buffer;
6566 private final int chunkSize;
6667 private final ApiCallContext callContext;
67- private final ResumableUploadFutureImpl<ResponseT> sessionFuture;
68+ private final SettableApiFuture<ResponseT> uploadResultFuture = SettableApiFuture.create();
69+ private volatile @Nullable ApiFuture<?> inFlightFuture;
6870
6971 ResumableUploadChunkCoordinator(
7072 UnaryCallable<ChunkUploadRequest, ChunkUploadResponse<ResponseT>> uploadChunkCallable,
7173 String uploadUrl,
7274 InputStream payload,
7375 int chunkSize,
74- ApiCallContext callContext,
75- ResumableUploadFutureImpl<ResponseT> sessionFuture) {
76+ ApiCallContext callContext) {
7677 this.uploadChunkCallable =
7778 checkNotNull(uploadChunkCallable, "uploadChunkCallable must not be null");
7879 this.uploadUrl = checkNotNull(uploadUrl, "uploadUrl must not be null");
7980 this.payload = checkNotNull(payload, "payload must not be null");
8081 this.chunkSize = chunkSize;
8182 this.callContext = checkNotNull(callContext, "callContext must not be null");
82- this.sessionFuture = checkNotNull(sessionFuture, "sessionFuture must not be null");
8383 this.buffer = new byte[chunkSize];
8484 }
8585
86+ ApiFuture<ResponseT> getFuture() {
87+ return uploadResultFuture;
88+ }
89+
8690 void start() {
91+ uploadResultFuture.addListener(
92+ () -> {
93+ ApiFuture<?> inFlight = inFlightFuture;
94+ if (uploadResultFuture.isCancelled() && inFlight != null) {
95+ inFlight.cancel(true);
96+ }
97+ },
98+ MoreExecutors.directExecutor());
8799 transmitChunk(0L);
88100 }
89101
90102 private void transmitChunk(long currentOffset) {
91- // Abort if the session was already completed or canceled.
92- if (sessionFuture.isDone()) {
93- return;
94- }
95-
96- // Read the next chunk slice from the payload stream.
97- int bytesRead;
98103 try {
99- bytesRead = ByteStreams.read(payload, buffer, 0, chunkSize);
100- } catch (IOException e) {
101- sessionFuture.fail(e);
102- return;
103- }
104+ // Abort if the session was already completed or canceled.
105+ if (uploadResultFuture.isDone()) {
106+ return;
107+ }
104108
105- // Determine if this is the final chunk and build the chunk request.
106- boolean isFinal = bytesRead < chunkSize;
107- byte[] chunkPayload;
108- if (bytesRead == chunkSize) {
109- chunkPayload = buffer;
110- } else if (bytesRead == 0) {
111- chunkPayload = EMPTY_PAYLOAD;
112- } else {
113- chunkPayload = Arrays.copyOf(buffer, bytesRead);
114- }
109+ // Read the next chunk slice from the payload stream.
110+ int bytesRead = ByteStreams.read(payload, buffer, 0, chunkSize);
115111
116- ChunkUploadRequest chunkRequest =
117- ChunkUploadRequest.newBuilder()
118- .setUploadUrl(uploadUrl)
119- .setPayload(chunkPayload)
120- .setOffset(currentOffset)
121- .setFinal(isFinal)
122- .build();
112+ // Determine if this is the final chunk and build the chunk request.
113+ boolean isFinal = bytesRead < chunkSize;
114+ byte[] chunkPayload;
115+ if (bytesRead == chunkSize) {
116+ chunkPayload = buffer;
117+ } else if (bytesRead == 0) {
118+ chunkPayload = EMPTY_PAYLOAD;
119+ } else {
120+ chunkPayload = Arrays.copyOf(buffer, bytesRead);
121+ }
123122
124- // Dispatch the chunk upload call and register the in-flight future for cancellation.
125- long chunkLength = chunkPayload.length;
126- try {
123+ ChunkUploadRequest chunkRequest =
124+ ChunkUploadRequest.newBuilder()
125+ .setUploadUrl(uploadUrl)
126+ .setPayload(chunkPayload)
127+ .setOffset(currentOffset)
128+ .setFinal(isFinal)
129+ .build();
130+
131+ // Dispatch the chunk upload call and register the in-flight future for cancellation.
132+ long chunkLength = chunkPayload.length;
127133 ApiFuture<ChunkUploadResponse<ResponseT>> chunkFuture =
128134 uploadChunkCallable.futureCall(chunkRequest, callContext);
129- sessionFuture.setInFlightFuture(chunkFuture);
135+ if (!tryRegisterInFlightFuture(chunkFuture)) {
136+ return;
137+ }
130138
131- // Asynchronously handle the response: complete, fail, or chain the next chunk.
132139 ApiFutures.addCallback(
133140 chunkFuture,
134141 new ApiFutureCallback<ChunkUploadResponse<ResponseT>>() {
135142 @Override
136143 public void onSuccess(ChunkUploadResponse<ResponseT> response) {
137- if (sessionFuture .isDone()) {
144+ if (uploadResultFuture .isDone()) {
138145 return;
139146 }
140147 long nextOffset = currentOffset + chunkLength;
141148 if (response.getUploadStatus() == ResumableUploadStatus.FINAL) {
142- sessionFuture.succeed (response.getResponse());
149+ uploadResultFuture.set (response.getResponse());
143150 } else if (isFinal) {
144- sessionFuture.fail (
151+ uploadResultFuture.setException (
145152 new IllegalStateException(
146153 "Upload stream ended and final chunk was transmitted, but server returned"
147154 + " incomplete status"));
@@ -152,15 +159,28 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
152159
153160 @Override
154161 public void onFailure(Throwable t) {
155- if (t instanceof CancellationException || sessionFuture .isDone()) {
162+ if (t instanceof CancellationException || uploadResultFuture .isDone()) {
156163 return;
157164 }
158- sessionFuture.fail (t);
165+ uploadResultFuture.setException (t);
159166 }
160167 },
161168 MoreExecutors.directExecutor());
162169 } catch (Throwable t) {
163- sessionFuture.fail(t);
170+ uploadResultFuture.setException(t);
171+ }
172+ }
173+
174+ /**
175+ * Registers the in-flight future for possible cancellation, returning false if the upload was
176+ * already cancelled.
177+ */
178+ private boolean tryRegisterInFlightFuture(ApiFuture<?> future) {
179+ this.inFlightFuture = future;
180+ if (uploadResultFuture.isCancelled()) {
181+ future.cancel(true);
182+ return false;
164183 }
184+ return true;
165185 }
166186}
0 commit comments