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