Skip to content

Commit 4459004

Browse files
committed
refactor(gax): invert resumable upload future and coordinator ownership
1 parent eb683df commit 4459004

3 files changed

Lines changed: 64 additions & 53 deletions

File tree

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadChunkCoordinator.java

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.google.api.core.ApiFutureCallback;
3636
import com.google.api.core.ApiFutures;
3737
import com.google.api.core.InternalApi;
38+
import com.google.api.core.SettableApiFuture;
3839
import com.google.api.gax.resumable.ChunkUploadRequest;
3940
import com.google.api.gax.resumable.ChunkUploadResponse;
4041
import com.google.api.gax.resumable.ResumableUploadStatus;
@@ -45,6 +46,7 @@
4546
import java.util.Arrays;
4647
import java.util.concurrent.CancellationException;
4748
import org.jspecify.annotations.NullMarked;
49+
import org.jspecify.annotations.Nullable;
4850

4951
/**
5052
* Coordinates chunk transmission steps of a resumable upload session.
@@ -64,32 +66,40 @@ final class ResumableUploadChunkCoordinator<ResponseT> {
6466
private final byte[] buffer;
6567
private final int chunkSize;
6668
private final ApiCallContext callContext;
67-
private final ResumableUploadFutureImpl<ResponseT> sessionFuture;
69+
private final SettableApiFuture<ResponseT> result = SettableApiFuture.create();
70+
private volatile @Nullable ApiFuture<?> currentChunkFuture;
6871

6972
ResumableUploadChunkCoordinator(
7073
UnaryCallable<ChunkUploadRequest, ChunkUploadResponse<ResponseT>> uploadChunkCallable,
7174
String uploadUrl,
7275
InputStream payload,
7376
int chunkSize,
74-
ApiCallContext callContext,
75-
ResumableUploadFutureImpl<ResponseT> sessionFuture) {
77+
ApiCallContext callContext) {
7678
this.uploadChunkCallable =
7779
checkNotNull(uploadChunkCallable, "uploadChunkCallable must not be null");
7880
this.uploadUrl = checkNotNull(uploadUrl, "uploadUrl must not be null");
7981
this.payload = checkNotNull(payload, "payload must not be null");
8082
this.chunkSize = chunkSize;
8183
this.callContext = checkNotNull(callContext, "callContext must not be null");
82-
this.sessionFuture = checkNotNull(sessionFuture, "sessionFuture must not be null");
8384
this.buffer = new byte[chunkSize];
8485
}
8586

86-
void start() {
87+
ApiFuture<ResponseT> start() {
88+
result.addListener(
89+
() -> {
90+
ApiFuture<?> chunk = currentChunkFuture;
91+
if (result.isCancelled() && chunk != null) {
92+
chunk.cancel(true);
93+
}
94+
},
95+
MoreExecutors.directExecutor());
8796
transmitChunk(0L);
97+
return result;
8898
}
8999

90100
private void transmitChunk(long currentOffset) {
91101
// Abort if the session was already completed or canceled.
92-
if (sessionFuture.isDone()) {
102+
if (result.isDone()) {
93103
return;
94104
}
95105

@@ -98,7 +108,7 @@ private void transmitChunk(long currentOffset) {
98108
try {
99109
bytesRead = ByteStreams.read(payload, buffer, 0, chunkSize);
100110
} catch (IOException e) {
101-
sessionFuture.fail(e);
111+
result.setException(e);
102112
return;
103113
}
104114

@@ -126,22 +136,25 @@ private void transmitChunk(long currentOffset) {
126136
try {
127137
ApiFuture<ChunkUploadResponse<ResponseT>> chunkFuture =
128138
uploadChunkCallable.futureCall(chunkRequest, callContext);
129-
sessionFuture.setInFlightFuture(chunkFuture);
139+
this.currentChunkFuture = chunkFuture;
140+
if (result.isCancelled()) {
141+
chunkFuture.cancel(true);
142+
return;
143+
}
130144

131-
// Asynchronously handle the response: complete, fail, or chain the next chunk.
132145
ApiFutures.addCallback(
133146
chunkFuture,
134147
new ApiFutureCallback<ChunkUploadResponse<ResponseT>>() {
135148
@Override
136149
public void onSuccess(ChunkUploadResponse<ResponseT> response) {
137-
if (sessionFuture.isDone()) {
150+
if (result.isDone()) {
138151
return;
139152
}
140153
long nextOffset = currentOffset + chunkLength;
141154
if (response.getUploadStatus() == ResumableUploadStatus.FINAL) {
142-
sessionFuture.succeed(response.getResponse());
155+
result.set(response.getResponse());
143156
} else if (isFinal) {
144-
sessionFuture.fail(
157+
result.setException(
145158
new IllegalStateException(
146159
"Upload stream ended and final chunk was transmitted, but server returned"
147160
+ " incomplete status"));
@@ -152,15 +165,15 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
152165

153166
@Override
154167
public void onFailure(Throwable t) {
155-
if (t instanceof CancellationException || sessionFuture.isDone()) {
168+
if (t instanceof CancellationException || result.isDone()) {
156169
return;
157170
}
158-
sessionFuture.fail(t);
171+
result.setException(t);
159172
}
160173
},
161174
MoreExecutors.directExecutor());
162175
} catch (Throwable t) {
163-
sessionFuture.fail(t);
176+
result.setException(t);
164177
}
165178
}
166179
}

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFutureImpl.java

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,46 @@ public void onSuccess(ResumableUploadSession session) {
128128
ResumableUploadChunkCoordinator<ResponseT> coordinator =
129129
new ResumableUploadChunkCoordinator<>(
130130
uploadChunkCallable,
131-
session.getUploadUrl(),
131+
uploadSessionUrl,
132132
payload,
133133
settings.getChunkSize(),
134-
callContext,
135-
ResumableUploadFutureImpl.this);
134+
callContext);
135+
ApiFuture<ResponseT> uploadFuture;
136136
try {
137-
coordinator.start();
137+
uploadFuture = coordinator.start();
138138
} catch (Throwable t) {
139139
fail(t);
140+
return;
141+
}
142+
boolean alreadyDone = false;
143+
synchronized (lock) {
144+
if (resultFuture.isDone()) {
145+
alreadyDone = true;
146+
} else {
147+
inFlightFuture = uploadFuture;
148+
}
149+
}
150+
if (alreadyDone) {
151+
uploadFuture.cancel(true);
152+
return;
140153
}
154+
ApiFutures.addCallback(
155+
uploadFuture,
156+
new ApiFutureCallback<ResponseT>() {
157+
@Override
158+
public void onSuccess(ResponseT response) {
159+
succeed(response);
160+
}
161+
162+
@Override
163+
public void onFailure(Throwable t) {
164+
if (t instanceof CancellationException) {
165+
return;
166+
}
167+
fail(t);
168+
}
169+
},
170+
MoreExecutors.directExecutor());
141171
}
142172

143173
@Override
@@ -151,33 +181,15 @@ public void onFailure(Throwable t) {
151181
MoreExecutors.directExecutor());
152182
}
153183

154-
/**
155-
* Registers the active in-flight future for cancellation. If this session future has already been
156-
* canceled, the supplied future is canceled immediately.
157-
*/
158-
void setInFlightFuture(ApiFuture<?> inFlightFuture) {
159-
boolean shouldCancel = false;
160-
synchronized (lock) {
161-
if (resultFuture.isDone()) {
162-
shouldCancel = resultFuture.isCancelled();
163-
} else {
164-
this.inFlightFuture = inFlightFuture;
165-
}
166-
}
167-
if (shouldCancel) {
168-
inFlightFuture.cancel(true);
169-
}
170-
}
171-
172-
void succeed(@Nullable ResponseT result) {
184+
private void succeed(@Nullable ResponseT result) {
173185
synchronized (lock) {
174186
inFlightFuture = null;
175187
}
176188
closePayload();
177189
resultFuture.set(result);
178190
}
179191

180-
void fail(Throwable t) {
192+
private void fail(Throwable t) {
181193
synchronized (lock) {
182194
inFlightFuture = null;
183195
}

sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ResumableUploadCallableImplTest.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -229,20 +229,6 @@ void testUploadCallable_cancelInFlight_haltsUpload() throws Exception {
229229
assertThrows(CancellationException.class, future::get);
230230
}
231231

232-
@Test
233-
void testUploadCallable_setInFlightFutureAfterCancel_immediatelyCancelsFuture() {
234-
SettableApiFuture<ResumableUploadSession> startFuture = SettableApiFuture.create();
235-
when(mockStartCallable.futureCall(any(), any())).thenReturn(startFuture);
236-
ResumableUploadFuture<String> future =
237-
callable.futureCall("resource-path", streamOf("data"), null);
238-
assertThat(future.cancel(true)).isTrue();
239-
assertThat(future.isCancelled()).isTrue();
240-
241-
SettableApiFuture<String> lateFuture = SettableApiFuture.create();
242-
((ResumableUploadFutureImpl<String>) future).setInFlightFuture(lateFuture);
243-
assertThat(lateFuture.isCancelled()).isTrue();
244-
}
245-
246232
@Test
247233
void testUploadCallable_startFailure_failsFuture() {
248234
when(mockStartCallable.futureCall(any(), any()))

0 commit comments

Comments
 (0)