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 ;
4546import java .util .Arrays ;
4647import java .util .concurrent .CancellationException ;
4748import 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,43 @@ 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 > uploadResultFuture = SettableApiFuture .create ();
70+ private volatile @ Nullable ApiFuture <?> inFlightFuture ;
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
87+ ApiFuture <ResponseT > getFuture () {
88+ return uploadResultFuture ;
89+ }
90+
8691 void start () {
92+ uploadResultFuture .addListener (
93+ () -> {
94+ ApiFuture <?> inFlight = inFlightFuture ;
95+ if (uploadResultFuture .isCancelled () && inFlight != null ) {
96+ inFlight .cancel (true );
97+ }
98+ },
99+ MoreExecutors .directExecutor ());
87100 transmitChunk (0L );
88101 }
89102
90103 private void transmitChunk (long currentOffset ) {
91104 // Abort if the session was already completed or canceled.
92- if (sessionFuture .isDone ()) {
105+ if (uploadResultFuture .isDone ()) {
93106 return ;
94107 }
95108
@@ -98,7 +111,7 @@ private void transmitChunk(long currentOffset) {
98111 try {
99112 bytesRead = ByteStreams .read (payload , buffer , 0 , chunkSize );
100113 } catch (IOException e ) {
101- sessionFuture . fail (e );
114+ uploadResultFuture . setException (e );
102115 return ;
103116 }
104117
@@ -126,22 +139,23 @@ private void transmitChunk(long currentOffset) {
126139 try {
127140 ApiFuture <ChunkUploadResponse <ResponseT >> chunkFuture =
128141 uploadChunkCallable .futureCall (chunkRequest , callContext );
129- sessionFuture .setInFlightFuture (chunkFuture );
142+ if (!tryRegisterInFlightFuture (chunkFuture )) {
143+ return ;
144+ }
130145
131- // Asynchronously handle the response: complete, fail, or chain the next chunk.
132146 ApiFutures .addCallback (
133147 chunkFuture ,
134148 new ApiFutureCallback <ChunkUploadResponse <ResponseT >>() {
135149 @ Override
136150 public void onSuccess (ChunkUploadResponse <ResponseT > response ) {
137- if (sessionFuture .isDone ()) {
151+ if (uploadResultFuture .isDone ()) {
138152 return ;
139153 }
140154 long nextOffset = currentOffset + chunkLength ;
141155 if (response .getUploadStatus () == ResumableUploadStatus .FINAL ) {
142- sessionFuture . succeed (response .getResponse ());
156+ uploadResultFuture . set (response .getResponse ());
143157 } else if (isFinal ) {
144- sessionFuture . fail (
158+ uploadResultFuture . setException (
145159 new IllegalStateException (
146160 "Upload stream ended and final chunk was transmitted, but server returned"
147161 + " incomplete status" ));
@@ -152,15 +166,28 @@ public void onSuccess(ChunkUploadResponse<ResponseT> response) {
152166
153167 @ Override
154168 public void onFailure (Throwable t ) {
155- if (t instanceof CancellationException || sessionFuture .isDone ()) {
169+ if (t instanceof CancellationException || uploadResultFuture .isDone ()) {
156170 return ;
157171 }
158- sessionFuture . fail (t );
172+ uploadResultFuture . setException (t );
159173 }
160174 },
161175 MoreExecutors .directExecutor ());
162176 } catch (Throwable t ) {
163- sessionFuture .fail (t );
177+ uploadResultFuture .setException (t );
178+ }
179+ }
180+
181+ /**
182+ * Registers the in-flight future for possible cancellation, returning false if the upload was
183+ * already cancelled.
184+ */
185+ private boolean tryRegisterInFlightFuture (ApiFuture <?> future ) {
186+ this .inFlightFuture = future ;
187+ if (uploadResultFuture .isCancelled ()) {
188+ future .cancel (true );
189+ return false ;
164190 }
191+ return true ;
165192 }
166193}
0 commit comments