com.google.cloud.storage.ParallelCompositeUploadWritableByteChannel Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of google-cloud-storage Show documentation
Show all versions of google-cloud-storage Show documentation
Java idiomatic client for Google Cloud Storage.
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.cloud.storage;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.api.core.SettableApiFuture;
import com.google.api.gax.grpc.GrpcStatusCode;
import com.google.api.gax.rpc.ApiExceptionFactory;
import com.google.api.gax.rpc.NotFoundException;
import com.google.cloud.BaseServiceException;
import com.google.cloud.storage.ApiFutureUtils.OnFailureApiFutureCallback;
import com.google.cloud.storage.ApiFutureUtils.OnSuccessApiFutureCallback;
import com.google.cloud.storage.AsyncAppendingQueue.ShortCircuitException;
import com.google.cloud.storage.BufferHandlePool.PooledBuffer;
import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel;
import com.google.cloud.storage.MetadataField.PartRange;
import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartCleanupStrategy;
import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy;
import com.google.cloud.storage.Storage.ComposeRequest;
import com.google.cloud.storage.UnifiedOpts.Crc32cMatch;
import com.google.cloud.storage.UnifiedOpts.GenerationMatch;
import com.google.cloud.storage.UnifiedOpts.GenerationNotMatch;
import com.google.cloud.storage.UnifiedOpts.Md5Match;
import com.google.cloud.storage.UnifiedOpts.MetagenerationMatch;
import com.google.cloud.storage.UnifiedOpts.MetagenerationNotMatch;
import com.google.cloud.storage.UnifiedOpts.ObjectSourceOpt;
import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt;
import com.google.cloud.storage.UnifiedOpts.Opts;
import com.google.cloud.storage.UnifiedOpts.SourceGenerationMatch;
import com.google.cloud.storage.UnifiedOpts.SourceGenerationNotMatch;
import com.google.cloud.storage.UnifiedOpts.SourceMetagenerationMatch;
import com.google.cloud.storage.UnifiedOpts.SourceMetagenerationNotMatch;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import io.grpc.Status.Code;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@SuppressWarnings("UnstableApiUsage") // guava hashing
final class ParallelCompositeUploadWritableByteChannel implements BufferedWritableByteChannel {
private static final MetadataField FINAL_OBJECT_NAME =
MetadataField.forString("pcu_finalObjectName");
private static final MetadataField PART_INDEX =
MetadataField.forPartRange("pcu_partIndex");
private static final MetadataField OBJECT_OFFSET =
MetadataField.forLong("pcu_objectOffset");
private static final Comparator comparator =
Comparator.comparing(PART_INDEX::readFrom, PartRange.COMP);
private static final Predicate TO_EXCLUDE_FROM_PARTS;
// when creating a part or composing we include a precondition so that it can be retried
private static final Opts DOES_NOT_EXIST =
Opts.from(UnifiedOpts.generationMatch(0));
static {
//noinspection deprecation
Predicate tmp =
o ->
o instanceof GenerationMatch
|| o instanceof GenerationNotMatch
|| o instanceof MetagenerationMatch
|| o instanceof MetagenerationNotMatch
|| o instanceof SourceGenerationMatch
|| o instanceof SourceGenerationNotMatch
|| o instanceof SourceMetagenerationMatch
|| o instanceof SourceMetagenerationNotMatch
|| o instanceof Crc32cMatch
|| o instanceof Md5Match;
TO_EXCLUDE_FROM_PARTS = tmp.negate();
}
// immutable provided values
private final BufferHandlePool bufferPool;
private final Executor exec;
private final PartNamingStrategy partNamingStrategy;
private final PartCleanupStrategy partCleanupStrategy;
private final int maxElementsPerCompact;
private final SettableApiFuture finalObject;
private final StorageInternal storage;
private final BlobInfo ultimateObject;
private final Opts opts;
// immutable bootstrapped state
private final Opts partOpts;
private final Opts srcOpts;
private final AsyncAppendingQueue queue;
private final FailureForwarder failureForwarder;
// mutable running state
private final List> pendingParts;
private final List successfulParts;
private final Hasher cumulativeHasher;
private boolean open;
private long totalObjectOffset;
private PooledBuffer current;
ParallelCompositeUploadWritableByteChannel(
BufferHandlePool bufferPool,
Executor exec,
PartNamingStrategy partNamingStrategy,
PartCleanupStrategy partCleanupStrategy,
int maxElementsPerCompact,
SettableApiFuture finalObject,
StorageInternal storage,
BlobInfo ultimateObject,
Opts opts) {
this.bufferPool = bufferPool;
this.exec = exec;
this.partNamingStrategy = partNamingStrategy;
this.partCleanupStrategy = partCleanupStrategy;
this.maxElementsPerCompact = maxElementsPerCompact;
this.finalObject = finalObject;
this.storage = storage;
this.ultimateObject = ultimateObject;
this.opts = opts;
this.queue = AsyncAppendingQueue.of(exec, maxElementsPerCompact, this::compose);
this.pendingParts = new ArrayList<>();
// this can be modified by another thread
this.successfulParts = Collections.synchronizedList(new ArrayList<>());
this.open = true;
this.totalObjectOffset = 0;
this.partOpts = getPartOpts(opts);
this.srcOpts = partOpts.transformTo(ObjectSourceOpt.class);
this.cumulativeHasher = Hashing.crc32c().newHasher();
this.failureForwarder = new FailureForwarder();
}
@Override
public synchronized int write(ByteBuffer src) throws IOException {
if (!open) {
throw new ClosedChannelException();
}
int remaining = src.remaining();
cumulativeHasher.putBytes(src.duplicate());
while (src.hasRemaining()) {
if (current == null) {
current = bufferPool.getBuffer();
}
ByteBuffer buf = current.getBufferHandle().get();
Buffers.copy(src, buf);
if (!buf.hasRemaining()) {
internalFlush(buf);
}
}
return remaining;
}
@Override
public synchronized boolean isOpen() {
return open;
}
@Override
public synchronized void flush() throws IOException {
if (current != null) {
ByteBuffer buf = current.getBufferHandle().get();
internalFlush(buf);
}
}
@Override
public synchronized void close() throws IOException {
if (!open) {
return;
}
open = false;
flush();
try {
queue.close();
} catch (NoSuchElementException e) {
// We never created any parts
// create an empty object
try {
BlobInfo blobInfo = storage.internalDirectUpload(ultimateObject, opts, Buffers.allocate(0));
finalObject.set(blobInfo);
return;
} catch (StorageException se) {
finalObject.setException(se);
throw se;
}
}
String expectedCrc32c = Utils.crc32cCodec.encode(cumulativeHasher.hash().asInt());
ApiFuture closingTransform =
ApiFutures.transformAsync(queue.getResult(), this::cleanupParts, exec);
ApiFuture validatingTransform =
ApiFutures.transformAsync(
closingTransform,
finalInfo -> {
String crc32c = finalInfo.getCrc32c();
if (expectedCrc32c.equals(crc32c)) {
return ApiFutures.immediateFuture(finalInfo);
} else {
return ApiFutures.immediateFailedFuture(
StorageException.coalesce(
buildParallelCompositeUploadException(
ApiExceptionFactory.createException(
String.format(
"CRC32C Checksum mismatch. expected: [%s] but was: [%s]",
expectedCrc32c, crc32c),
null,
GrpcStatusCode.of(Code.DATA_LOSS),
false),
exec,
pendingParts,
successfulParts)));
}
},
exec);
if (partCleanupStrategy.isDeleteAllOnError()) {
ApiFuture cleaningFuture =
ApiFutures.catchingAsync(
validatingTransform, Throwable.class, this::asyncCleanupAfterFailure, exec);
ApiFutures.addCallback(cleaningFuture, failureForwarder, exec);
} else {
ApiFutures.addCallback(validatingTransform, failureForwarder, exec);
}
// we don't need the value from this, but we do need any exception that might be present
try {
ApiFutureUtils.await(validatingTransform);
} catch (Throwable t) {
AsynchronousCloseException e = new AsynchronousCloseException();
e.initCause(t);
throw e;
}
}
private void internalFlush(ByteBuffer buf) {
Buffers.flip(buf);
int pendingByteCount = buf.remaining();
int partIndex = pendingParts.size() + 1;
BlobInfo partInfo = definePart(ultimateObject, PartRange.of(partIndex), totalObjectOffset);
ApiFuture partFuture =
ApiFutures.transform(
ApiFutures.immediateFuture(partInfo),
info -> {
try {
return storage.internalDirectUpload(info, partOpts, buf);
} catch (StorageException e) {
// a precondition failure usually means the part was created, but we didn't get the
// response. And when we tried to retry the object already exists.
if (e.getCode() == 412) {
return storage.internalObjectGet(info.getBlobId(), srcOpts);
} else {
throw e;
}
}
},
exec);
ApiFutures.addCallback(
partFuture,
new BufferHandleReleaser<>(
bufferPool,
current,
(OnSuccessApiFutureCallback)
result -> successfulParts.add(result.getBlobId())),
exec);
pendingParts.add(partFuture);
try {
queue.append(partFuture);
totalObjectOffset += pendingByteCount;
} catch (ShortCircuitException e) {
open = false;
bufferPool.returnBuffer(current);
// attempt to cancel any pending requests which haven't started yet
for (ApiFuture pendingPart : pendingParts) {
pendingPart.cancel(false);
}
Throwable cause = e.getCause();
BaseServiceException storageException;
if (partCleanupStrategy.isDeleteAllOnError()) {
storageException = StorageException.coalesce(cause);
ApiFuture