Skip to content

Commit 778fac8

Browse files
committed
refactor(gax): invert resumable upload future and coordinator ownership
1 parent 4ee20f2 commit 778fac8

3 files changed

Lines changed: 54 additions & 53 deletions

File tree

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

Lines changed: 24 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,21 @@ 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;
130140

131-
// Asynchronously handle the response: complete, fail, or chain the next chunk.
132141
ApiFutures.addCallback(
133142
chunkFuture,
134143
new ApiFutureCallback<ChunkUploadResponse<ResponseT>>() {
135144
@Override
136145
public void onSuccess(ChunkUploadResponse<ResponseT> response) {
137-
if (sessionFuture.isDone()) {
146+
if (result.isDone()) {
138147
return;
139148
}
140149
long nextOffset = currentOffset + chunkLength;
141150
if (response.getUploadStatus() == ResumableUploadStatus.FINAL) {
142-
sessionFuture.succeed(response.getResponse());
151+
result.set(response.getResponse());
143152
} else if (isFinal) {
144-
sessionFuture.fail(
153+
result.setException(
145154
new IllegalStateException(
146155
"Upload stream ended and final chunk was transmitted, but server returned"
147156
+ " incomplete status"));
@@ -152,15 +161,15 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
152161

153162
@Override
154163
public void onFailure(Throwable t) {
155-
if (t instanceof CancellationException || sessionFuture.isDone()) {
164+
if (t instanceof CancellationException || result.isDone()) {
156165
return;
157166
}
158-
sessionFuture.fail(t);
167+
result.setException(t);
159168
}
160169
},
161170
MoreExecutors.directExecutor());
162171
} catch (Throwable t) {
163-
sessionFuture.fail(t);
172+
result.setException(t);
164173
}
165174
}
166175
}

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

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,40 @@ 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+
synchronized (lock) {
143+
if (resultFuture.isDone()) {
144+
return;
145+
}
146+
inFlightFuture = uploadFuture;
140147
}
148+
ApiFutures.addCallback(
149+
uploadFuture,
150+
new ApiFutureCallback<ResponseT>() {
151+
@Override
152+
public void onSuccess(ResponseT response) {
153+
succeed(response);
154+
}
155+
156+
@Override
157+
public void onFailure(Throwable t) {
158+
if (t instanceof CancellationException) {
159+
return;
160+
}
161+
fail(t);
162+
}
163+
},
164+
MoreExecutors.directExecutor());
141165
}
142166

143167
@Override
@@ -151,33 +175,15 @@ public void onFailure(Throwable t) {
151175
MoreExecutors.directExecutor());
152176
}
153177

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) {
178+
private void succeed(@Nullable ResponseT result) {
173179
synchronized (lock) {
174180
inFlightFuture = null;
175181
}
176182
closePayload();
177183
resultFuture.set(result);
178184
}
179185

180-
void fail(Throwable t) {
186+
private void fail(Throwable t) {
181187
synchronized (lock) {
182188
inFlightFuture = null;
183189
}

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)