22
22
import com .google .api .core .SettableApiFuture ;
23
23
import com .google .api .gax .grpc .GrpcStatusCode ;
24
24
import com .google .api .gax .rpc .ApiExceptionFactory ;
25
+ import com .google .api .gax .rpc .NotFoundException ;
25
26
import com .google .cloud .BaseServiceException ;
26
27
import com .google .cloud .storage .ApiFutureUtils .OnFailureApiFutureCallback ;
27
28
import com .google .cloud .storage .ApiFutureUtils .OnSuccessApiFutureCallback ;
@@ -117,6 +118,7 @@ final class ParallelCompositeUploadWritableByteChannel implements BufferedWritab
117
118
118
119
// immutable bootstrapped state
119
120
private final Opts <ObjectTargetOpt > partOpts ;
121
+ private final Opts <ObjectSourceOpt > srcOpts ;
120
122
private final AsyncAppendingQueue <BlobInfo > queue ;
121
123
private final FailureForwarder failureForwarder ;
122
124
// mutable running state
@@ -154,6 +156,7 @@ final class ParallelCompositeUploadWritableByteChannel implements BufferedWritab
154
156
this .totalObjectOffset = 0 ;
155
157
156
158
this .partOpts = getPartOpts (opts );
159
+ this .srcOpts = partOpts .transformTo (ObjectSourceOpt .class );
157
160
this .cumulativeHasher = Hashing .crc32c ().newHasher ();
158
161
this .failureForwarder = new FailureForwarder ();
159
162
}
@@ -250,14 +253,7 @@ public synchronized void close() throws IOException {
250
253
if (partCleanupStrategy .isDeleteOnError ()) {
251
254
ApiFuture <BlobInfo > cleaningFuture =
252
255
ApiFutures .catchingAsync (
253
- validatingTransform ,
254
- Throwable .class ,
255
- t -> {
256
- // todo:
257
- return ApiFutures .immediateFailedFuture (t );
258
- },
259
- exec );
260
- // todo: verify this gets the first failure and not one from cleaning
256
+ validatingTransform , Throwable .class , this ::asyncCleanupAfterFailure , exec );
261
257
ApiFutures .addCallback (cleaningFuture , failureForwarder , exec );
262
258
} else {
263
259
ApiFutures .addCallback (validatingTransform , failureForwarder , exec );
@@ -288,7 +284,6 @@ private void internalFlush(ByteBuffer buf) {
288
284
// a precondition failure usually means the part was created, but we didn't get the
289
285
// response. And when we tried to retry the object already exists.
290
286
if (e .getCode () == 412 ) {
291
- Opts <ObjectSourceOpt > srcOpts = partOpts .transformTo (ObjectSourceOpt .class );
292
287
return storage .internalObjectGet (info .getBlobId (), srcOpts );
293
288
} else {
294
289
throw e ;
@@ -320,23 +315,46 @@ private void internalFlush(ByteBuffer buf) {
320
315
}
321
316
322
317
Throwable cause = e .getCause ();
323
- // create our exception containing information about the upload context
324
- ParallelCompositeUploadException pcue =
325
- buildParallelCompositeUploadException (cause , exec , pendingParts , successfulParts );
326
- BaseServiceException storageException = StorageException .coalesce (pcue );
327
-
328
- // asynchronously fail the finalObject future
329
- CancellationException cancellationException =
330
- new CancellationException (storageException .getMessage ());
331
- cancellationException .initCause (storageException );
332
- ApiFutures .addCallback (
333
- ApiFutures .immediateFailedFuture (cancellationException ),
334
- (OnFailureApiFutureCallback <BlobInfo >) failureForwarder ::onFailure ,
335
- exec );
318
+ BaseServiceException storageException ;
336
319
if (partCleanupStrategy .isDeleteOnError ()) {
337
- // TODO: cleanup
320
+ storageException = StorageException .coalesce (cause );
321
+ ApiFuture <Object > cleanupFutures = asyncCleanupAfterFailure (storageException );
322
+ // asynchronously fail the finalObject future
323
+ CancellationException cancellationException =
324
+ new CancellationException (storageException .getMessage ());
325
+ cancellationException .initCause (storageException );
326
+ ApiFutures .addCallback (
327
+ cleanupFutures ,
328
+ new ApiFutureCallback <Object >() {
329
+ @ Override
330
+ public void onFailure (Throwable throwable ) {
331
+ cancellationException .addSuppressed (throwable );
332
+ failureForwarder .onFailure (cancellationException );
333
+ }
334
+
335
+ @ Override
336
+ public void onSuccess (Object o ) {
337
+ failureForwarder .onFailure (cancellationException );
338
+ }
339
+ },
340
+ exec );
341
+ // this will throw out if anything fails
342
+ ApiFutureUtils .await (cleanupFutures );
343
+ } else {
344
+ // create our exception containing information about the upload context
345
+ ParallelCompositeUploadException pcue =
346
+ buildParallelCompositeUploadException (cause , exec , pendingParts , successfulParts );
347
+ storageException = StorageException .coalesce (pcue );
348
+ // asynchronously fail the finalObject future
349
+ CancellationException cancellationException =
350
+ new CancellationException (storageException .getMessage ());
351
+ cancellationException .initCause (storageException );
352
+ ApiFutures .addCallback (
353
+ ApiFutures .immediateFailedFuture (cancellationException ),
354
+ (OnFailureApiFutureCallback <BlobInfo >) failureForwarder ::onFailure ,
355
+ exec );
356
+ throw storageException ;
338
357
}
339
- throw storageException ;
340
358
} finally {
341
359
current = null ;
342
360
}
@@ -383,13 +401,16 @@ private ApiFuture<BlobInfo> cleanupParts(BlobInfo finalInfo) {
383
401
successfulParts .stream ()
384
402
// make sure we don't delete the object we're wanting to create
385
403
.filter (id -> !id .equals (finalInfo .getBlobId ()))
386
- .map (ApiFutures ::immediateFuture )
387
- .map (f -> ApiFutures .transform (f , storage ::delete , exec ))
404
+ .map (this ::deleteAsync )
388
405
.collect (Collectors .toList ());
389
406
390
407
ApiFuture <List <Boolean >> deletes2 = ApiFutureUtils .quietAllAsList (deletes );
391
408
392
- return ApiFutures .transform (deletes2 , ignore -> finalInfo , exec );
409
+ return ApiFutures .catchingAsync (
410
+ ApiFutures .transform (deletes2 , ignore -> finalInfo , exec ),
411
+ Throwable .class ,
412
+ cause -> ApiFutures .immediateFailedFuture (StorageException .coalesce (cause )),
413
+ exec );
393
414
}
394
415
395
416
private BlobInfo definePart (BlobInfo ultimateObject , PartRange partRange , long offset ) {
@@ -409,6 +430,75 @@ private BlobInfo definePart(BlobInfo ultimateObject, PartRange partRange, long o
409
430
return b .build ();
410
431
}
411
432
433
+ private <R > ApiFuture <R > asyncCleanupAfterFailure (Throwable originalFailure ) {
434
+ ApiFuture <ImmutableList <BlobId >> pendingAndSuccessfulBlobIds =
435
+ getPendingAndSuccessfulBlobIds (exec , pendingParts , successfulParts );
436
+ return ApiFutures .transformAsync (
437
+ pendingAndSuccessfulBlobIds ,
438
+ blobIds -> {
439
+ ImmutableList <ApiFuture <Boolean >> pendingDeletes =
440
+ blobIds .stream ().map (this ::deleteAsync ).collect (ImmutableList .toImmutableList ());
441
+
442
+ ApiFuture <List <Boolean >> futureDeleteResults =
443
+ ApiFutures .successfulAsList (pendingDeletes );
444
+
445
+ return ApiFutures .transformAsync (
446
+ futureDeleteResults ,
447
+ deleteResults -> {
448
+ List <BlobId > failedDeletes = new ArrayList <>();
449
+ for (int i = 0 ; i < blobIds .size (); i ++) {
450
+ BlobId id = blobIds .get (i );
451
+ Boolean deleteResult = deleteResults .get (i );
452
+ // deleteResult not equal to true means the request completed but was
453
+ // unsuccessful
454
+ // deleteResult being null means the future failed
455
+ if (!Boolean .TRUE .equals (deleteResult )) {
456
+ failedDeletes .add (id );
457
+ }
458
+ }
459
+
460
+ if (!failedDeletes .isEmpty ()) {
461
+ String failedGsUris =
462
+ failedDeletes .stream ()
463
+ .map (BlobId ::toGsUtilUriWithGeneration )
464
+ .collect (Collectors .joining (",\n " , "[\n " , "\n ]" ));
465
+
466
+ String message =
467
+ String .format (
468
+ "Incomplete parallel composite upload cleanup after previous error. Unknown object ids: %s" ,
469
+ failedGsUris );
470
+ StorageException storageException = new StorageException (0 , message , null );
471
+ originalFailure .addSuppressed (storageException );
472
+ }
473
+ return ApiFutures .immediateFailedFuture (originalFailure );
474
+ },
475
+ exec );
476
+ },
477
+ exec );
478
+ }
479
+
480
+ @ NonNull
481
+ private ApiFuture <Boolean > deleteAsync (BlobId id ) {
482
+ return ApiFutures .transform (
483
+ ApiFutures .immediateFuture (id ),
484
+ v -> {
485
+ try {
486
+ storage .internalObjectDelete (v , srcOpts );
487
+ return true ;
488
+ } catch (NotFoundException e ) {
489
+ // not found means the part doesn't exist, which is what we want
490
+ return true ;
491
+ } catch (StorageException e ) {
492
+ if (e .getCode () == 404 ) {
493
+ return true ;
494
+ } else {
495
+ throw e ;
496
+ }
497
+ }
498
+ },
499
+ exec );
500
+ }
501
+
412
502
@ VisibleForTesting
413
503
@ NonNull
414
504
static Opts <ObjectTargetOpt > getPartOpts (Opts <ObjectTargetOpt > opts ) {
@@ -468,6 +558,15 @@ static ParallelCompositeUploadException buildParallelCompositeUploadException(
468
558
Executor exec ,
469
559
List <ApiFuture <BlobInfo >> pendingParts ,
470
560
List <BlobId > successfulParts ) {
561
+ ApiFuture <ImmutableList <BlobId >> fCreatedObjects =
562
+ getPendingAndSuccessfulBlobIds (exec , pendingParts , successfulParts );
563
+
564
+ return ParallelCompositeUploadException .of (cause , fCreatedObjects );
565
+ }
566
+
567
+ @ NonNull
568
+ private static ApiFuture <ImmutableList <BlobId >> getPendingAndSuccessfulBlobIds (
569
+ Executor exec , List <ApiFuture <BlobInfo >> pendingParts , List <BlobId > successfulParts ) {
471
570
ApiFuture <List <BlobInfo >> successfulList = ApiFutures .successfulAsList (pendingParts );
472
571
// suppress any failure that might happen when waiting for any pending futures to resolve
473
572
ApiFuture <List <BlobInfo >> catching =
@@ -490,7 +589,6 @@ static ParallelCompositeUploadException buildParallelCompositeUploadException(
490
589
.distinct ()
491
590
.collect (ImmutableList .toImmutableList ()),
492
591
exec );
493
-
494
- return ParallelCompositeUploadException .of (cause , fCreatedObjects );
592
+ return fCreatedObjects ;
495
593
}
496
594
}
0 commit comments