From 2bffbfbf0d82437bc33ee3926366752be7283545 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 25 Jul 2023 00:02:51 +0200 Subject: [PATCH 01/17] chore(deps): update dependency com.google.cloud:google-cloud-storage to v2.25.0 (#2136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update dependency com.google.cloud:google-cloud-storage to v2.25.0 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- README.md | 8 ++++---- samples/install-without-bom/pom.xml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ec3dc8c51a..1aa0a611ae 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies: com.google.cloud google-cloud-storage - 2.24.0 + 2.25.0 ``` @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-storage' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-storage:2.24.0' +implementation 'com.google.cloud:google-cloud-storage:2.25.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.24.0" +libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.25.0" ``` @@ -428,7 +428,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-storage/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-storage.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.24.0 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.25.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 7b16d416c3..f3d8b89067 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -30,7 +30,7 @@ com.google.cloud google-cloud-storage - 2.24.0 + 2.25.0 From 8cce2e07694a9dd9f81017ec856fb0bcf73ce748 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:11:25 -0400 Subject: [PATCH 02/17] chore(main): release 2.25.1-SNAPSHOT (#2135) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- gapic-google-cloud-storage-v2/pom.xml | 4 ++-- google-cloud-storage-bom/pom.xml | 10 +++++----- google-cloud-storage/pom.xml | 4 ++-- grpc-google-cloud-storage-v2/pom.xml | 4 ++-- pom.xml | 10 +++++----- proto-google-cloud-storage-v2/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml index 83590fc99c..b5d90a4d78 100644 --- a/gapic-google-cloud-storage-v2/pom.xml +++ b/gapic-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc gapic-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT gapic-google-cloud-storage-v2 GRPC library for gapic-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.25.0 + 2.25.1-SNAPSHOT diff --git a/google-cloud-storage-bom/pom.xml b/google-cloud-storage-bom/pom.xml index 9df55b375b..522b0c8082 100644 --- a/google-cloud-storage-bom/pom.xml +++ b/google-cloud-storage-bom/pom.xml @@ -19,7 +19,7 @@ 4.0.0 com.google.cloud google-cloud-storage-bom - 2.25.0 + 2.25.1-SNAPSHOT pom com.google.cloud @@ -69,22 +69,22 @@ com.google.cloud google-cloud-storage - 2.25.0 + 2.25.1-SNAPSHOT com.google.api.grpc gapic-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT com.google.api.grpc grpc-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT com.google.api.grpc proto-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 2a1fc31718..aeb9589e20 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-storage - 2.25.0 + 2.25.1-SNAPSHOT jar Google Cloud Storage https://github.com/googleapis/java-storage @@ -12,7 +12,7 @@ com.google.cloud google-cloud-storage-parent - 2.25.0 + 2.25.1-SNAPSHOT google-cloud-storage diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml index 825c606a69..a55f3c2326 100644 --- a/grpc-google-cloud-storage-v2/pom.xml +++ b/grpc-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT grpc-google-cloud-storage-v2 GRPC library for grpc-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.25.0 + 2.25.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index 837179197b..787a218a5a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-storage-parent pom - 2.25.0 + 2.25.1-SNAPSHOT Storage Parent https://github.com/googleapis/java-storage @@ -83,7 +83,7 @@ com.google.cloud google-cloud-storage - 2.25.0 + 2.25.1-SNAPSHOT com.google.apis @@ -124,17 +124,17 @@ com.google.api.grpc proto-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT com.google.api.grpc grpc-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT com.google.api.grpc gapic-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT com.google.cloud diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml index 19bbbf8f7c..0a67a9eba6 100644 --- a/proto-google-cloud-storage-v2/pom.xml +++ b/proto-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-storage-v2 - 2.25.0-alpha + 2.25.1-alpha-SNAPSHOT proto-google-cloud-storage-v2 PROTO library for proto-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.25.0 + 2.25.1-SNAPSHOT diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 26760883f1..89306564e1 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-storage - 2.25.0 + 2.25.1-SNAPSHOT diff --git a/versions.txt b/versions.txt index 03bad26208..3aafb4560a 100644 --- a/versions.txt +++ b/versions.txt @@ -1,7 +1,7 @@ # Format: # module:released-version:current-version -google-cloud-storage:2.25.0:2.25.0 -gapic-google-cloud-storage-v2:2.25.0-alpha:2.25.0-alpha -grpc-google-cloud-storage-v2:2.25.0-alpha:2.25.0-alpha -proto-google-cloud-storage-v2:2.25.0-alpha:2.25.0-alpha +google-cloud-storage:2.25.0:2.25.1-SNAPSHOT +gapic-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT +grpc-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT +proto-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT From e0191b518e50a49fae0691894b50f0c5f33fc6af Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Tue, 25 Jul 2023 15:01:12 -0400 Subject: [PATCH 03/17] feat: introduce new BlobWriteSession (#2123) When writing a new Blob to GCS, there are secondary session related state and actions which can't be represented by WriteChannel. BlobWriteSession provides a new construct to allow retrieving the resultant object which is created after the WritableByteChannel is closed. Along with the new session, configuration for this is now performed at the StorageOptions level where cross session considerations can influence the implementation of the returned session. The configurable option present for this new StorageWriterConfig is chunkSize. In the future new configurations will be added with their corresponding options. For example, in a future release it will be possible to change from in memory buffering to instead buffer to disk thereby reducing heap usage. --- .../clirr-ignored-differences.xml | 24 +-- .../cloud/storage/BlobWriteSession.java | 73 ++++++++ .../cloud/storage/BlobWriteSessionConfig.java | 59 +++++++ .../storage/BlobWriteSessionConfigs.java | 49 ++++++ .../cloud/storage/BlobWriteSessions.java | 48 ++++++ .../cloud/storage/CrossTransportUtils.java | 67 +++++++ .../DefaultBlobWriteSessionConfig.java | 163 ++++++++++++++++++ .../google/cloud/storage/GapicCopyWriter.java | 2 +- .../cloud/storage/GrpcBlobReadChannel.java | 2 +- .../cloud/storage/GrpcBlobWriteChannel.java | 2 +- .../google/cloud/storage/GrpcStorageImpl.java | 62 ++++--- .../cloud/storage/GrpcStorageOptions.java | 32 +++- .../com/google/cloud/storage/Storage.java | 50 ++++++ .../google/cloud/storage/StorageInternal.java | 29 ++++ .../com/google/cloud/storage/UnifiedOpts.java | 2 +- .../storage/TransportCompatibilityTest.java | 2 +- .../storage/it/ITBlobWriteSessionTest.java | 121 +++++++++++++ .../runner/registry/AbstractStorageProxy.java | 6 + 18 files changed, 740 insertions(+), 53 deletions(-) create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/CrossTransportUtils.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml index ad681c4be3..84d9907047 100644 --- a/google-cloud-storage/clirr-ignored-differences.xml +++ b/google-cloud-storage/clirr-ignored-differences.xml @@ -1,29 +1,11 @@ - + 7012 - com/google/cloud/storage/UnbufferedWritableByteChannelSession$UnbufferedWritableByteChannel - * write(*) - - - - 7012 - com/google/cloud/storage/spi/v1/StorageRpc - * getStorage() - - - - 8001 - com/google/cloud/storage/Hasher$ConstantConcatValueHasher - - - - - 7002 - com/google/cloud/storage/HttpDownloadSessionBuilder$ReadableByteChannelSessionBuilder - com.google.cloud.storage.HttpDownloadSessionBuilder$ReadableByteChannelSessionBuilder setCallback(java.util.function.Consumer) + com/google/cloud/storage/Storage + com.google.cloud.storage.BlobWriteSession blobWriteSession(com.google.cloud.storage.BlobInfo, com.google.cloud.storage.Storage$BlobWriteOption[]) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java new file mode 100644 index 0000000000..02ea23a6a7 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSession.java @@ -0,0 +1,73 @@ +/* + * 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.BetaApi; +import java.io.IOException; +import java.nio.channels.WritableByteChannel; + +/** + * A session to write an object to Google Cloud Storage. + * + *

A session can only write a single version of an object. If writing multiple versions of an + * object a new session must be created each time. + * + *

Provides an api that allows writing to and retrieving the resulting {@link BlobInfo} after + * write finalization. + * + *

The underlying implementation is dictated based upon the specified {@link + * BlobWriteSessionConfig} provided at {@link StorageOptions} creation time. + * + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + * @see BlobWriteSessionConfig + * @see BlobWriteSessionConfigs + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ +@BetaApi +public interface BlobWriteSession { + + /** + * Open the {@link WritableByteChannel} for this session. + * + *

A session may only be {@code open}ed once. If multiple calls to open are made, an illegal + * state exception will be thrown + * + *

Upon calling {@link WritableByteChannel#close()} the object creation will be finalized, and + * {@link #getResult()}s future should resolve. + * + * @throws IOException When creating the {@link WritableByteChannel} if an unrecoverable + * underlying IOException occurs it can be rethrown + * @throws IllegalStateException if open is called more than once + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + WritableByteChannel open() throws IOException; + + /** + * Return an {@link ApiFuture}{@code } which will represent the state of the object upon + * finalization and success response from Google Cloud Storage. + * + *

This future will not resolve until: 1. The object is successfully finalized and created in + * Google Cloud Storage 2. A terminal failure occurs, the terminal failure will become the + * exception result + * + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + ApiFuture getResult(); +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java new file mode 100644 index 0000000000..de8622c754 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java @@ -0,0 +1,59 @@ +/* + * 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.InternalApi; +import com.google.cloud.storage.Conversions.Decoder; +import com.google.cloud.storage.Storage.BlobWriteOption; +import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt; +import com.google.cloud.storage.UnifiedOpts.Opts; +import com.google.storage.v2.WriteObjectResponse; +import java.io.IOException; +import java.time.Clock; + +/** + * A sealed internal implementation only class which provides the means of configuring a {@link + * BlobWriteSession}. + * + *

A {@code BlobWriteSessionConfig} will be used to configure all {@link BlobWriteSession}s + * produced by an instance of {@link Storage}. + * + * @see BlobWriteSessionConfigs + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...) + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ +// When we have java modules, actually seal this to internal extension only +@InternalApi +public abstract class BlobWriteSessionConfig { + + @InternalApi + BlobWriteSessionConfig() {} + + @InternalApi + abstract WriterFactory createFactory(Clock clock) throws IOException; + + @InternalApi + interface WriterFactory { + @InternalApi + WritableByteChannelSession writeSession( + StorageInternal s, + BlobInfo info, + Opts opts, + Decoder d); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java new file mode 100644 index 0000000000..cc5e691e6b --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java @@ -0,0 +1,49 @@ +/* + * 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.BetaApi; +import com.google.cloud.storage.GrpcStorageOptions.GrpcStorageDefaults; +import com.google.cloud.storage.Storage.BlobWriteOption; + +/** + * Factory class to select and construct {@link BlobWriteSessionConfig}s. + * + * @see BlobWriteSessionConfig + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...) + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ +@BetaApi +public final class BlobWriteSessionConfigs { + + private BlobWriteSessionConfigs() {} + + /** + * Factory to produce the default configuration for uploading an object to Cloud Storage. + * + *

Configuration of the chunk size can be performed via {@link + * DefaultBlobWriteSessionConfig#withChunkSize(int)}. + * + * @see GrpcStorageDefaults#getDefaultStorageWriterConfig() + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public static DefaultBlobWriteSessionConfig getDefault() { + return new DefaultBlobWriteSessionConfig(ByteSizeConstants._16MiB); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java new file mode 100644 index 0000000000..878552a125 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessions.java @@ -0,0 +1,48 @@ +/* + * 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 java.io.IOException; +import java.nio.channels.WritableByteChannel; + +final class BlobWriteSessions { + + private BlobWriteSessions() {} + + static BlobWriteSession of(WritableByteChannelSession s) { + return new WritableByteChannelSessionAdapter(s); + } + + static final class WritableByteChannelSessionAdapter implements BlobWriteSession { + private final WritableByteChannelSession delegate; + + private WritableByteChannelSessionAdapter(WritableByteChannelSession delegate) { + this.delegate = delegate; + } + + @Override + public WritableByteChannel open() throws IOException { + return delegate.open(); + } + + @Override + public ApiFuture getResult() { + return delegate.getResult(); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/CrossTransportUtils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/CrossTransportUtils.java new file mode 100644 index 0000000000..1c5aa1d97d --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/CrossTransportUtils.java @@ -0,0 +1,67 @@ +/* + * 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.cloud.storage.TransportCompatibility.Transport; +import java.util.Arrays; +import java.util.stream.Collectors; + +final class CrossTransportUtils { + + static T throwHttpJsonOnly(String methodName) { + return throwHttpJsonOnly(Storage.class, methodName); + } + + static T throwHttpJsonOnly(Class clazz, String methodName) { + return throwTransportOnly(clazz, methodName, Transport.HTTP); + } + + static T throwGrpcOnly(String methodName) { + return throwGrpcOnly(Storage.class, methodName); + } + + static T throwGrpcOnly(Class clazz, String methodName) { + return throwTransportOnly(clazz, methodName, Transport.GRPC); + } + + static T throwTransportOnly(Class clazz, String methodName, Transport transport) { + String builder; + switch (transport) { + case HTTP: + builder = "StorageOptions.http()"; + break; + case GRPC: + builder = "StorageOptions.grpc()"; + break; + default: + throw new IllegalStateException( + String.format("Broken Java Enum: %s received value: '%s'", Transport.class, transport)); + } + String message = + String.format( + "%s#%s is only supported for %s transport. Please use %s to construct a compatible instance.", + clazz.getName(), methodName, transport, builder); + throw new UnsupportedOperationException(message); + } + + static String fmtMethodName(String name, Class... args) { + return name + + "(" + + Arrays.stream(args).map(Class::getName).collect(Collectors.joining(", ")) + + ")"; + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java new file mode 100644 index 0000000000..dfea0d4190 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java @@ -0,0 +1,163 @@ +/* + * 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.ApiFutures; +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel; +import com.google.cloud.storage.Conversions.Decoder; +import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt; +import com.google.cloud.storage.UnifiedOpts.Opts; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.storage.v2.WriteObjectResponse; +import java.nio.channels.WritableByteChannel; +import java.time.Clock; +import javax.annotation.concurrent.Immutable; + +/** + * Default Configuration to represent uploading to Google Cloud Storage in a chunked manner. + * + *

Perform a resumable upload, uploading at most {@code chunkSize} bytes each PUT. + * + *

Configuration of chunk size can be performed via {@link + * DefaultBlobWriteSessionConfig#withChunkSize(int)}. + * + *

An instance of this class will provide a {@link BlobWriteSession} is logically equivalent to + * the following: + * + *

{@code
+ * Storage storage = ...;
+ * WriteChannel writeChannel = storage.writer(BlobInfo, BlobWriteOption);
+ * writeChannel.setChunkSize(chunkSize);
+ * }
+ * + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ +@Immutable +@BetaApi +public final class DefaultBlobWriteSessionConfig extends BlobWriteSessionConfig { + + private final int chunkSize; + + @InternalApi + DefaultBlobWriteSessionConfig(int chunkSize) { + this.chunkSize = chunkSize; + } + + /** + * The number of bytes each chunk can be. + * + *

Default: {@code 16777216 (16 MiB)} + * + * @see #withChunkSize(int) + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ + public int getChunkSize() { + return chunkSize; + } + + /** + * Create a new instance with the {@code chunkSize} set to the specified value. + * + *

Default: {@code 16777216 (16 MiB)} + * + * @param chunkSize The number of bytes each chunk should be. Must be >= {@code 262144 (256 KiB)} + * @return The new instance + * @see #getChunkSize() + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public DefaultBlobWriteSessionConfig withChunkSize(int chunkSize) { + Preconditions.checkArgument( + chunkSize >= ByteSizeConstants._256KiB, + "chunkSize must be >= %d", + ByteSizeConstants._256KiB); + return new DefaultBlobWriteSessionConfig(chunkSize); + } + + @Override + @InternalApi + WriterFactory createFactory(Clock clock) { + return new Factory(chunkSize); + } + + @InternalApi + private static final class Factory implements WriterFactory { + + private final int chunkSize; + + private Factory(int chunkSize) { + this.chunkSize = chunkSize; + } + + @InternalApi + @Override + public WritableByteChannelSession writeSession( + StorageInternal s, + BlobInfo info, + Opts opts, + Decoder d) { + // todo: invert this + // make GrpcBlobWriteChannel use this factory to produce its WriteSession + if (s instanceof GrpcStorageImpl) { + GrpcStorageImpl g = (GrpcStorageImpl) s; + GrpcBlobWriteChannel writer = g.internalWriter(info, opts); + writer.setChunkSize(chunkSize); + WritableByteChannelSession session = + writer.newLazyWriteChannel().getSession(); + return new DecoratedWritableByteChannelSession<>(session, d); + } + return CrossTransportUtils.throwGrpcOnly(DefaultBlobWriteSessionConfig.class, ""); + } + } + + private static final class DecoratedWritableByteChannelSession + implements WritableByteChannelSession { + + private final WritableByteChannelSession delegate; + private final Decoder decoder; + + private DecoratedWritableByteChannelSession( + WritableByteChannelSession delegate, Decoder decoder) { + this.delegate = delegate; + this.decoder = decoder; + } + + @Override + public WBC open() { + try { + return WritableByteChannelSession.super.open(); + } catch (Exception e) { + throw StorageException.coalesce(e); + } + } + + @Override + public ApiFuture openAsync() { + return delegate.openAsync(); + } + + @Override + public ApiFuture getResult() { + return ApiFutures.transform( + delegate.getResult(), decoder::decode, MoreExecutors.directExecutor()); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java index cae70d6767..038ff46672 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicCopyWriter.java @@ -87,6 +87,6 @@ public void copyChunk() { @Override public RestorableState capture() { - return GrpcStorageImpl.throwHttpJsonOnly(CopyWriter.class, "capture"); + return CrossTransportUtils.throwHttpJsonOnly(CopyWriter.class, "capture"); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java index b58b9663f7..4ae3f24466 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobReadChannel.java @@ -43,7 +43,7 @@ final class GrpcBlobReadChannel extends BaseStorageReadChannel { @Override public RestorableState capture() { - return GrpcStorageImpl.throwHttpJsonOnly(ReadChannel.class, "capture"); + return CrossTransportUtils.throwHttpJsonOnly(ReadChannel.class, "capture"); } @Override diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java index f3520180b3..a1b1d30306 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcBlobWriteChannel.java @@ -50,7 +50,7 @@ final class GrpcBlobWriteChannel extends BaseStorageWriteChannel capture() { - return GrpcStorageImpl.throwHttpJsonOnly(WriteChannel.class, "capture"); + return CrossTransportUtils.throwHttpJsonOnly(WriteChannel.class, "capture"); } @Override diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index d7d4059196..fdd67a7eb7 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -18,6 +18,8 @@ import static com.google.cloud.storage.ByteSizeConstants._16MiB; import static com.google.cloud.storage.ByteSizeConstants._256KiB; +import static com.google.cloud.storage.CrossTransportUtils.fmtMethodName; +import static com.google.cloud.storage.CrossTransportUtils.throwHttpJsonOnly; import static com.google.cloud.storage.GrpcToHttpStatusCodeTranslation.resultRetryAlgorithmToCodes; import static com.google.cloud.storage.StorageV2ProtoUtils.bucketAclEntityOrAltEq; import static com.google.cloud.storage.StorageV2ProtoUtils.objectAclEntityOrAltEq; @@ -44,6 +46,7 @@ import com.google.cloud.Policy; import com.google.cloud.WriteChannel; import com.google.cloud.storage.Acl.Entity; +import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory; import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel; import com.google.cloud.storage.Conversions.Decoder; import com.google.cloud.storage.HmacKey.HmacKeyMetadata; @@ -131,7 +134,6 @@ import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; @@ -145,13 +147,12 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator; -import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.checkerframework.checker.nullness.qual.Nullable; @BetaApi -final class GrpcStorageImpl extends BaseService implements Storage { +final class GrpcStorageImpl extends BaseService implements StorageInternal { private static final byte[] ZERO_BYTES = new byte[0]; private static final Set READ_OPS = ImmutableSet.of(StandardOpenOption.READ); @@ -163,22 +164,30 @@ final class GrpcStorageImpl extends BaseService implements Stora private static final BucketSourceOption[] EMPTY_BUCKET_SOURCE_OPTIONS = new BucketSourceOption[0]; final StorageClient storageClient; + final WriterFactory writerFactory; final GrpcConversions codecs; final GrpcRetryAlgorithmManager retryAlgorithmManager; final SyntaxDecoders syntaxDecoders; + private final Decoder writeObjectResponseBlobInfoDecoder; // workaround for https://github.com/googleapis/java-storage/issues/1736 private final Opts defaultOpts; @Deprecated private final ProjectId defaultProjectId; GrpcStorageImpl( - GrpcStorageOptions options, StorageClient storageClient, Opts defaultOpts) { + GrpcStorageOptions options, + StorageClient storageClient, + WriterFactory writerFactory, + Opts defaultOpts) { super(options); this.storageClient = storageClient; + this.writerFactory = writerFactory; this.defaultOpts = defaultOpts; this.codecs = Conversions.grpc(); this.retryAlgorithmManager = options.getRetryAlgorithmManager(); this.syntaxDecoders = new SyntaxDecoders(); + this.writeObjectResponseBlobInfoDecoder = + codecs.blobInfo().compose(WriteObjectResponse::getResource); this.defaultProjectId = UnifiedOpts.projectId(options.getProjectId()); } @@ -278,15 +287,21 @@ public Blob createFrom(BlobInfo blobInfo, Path path, BlobWriteOption... options) @Override public Blob createFrom(BlobInfo blobInfo, Path path, int bufferSize, BlobWriteOption... options) throws IOException { + Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); + return internalCreateFrom(path, blobInfo, opts); + } + + @Override + public Blob internalCreateFrom(Path path, BlobInfo info, Opts opts) + throws IOException { requireNonNull(path, "path must be non null"); if (Files.isDirectory(path)) { throw new StorageException(0, path + " is a directory"); } - Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); - WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts); + WriteObjectRequest req = getWriteObjectRequest(info, opts); ClientStreamingCallable write = storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext); @@ -714,9 +729,14 @@ public void downloadTo(BlobId blob, OutputStream outputStream, BlobSourceOption. @Override public GrpcBlobWriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options) { Opts opts = Opts.unwrap(options).resolveFrom(blobInfo).prepend(defaultOpts); + return internalWriter(blobInfo, opts); + } + + @Override + public GrpcBlobWriteChannel internalWriter(BlobInfo info, Opts opts) { GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); - WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts); + WriteObjectRequest req = getWriteObjectRequest(info, opts); Hasher hasher = Hasher.noop(); return new GrpcBlobWriteChannel( storageClient.writeObjectCallable(), @@ -1483,6 +1503,15 @@ public boolean deleteNotification(String bucket, String notificationId) { Decoder.identity())); } + @BetaApi + @Override + public BlobWriteSession blobWriteSession(BlobInfo info, BlobWriteOption... options) { + Opts opts = Opts.unwrap(options).resolveFrom(info); + WritableByteChannelSession writableByteChannelSession = + writerFactory.writeSession(this, info, opts, writeObjectResponseBlobInfoDecoder); + return BlobWriteSessions.of(writableByteChannelSession); + } + @Override public GrpcStorageOptions getOptions() { return (GrpcStorageOptions) super.getOptions(); @@ -1720,25 +1749,6 @@ public boolean tryAdvance(Consumer action) { return StreamSupport.stream(spliterator, false); } - static T throwHttpJsonOnly(String methodName) { - return throwHttpJsonOnly(Storage.class, methodName); - } - - static T throwHttpJsonOnly(Class clazz, String methodName) { - String message = - String.format( - "%s#%s is only supported for HTTP_JSON transport. Please use StorageOptions.http() to construct a compatible instance.", - clazz.getName(), methodName); - throw new UnsupportedOperationException(message); - } - - private static String fmtMethodName(String name, Class... args) { - return name - + "(" - + Arrays.stream(args).map(Class::getName).collect(Collectors.joining(", ")) - + ")"; - } - ReadObjectRequest getReadObjectRequest(BlobId blob, Opts opts) { Object object = codecs.blobId().encode(blob); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index d623745a20..8bb3115c52 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -42,6 +42,7 @@ import com.google.cloud.Tuple; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spi.ServiceRpcFactory; +import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.UnifiedOpts.Opts; import com.google.cloud.storage.UnifiedOpts.UserProject; @@ -58,6 +59,7 @@ import java.io.IOException; import java.io.Serializable; import java.net.URI; +import java.time.Clock; import java.util.List; import java.util.Locale; import java.util.Map; @@ -82,6 +84,7 @@ public final class GrpcStorageOptions extends StorageOptions private final Duration terminationAwaitDuration; private final boolean attemptDirectPath; private final GrpcInterceptorProvider grpcInterceptorProvider; + private final BlobWriteSessionConfig blobWriteSessionConfig; private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -94,6 +97,7 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) builder.terminationAwaitDuration, serviceDefaults.getTerminationAwaitDuration()); this.attemptDirectPath = builder.attemptDirectPath; this.grpcInterceptorProvider = builder.grpcInterceptorProvider; + this.blobWriteSessionConfig = builder.blobWriteSessionConfig; } @Override @@ -346,6 +350,8 @@ public static final class Builder extends StorageOptions.Builder { private boolean attemptDirectPath = GrpcStorageDefaults.INSTANCE.isAttemptDirectPath(); private GrpcInterceptorProvider grpcInterceptorProvider = GrpcStorageDefaults.INSTANCE.grpcInterceptorProvider(); + private BlobWriteSessionConfig blobWriteSessionConfig = + GrpcStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); Builder() {} @@ -506,6 +512,21 @@ public GrpcStorageOptions.Builder setGrpcInterceptorProvider( return this; } + /** + * @see BlobWriteSessionConfig + * @see BlobWriteSessionConfigs + * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...) + * @see GrpcStorageDefaults#getDefaultStorageWriterConfig() + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public GrpcStorageOptions.Builder setBlobWriteSessionConfig( + @NonNull BlobWriteSessionConfig blobWriteSessionConfig) { + requireNonNull(blobWriteSessionConfig, "blobWriteSessionConfig must be non null"); + this.blobWriteSessionConfig = blobWriteSessionConfig; + return this; + } + /** @since 2.14.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override @@ -569,6 +590,12 @@ public boolean isAttemptDirectPath() { public GrpcInterceptorProvider grpcInterceptorProvider() { return INTERCEPTOR_PROVIDER; } + + /** @since 2.26.0 This new api is in preview and is subject to breaking changes. */ + @BetaApi + public BlobWriteSessionConfig getDefaultStorageWriterConfig() { + return BlobWriteSessionConfigs.getDefault(); + } } /** @@ -618,7 +645,10 @@ public Storage create(StorageOptions options) { StorageSettings storageSettings = t.x(); Opts defaultOpts = t.y(); return new GrpcStorageImpl( - grpcStorageOptions, StorageClient.create(storageSettings), defaultOpts); + grpcStorageOptions, + StorageClient.create(storageSettings), + grpcStorageOptions.blobWriteSessionConfig.createFactory(Clock.systemUTC()), + defaultOpts); } catch (IOException e) { throw new IllegalStateException( "Unable to instantiate gRPC com.google.cloud.storage.Storage client.", e); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index daab166f56..0372957788 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -16,6 +16,8 @@ package com.google.cloud.storage; +import static com.google.cloud.storage.CrossTransportUtils.fmtMethodName; +import static com.google.cloud.storage.CrossTransportUtils.throwGrpcOnly; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static java.util.Objects.requireNonNull; @@ -4615,4 +4617,52 @@ List testIamPermissions( */ @Override default void close() throws Exception {} + + /** + * Create a new {@link BlobWriteSession} for the specified {@code blobInfo} and {@code options}. + * + *

The returned {@code BlobWriteSession} can be used to write an individual version, a new + * session must be created each time you want to create a new version. + * + *

By default, any MD5 value in the provided {@code blobInfo} is ignored unless the option + * {@link BlobWriteOption#md5Match()} is included in {@code options}. + * + *

By default, any CRC32c value in the provided {@code blobInfo} is ignored unless the option + * {@link BlobWriteOption#crc32cMatch()} is included in {@code options}. + * + *

Example of creating an object using {@code BlobWriteSession}:

+ * + *
{@code
+   * String bucketName = "my-unique-bucket";
+   * String blobName = "my-blob-name";
+   * BlobId blobId = BlobId.of(bucketName, blobName);
+   * BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
+   * ReadableByteChannel readableByteChannel = ...;
+   * BlobWriteSession blobWriteSession = storage.blobWriteSession(blobInfo, BlobWriteOption.doesNotExist());
+   *
+   * // open the channel for writing
+   * try (WritableByteChannel writableByteChannel = blobWriteSession.open()) {
+   *   // copy all bytes
+   *   ByteStreams.copy(readableByteChannel, writableByteChannel);
+   * } catch (IOException e) {
+   *   // handle IOException
+   * }
+   *
+   * // get the resulting object metadata
+   * ApiFuture resultFuture = blobWriteSession.getResult();
+   * BlobInfo gen1 = resultFuture.get();
+   * }
+ * + * @param blobInfo blob to create + * @param options blob write options + * @since 2.26.0 This new api is in preview and is subject to breaking changes. + * @see BlobWriteSessionConfig + * @see BlobWriteSessionConfigs + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + */ + @BetaApi + @TransportCompatibility({Transport.GRPC}) + default BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... options) { + return throwGrpcOnly(fmtMethodName("blobWriteSession", BlobInfo.class, BlobWriteOption.class)); + } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java new file mode 100644 index 0000000000..deb8a05043 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java @@ -0,0 +1,29 @@ +/* + * 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.cloud.storage.UnifiedOpts.ObjectTargetOpt; +import com.google.cloud.storage.UnifiedOpts.Opts; +import java.io.IOException; +import java.nio.file.Path; + +interface StorageInternal extends Storage { + + Blob internalCreateFrom(Path path, BlobInfo info, Opts opts) throws IOException; + + StorageWriteChannel internalWriter(BlobInfo info, Opts opts); +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java index f51bc05da4..f794c29bcb 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java @@ -1083,7 +1083,7 @@ private MatchGlob(String val) { @Override public Mapper listObjects() { - return GrpcStorageImpl.throwHttpJsonOnly( + return CrossTransportUtils.throwHttpJsonOnly( com.google.cloud.storage.Storage.BlobListOption.class, "matchGlob(String)"); } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java index d22b18294d..76219beccd 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TransportCompatibilityTest.java @@ -38,7 +38,7 @@ public void verifyUnsupportedMethodsGenerateMeaningfulException() { .setCredentials(NoCredentials.getInstance()) .build(); @SuppressWarnings("resource") - Storage s = new GrpcStorageImpl(options, null, Opts.empty()); + Storage s = new GrpcStorageImpl(options, null, null, Opts.empty()); ImmutableList messages = Stream.>of( s::batch, diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java new file mode 100644 index 0000000000..1bb24fb975 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java @@ -0,0 +1,121 @@ +/* + * 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.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.BlobWriteSession; +import com.google.cloud.storage.BlobWriteSessionConfigs; +import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.DataGenerator; +import com.google.cloud.storage.GrpcStorageOptions; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.Storage.BlobWriteOption; +import com.google.cloud.storage.StorageException; +import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.it.runner.StorageITRunner; +import com.google.cloud.storage.it.runner.annotations.Backend; +import com.google.cloud.storage.it.runner.annotations.Inject; +import com.google.cloud.storage.it.runner.annotations.SingleBackend; +import com.google.cloud.storage.it.runner.annotations.StorageFixture; +import com.google.cloud.storage.it.runner.registry.Generator; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(StorageITRunner.class) +@SingleBackend(Backend.PROD) +public final class ITBlobWriteSessionTest { + + @Inject + @StorageFixture(Transport.GRPC) + public Storage storage; + + @Inject public BucketInfo bucket; + + @Inject public Generator generator; + + @Test + public void allDefaults() throws Exception { + doTest(storage); + } + + @Test + public void overrideDefaultBufferSize() throws Exception { + GrpcStorageOptions options = + ((GrpcStorageOptions) storage.getOptions()) + .toBuilder() + .setBlobWriteSessionConfig( + BlobWriteSessionConfigs.getDefault().withChunkSize(256 * 1024)) + .build(); + try (Storage s = options.getService()) { + doTest(s); + } + } + + @Test + public void closingAnOpenedSessionWithoutCallingWriteShouldMakeAnEmptyObject() + throws IOException, ExecutionException, InterruptedException, TimeoutException { + BlobInfo info = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build(); + BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist()); + + WritableByteChannel open = session.open(); + open.close(); + BlobInfo gen1 = session.getResult().get(1, TimeUnit.SECONDS); + System.out.println("gen1 = " + gen1); + + assertThat(gen1.getSize()).isEqualTo(0); + } + + @Test + public void attemptingToOpenASessionWhichResultsInFailureShouldThrowAStorageException() { + // attempt to write to a bucket which we have not created + String badBucketName = bucket.getName() + "x"; + BlobInfo info = BlobInfo.newBuilder(badBucketName, generator.randomObjectName()).build(); + + BlobWriteSession session = storage.blobWriteSession(info, BlobWriteOption.doesNotExist()); + StorageException se = assertThrows(StorageException.class, () -> session.open().close()); + + assertThat(se.getCode()).isEqualTo(404); + assertThat(se).hasMessageThat().contains(badBucketName); + } + + private void doTest(Storage underTest) throws Exception { + BlobWriteSession sess = + underTest.blobWriteSession( + BlobInfo.newBuilder(bucket, generator.randomObjectName()).build(), + BlobWriteOption.doesNotExist()); + + byte[] bytes = DataGenerator.base64Characters().genBytes(512 * 1024); + try (WritableByteChannel w = sess.open()) { + w.write(ByteBuffer.wrap(bytes)); + } + + BlobInfo gen1 = sess.getResult().get(10, TimeUnit.SECONDS); + + byte[] allBytes = storage.readAllBytes(gen1.getBlobId()); + + assertThat(allBytes).isEqualTo(bytes); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java index 11440cb190..d264e5a6d0 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java @@ -25,6 +25,7 @@ import com.google.cloud.storage.Blob; import com.google.cloud.storage.BlobId; import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.BlobWriteSession; import com.google.cloud.storage.Bucket; import com.google.cloud.storage.BucketInfo; import com.google.cloud.storage.CopyWriter; @@ -478,6 +479,11 @@ public boolean deleteNotification(String bucket, String notificationId) { return delegate.deleteNotification(bucket, notificationId); } + @Override + public BlobWriteSession blobWriteSession(BlobInfo blobInfo, BlobWriteOption... options) { + return delegate.blobWriteSession(blobInfo, options); + } + @Override public void close() throws Exception { delegate.close(); From b61a9764a9d953d2b214edb2b543b8df42fbfa06 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Thu, 27 Jul 2023 15:04:24 -0400 Subject: [PATCH 04/17] fix(grpc): return error if credentials are detected to be null (#2142) --- .../main/java/com/google/cloud/storage/GrpcStorageOptions.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index 8bb3115c52..a6aa331350 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -48,6 +48,7 @@ import com.google.cloud.storage.UnifiedOpts.UserProject; import com.google.cloud.storage.spi.StorageRpcFactory; import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.storage.v2.ReadObjectRequest; @@ -169,6 +170,7 @@ private Tuple> resolveSettingsAndOpts() throw Opts defaultOpts = Opts.empty(); CredentialsProvider credentialsProvider; + Preconditions.checkState(credentials != null, "Unable to resolve credentials"); if (credentials instanceof NoCredentials) { credentialsProvider = NoCredentialsProvider.create(); } else { From 0b52f90a12b381520a15e5b36272c3799fe82b19 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Fri, 28 Jul 2023 14:21:06 -0400 Subject: [PATCH 05/17] chore: remove manual grpc version management (#2149) Now that shared-dependencies is tracking grpc 1.56.1 we no longer need to override it ourselves. --- pom.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pom.xml b/pom.xml index 787a218a5a..ad7e2756ca 100644 --- a/pom.xml +++ b/pom.xml @@ -59,13 +59,6 @@ - - io.grpc - grpc-bom - 1.56.1 - pom - import - com.google.cloud google-cloud-shared-dependencies From df9a1549477e4f7b4af2b3b273c2b6eecfb6f2a0 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Fri, 28 Jul 2023 14:21:21 -0400 Subject: [PATCH 06/17] chore: update BlobWriteSessionConfig to implement Serializable (#2147) BlobWriteSessionConfig needs to implement Serializable because it plugs into StorageOptions which is Serializable. Not sure, how the SerializationTest was passing when the PR was posted and now fails on main. --- .../java/com/google/cloud/storage/BlobWriteSessionConfig.java | 3 ++- .../google/cloud/storage/DefaultBlobWriteSessionConfig.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java index de8622c754..a67d5a20e6 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java @@ -23,6 +23,7 @@ import com.google.cloud.storage.UnifiedOpts.Opts; import com.google.storage.v2.WriteObjectResponse; import java.io.IOException; +import java.io.Serializable; import java.time.Clock; /** @@ -39,7 +40,7 @@ */ // When we have java modules, actually seal this to internal extension only @InternalApi -public abstract class BlobWriteSessionConfig { +public abstract class BlobWriteSessionConfig implements Serializable { @InternalApi BlobWriteSessionConfig() {} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java index dfea0d4190..9b4a39834f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java @@ -53,6 +53,7 @@ @Immutable @BetaApi public final class DefaultBlobWriteSessionConfig extends BlobWriteSessionConfig { + private static final long serialVersionUID = -6873740918589930633L; private final int chunkSize; From b318075e94c2e65bab1ee9e5384b895a151b58e8 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Mon, 31 Jul 2023 16:59:02 -0400 Subject: [PATCH 07/17] chore: optimize resumable uploads to allow sending bytes during finalization (#2146) Add new methods to UnbufferedWritableByteChannel to allow writing and closing in a single call * writeAndClose(ByteBuffer) * writeAndClose(ByteBuffer[]) * writeAndClose(ByteBuffer[], int, int) Update grpc and json implementation to leverage new methods and to write and finalize in the same call. DefaultBufferedWritableByteChannel will use the new methods as appropriate. --- .../clirr-ignored-differences.xml | 7 ++ .../ApiaryUnbufferedWritableByteChannel.java | 64 ++++++++----- .../DefaultBufferedWritableByteChannel.java | 13 ++- .../storage/DefaultStorageRetryStrategy.java | 2 +- .../GapicUnbufferedWritableByteChannel.java | 90 +++++++++++-------- .../cloud/storage/GrpcResumableSession.java | 1 + .../cloud/storage/StorageByteChannels.java | 16 ++++ .../UnbufferedWritableByteChannelSession.java | 16 +++- ...efaultBufferedWritableByteChannelTest.java | 57 ++++++++++++ ...apicUnbufferedWritableByteChannelTest.java | 51 +++++++++++ 10 files changed, 250 insertions(+), 67 deletions(-) diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml index 84d9907047..c7189a7a9e 100644 --- a/google-cloud-storage/clirr-ignored-differences.xml +++ b/google-cloud-storage/clirr-ignored-differences.xml @@ -8,4 +8,11 @@ com.google.cloud.storage.BlobWriteSession blobWriteSession(com.google.cloud.storage.BlobInfo, com.google.cloud.storage.Storage$BlobWriteOption[]) + + + 7012 + com/google/cloud/storage/UnbufferedWritableByteChannelSession$UnbufferedWritableByteChannel + * writeAndClose(*) + + diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedWritableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedWritableByteChannel.java index 8201fbc67d..da13857291 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedWritableByteChannel.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedWritableByteChannel.java @@ -57,6 +57,40 @@ final class ApiaryUnbufferedWritableByteChannel implements UnbufferedWritableByt @Override public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + return internalWrite(srcs, offset, length, false); + } + + @Override + public long writeAndClose(ByteBuffer[] srcs, int offset, int length) throws IOException { + long write = internalWrite(srcs, offset, length, true); + close(); + return write; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public void close() throws IOException { + open = false; + if (!finished) { + try { + ResumableOperationResult<@Nullable StorageObject> operationResult = + session.put(RewindableContent.empty(), HttpContentRange.of(cumulativeByteCount)); + long persistedSize = operationResult.getPersistedSize(); + committedBytesCallback.accept(persistedSize); + result.set(operationResult.getObject()); + } catch (Exception e) { + result.setException(e); + throw StorageException.coalesce(e); + } + } + } + + private long internalWrite(ByteBuffer[] srcs, int offset, int length, boolean finalize) + throws ClosedChannelException { if (!open) { throw new ClosedChannelException(); } @@ -65,9 +99,13 @@ public long write(ByteBuffer[] srcs, int offset, int length) throws IOException long newFinalByteOffset = cumulativeByteCount + available; final HttpContentRange header; ByteRangeSpec rangeSpec = ByteRangeSpec.explicit(cumulativeByteCount, newFinalByteOffset); - if (available % ByteSizeConstants._256KiB == 0) { + boolean quantumAligned = available % ByteSizeConstants._256KiB == 0; + if (quantumAligned && finalize) { + header = HttpContentRange.of(rangeSpec, newFinalByteOffset); + finished = true; + } else if (quantumAligned) { header = HttpContentRange.of(rangeSpec); - } else { + } else { // not quantum aligned, have to finalize header = HttpContentRange.of(rangeSpec, newFinalByteOffset); finished = true; } @@ -87,26 +125,4 @@ public long write(ByteBuffer[] srcs, int offset, int length) throws IOException throw StorageException.coalesce(e); } } - - @Override - public boolean isOpen() { - return open; - } - - @Override - public void close() throws IOException { - open = false; - if (!finished) { - try { - ResumableOperationResult<@Nullable StorageObject> operationResult = - session.put(RewindableContent.empty(), HttpContentRange.of(cumulativeByteCount)); - long persistedSize = operationResult.getPersistedSize(); - committedBytesCallback.accept(persistedSize); - result.set(operationResult.getObject()); - } catch (Exception e) { - result.setException(e); - throw StorageException.coalesce(e); - } - } - } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedWritableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedWritableByteChannel.java index 3514b7d664..f1b14d063b 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedWritableByteChannel.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBufferedWritableByteChannel.java @@ -160,8 +160,17 @@ public boolean isOpen() { @Override public void close() throws IOException { - try (UnbufferedWritableByteChannel ignored = channel) { - flush(); + if (enqueuedBytes()) { + ByteBuffer buffer = handle.get(); + Buffers.flip(buffer); + channel.writeAndClose(buffer); + if (buffer.hasRemaining()) { + buffer.compact(); + } else { + Buffers.clear(buffer); + } + } else { + channel.close(); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java index fb3ac380fd..cf85b7400d 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java @@ -145,7 +145,7 @@ private static final class EmptyJsonParsingExceptionInterceptor implements BaseI public RetryResult beforeEval(Exception exception) { if (exception instanceof IllegalArgumentException) { IllegalArgumentException illegalArgumentException = (IllegalArgumentException) exception; - if (illegalArgumentException.getMessage().equals("no JSON input found")) { + if ("no JSON input found".equals(illegalArgumentException.getMessage())) { return RetryResult.RETRY; } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java index 951a52edbc..ebe375b577 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannel.java @@ -63,12 +63,60 @@ final class GapicUnbufferedWritableByteChannel< } @Override - public long write(ByteBuffer[] srcs, int srcsOffset, int srcLength) throws IOException { + public long write(ByteBuffer[] srcs, int srcsOffset, int srcsLength) throws IOException { + return internalWrite(srcs, srcsOffset, srcsLength, false); + } + + @Override + public long writeAndClose(ByteBuffer[] srcs, int srcsOffset, int srcsLength) throws IOException { + long write = internalWrite(srcs, srcsOffset, srcsLength, true); + close(); + return write; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public void close() throws IOException { + if (!finished) { + long offset = writeCtx.getTotalSentBytes().get(); + Crc32cLengthKnown crc32cValue = writeCtx.getCumulativeCrc32c().get(); + + WriteObjectRequest.Builder b = + writeCtx.newRequestBuilder().setFinishWrite(true).setWriteOffset(offset); + if (crc32cValue != null) { + b.setObjectChecksums( + ObjectChecksums.newBuilder().setCrc32C(crc32cValue.getValue()).build()); + } + WriteObjectRequest message = b.build(); + try { + flusher.close(message); + finished = true; + } catch (RuntimeException e) { + resultFuture.setException(e); + throw e; + } + } else { + flusher.close(null); + } + open = false; + } + + @VisibleForTesting + WriteCtx getWriteCtx() { + return writeCtx; + } + + private long internalWrite(ByteBuffer[] srcs, int srcsOffset, int srcsLength, boolean finalize) + throws ClosedChannelException { if (!open) { throw new ClosedChannelException(); } - ChunkSegment[] data = chunkSegmenter.segmentBuffers(srcs, srcsOffset, srcLength); + ChunkSegment[] data = chunkSegmenter.segmentBuffers(srcs, srcsOffset, srcsLength); List messages = new ArrayList<>(); @@ -91,7 +139,7 @@ public long write(ByteBuffer[] srcs, int srcsOffset, int srcLength) throws IOExc .newRequestBuilder() .setWriteOffset(offset) .setChecksummedData(checksummedData.build()); - if (!datum.isOnlyFullBlocks()) { + if (!datum.isOnlyFullBlocks() || finalize) { builder.setFinishWrite(true); if (cumulative != null) { builder.setObjectChecksums( @@ -114,40 +162,4 @@ public long write(ByteBuffer[] srcs, int srcsOffset, int srcLength) throws IOExc return bytesConsumed; } - - @Override - public boolean isOpen() { - return open; - } - - @Override - public void close() throws IOException { - if (!finished) { - long offset = writeCtx.getTotalSentBytes().get(); - Crc32cLengthKnown crc32cValue = writeCtx.getCumulativeCrc32c().get(); - - WriteObjectRequest.Builder b = - writeCtx.newRequestBuilder().setFinishWrite(true).setWriteOffset(offset); - if (crc32cValue != null) { - b.setObjectChecksums( - ObjectChecksums.newBuilder().setCrc32C(crc32cValue.getValue()).build()); - } - WriteObjectRequest message = b.build(); - try { - flusher.close(message); - finished = true; - } catch (RuntimeException e) { - resultFuture.setException(e); - throw e; - } - } else { - flusher.close(null); - } - open = false; - } - - @VisibleForTesting - WriteCtx getWriteCtx() { - return writeCtx; - } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcResumableSession.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcResumableSession.java index 8de44fb654..2c60c9fb1a 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcResumableSession.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcResumableSession.java @@ -91,6 +91,7 @@ final class GrpcResumableSession { if (query.getObject() != null) { return query; } else { + handle.get().clear(); content.rewindTo(query.getPersistedSize()); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java index c3ff80138b..fb14d2e808 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageByteChannels.java @@ -184,6 +184,22 @@ public synchronized long write(ByteBuffer[] srcs, int offset, int length) throws return delegate.write(srcs, offset, length); } + @Override + public synchronized int writeAndClose(ByteBuffer src) throws IOException { + return delegate.writeAndClose(src); + } + + @Override + public synchronized long writeAndClose(ByteBuffer[] srcs) throws IOException { + return delegate.writeAndClose(srcs); + } + + @Override + public synchronized long writeAndClose(ByteBuffer[] srcs, int offset, int length) + throws IOException { + return delegate.writeAndClose(srcs, offset, length); + } + @Override public boolean isOpen() { return delegate.isOpen(); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnbufferedWritableByteChannelSession.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnbufferedWritableByteChannelSession.java index 8affde6b59..d7a5fcef60 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnbufferedWritableByteChannelSession.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnbufferedWritableByteChannelSession.java @@ -28,12 +28,26 @@ interface UnbufferedWritableByteChannelSession interface UnbufferedWritableByteChannel extends WritableByteChannel, GatheringByteChannel { @Override default int write(ByteBuffer src) throws IOException { - return Math.toIntExact(write(new ByteBuffer[] {src})); + return Math.toIntExact(write(new ByteBuffer[] {src}, 0, 1)); } @Override default long write(ByteBuffer[] srcs) throws IOException { return write(srcs, 0, srcs.length); } + + default int writeAndClose(ByteBuffer src) throws IOException { + return Math.toIntExact(writeAndClose(new ByteBuffer[] {src}, 0, 1)); + } + + default long writeAndClose(ByteBuffer[] srcs) throws IOException { + return writeAndClose(srcs, 0, srcs.length); + } + + default long writeAndClose(ByteBuffer[] srcs, int offset, int length) throws IOException { + long write = write(srcs, offset, length); + close(); + return write; + } } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultBufferedWritableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultBufferedWritableByteChannelTest.java index 23d5b98432..2653d27f02 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultBufferedWritableByteChannelTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultBufferedWritableByteChannelTest.java @@ -17,9 +17,11 @@ package com.google.cloud.storage; import static com.google.cloud.storage.ChunkSegmenterTest.TestData.fmt; +import static com.google.cloud.storage.TestUtils.xxd; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel; import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel; @@ -36,6 +38,7 @@ import java.util.Deque; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import net.jqwik.api.Arbitraries; import net.jqwik.api.Arbitrary; import net.jqwik.api.Combinators; @@ -343,6 +346,60 @@ void writeOpsOfGeneratesAccurately_2() { assertThat(actual).isEqualTo(expected); } + @Example + @SuppressWarnings("JUnit5AssertionsConverter") + void callingCloseWithBufferedDataShouldCallWriteAndClose() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + AtomicBoolean closed = new AtomicBoolean(false); + UnbufferedWritableByteChannel delegate = + new UnbufferedWritableByteChannel() { + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + fail("unexpected write(ByteBuffer[], int, int) call"); + return 0; + } + + @Override + public long writeAndClose(ByteBuffer[] srcs, int offset, int length) throws IOException { + long total = 0; + try (WritableByteChannel out = Channels.newChannel(baos)) { + for (ByteBuffer src : srcs) { + total += out.write(src); + } + } + closed.compareAndSet(false, true); + return total; + } + + @Override + public boolean isOpen() { + return !closed.get(); + } + + @Override + public void close() throws IOException { + fail("unexpected close() call"); + } + }; + DefaultBufferedWritableByteChannel test = + new DefaultBufferedWritableByteChannel(BufferHandle.allocate(20), delegate); + + byte[] bytes = DataGenerator.base64Characters().genBytes(10); + String expected = xxd(bytes); + + int write = test.write(ByteBuffer.wrap(bytes)); + assertThat(write).isEqualTo(10); + + assertThat(closed.get()).isFalse(); + + test.close(); + + String actual = xxd(baos.toByteArray()); + assertThat(actual).isEqualTo(expected); + assertThat(closed.get()).isTrue(); + } + @Property void bufferAllocationShouldOnlyHappenWhenNeeded(@ForAll("BufferSizes") WriteOps writeOps) throws IOException { diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java index 08d228c27a..3b2bf1bf2d 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGapicUnbufferedWritableByteChannelTest.java @@ -17,6 +17,7 @@ package com.google.cloud.storage; import static com.google.cloud.storage.TestUtils.apiException; +import static com.google.cloud.storage.TestUtils.getChecksummedData; import static com.google.common.truth.Truth.assertThat; import com.google.api.core.SettableApiFuture; @@ -25,6 +26,7 @@ import com.google.api.gax.rpc.PermissionDeniedException; import com.google.cloud.storage.Retrying.RetryingDependencies; import com.google.cloud.storage.WriteCtx.WriteObjectRequestBuilderFactory; +import com.google.cloud.storage.WriteFlushStrategy.Flusher; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -48,10 +50,13 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.logging.Logger; import java.util.stream.Collector; import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Test; public final class ITGapicUnbufferedWritableByteChannelTest { @@ -307,6 +312,52 @@ public boolean shouldRetry(Throwable t, Object ignore) { assertThat(writeCtx.getConfirmedBytes().get()).isEqualTo(40); } + @Test + public void resumableUpload_finalizeWhenWriteAndCloseCalledEvenWhenQuantumAligned() + throws IOException, InterruptedException, ExecutionException { + int quantum = 10; + ChunkSegmenter segmenter = + new ChunkSegmenter(Hasher.noop(), ByteStringStrategy.copy(), 50, quantum); + SettableApiFuture result = SettableApiFuture.create(); + + AtomicReference> actualFlush = new AtomicReference<>(); + WriteObjectRequest closeRequestSentinel = + WriteObjectRequest.newBuilder().setUploadId("sentinel").build(); + AtomicReference actualClose = new AtomicReference<>(closeRequestSentinel); + GapicUnbufferedWritableByteChannel c = + new GapicUnbufferedWritableByteChannel<>( + result, + segmenter, + reqFactory, + (bucketName, committedTotalBytesCallback, onSuccessCallback) -> + new Flusher() { + @Override + public void flush(@NonNull List segments) { + actualFlush.compareAndSet(null, segments); + } + + @Override + public void close(@Nullable WriteObjectRequest req) { + actualClose.compareAndSet(closeRequestSentinel, req); + } + }); + + byte[] bytes = DataGenerator.base64Characters().genBytes(quantum); + + long written = c.writeAndClose(ByteBuffer.wrap(bytes)); + WriteObjectRequest expectedRequest = + WriteObjectRequest.newBuilder() + .setUploadId(uploadId) + .setChecksummedData(getChecksummedData(ByteString.copyFrom(bytes), Hasher.noop())) + .setFinishWrite(true) + .build(); + + assertThat(written).isEqualTo(10); + assertThat(actualFlush.get()).isEqualTo(ImmutableList.of(expectedRequest)); + // calling close is okay, as long as the provided request is null + assertThat(actualClose.get()).isAnyOf(closeRequestSentinel, null); + } + static class DirectWriteService extends StorageImplBase { private static final Logger LOGGER = Logger.getLogger(DirectWriteService.class.getName()); private final BiConsumer, List> c; From a422a70d849aabbae932227dc75671710775a59b Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Mon, 31 Jul 2023 18:21:47 -0400 Subject: [PATCH 08/17] chore: introduce Throughput and ThroughputSink (#2138) _Pre-work_ In some upcoming work we want to be able to keep a running window of throughput performance in order to provide improved throughput. This PR introduces a new utility class to model and compute throughput, and the concept of a ThroughputSink which values can be appended to. --- .../com/google/cloud/storage/Throughput.java | 94 ++++ .../cloud/storage/ThroughputMovingWindow.java | 98 ++++ .../google/cloud/storage/ThroughputSink.java | 251 +++++++++++ .../com/google/cloud/storage/TestClock.java | 62 +++ .../ThroughputMovingWindowPropertyTest.java | 422 ++++++++++++++++++ .../cloud/storage/ThroughputSinkTest.java | 262 +++++++++++ .../google/cloud/storage/ThroughputTest.java | 35 ++ 7 files changed, 1224 insertions(+) create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/Throughput.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputMovingWindow.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputMovingWindowPropertyTest.java create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputSinkTest.java create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputTest.java diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Throughput.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Throughput.java new file mode 100644 index 0000000000..f0f54140d1 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Throughput.java @@ -0,0 +1,94 @@ +/* + * 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.common.base.MoreObjects; +import java.time.Duration; +import java.util.Objects; + +/** + * Convenience class to encapsulate the concept of a throughput value. + * + *

Given a number of bytes and a duration compute the number of bytes per second. + */ +final class Throughput { + + private static final double NANOS_PER_SECOND = 1_000_000_000d; + private final long numBytes; + private final Duration duration; + + // TODO: is there a efficient way we can limit precision without having to use BigDecimal? + // Realistically, we don't need precision smaller than 1 byte per microsecond, leading to + // 6 digits past the decimal of needed precision. + private final double bytesPerSecond; + + private Throughput(long numBytes, Duration duration) { + this.numBytes = numBytes; + this.duration = duration; + this.bytesPerSecond = numBytes / (duration.toNanos() / NANOS_PER_SECOND); + } + + public long getNumBytes() { + return numBytes; + } + + public Duration getDuration() { + return duration; + } + + public double toBps() { + return bytesPerSecond; + } + + public Throughput plus(Throughput other) { + return new Throughput(this.numBytes + other.numBytes, this.duration.plus(other.duration)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Throughput)) { + return false; + } + Throughput that = (Throughput) o; + return Double.compare(that.bytesPerSecond, bytesPerSecond) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(bytesPerSecond); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("bytesPerSecond", bytesPerSecond).toString(); + } + + public static Throughput zero() { + return new Throughput(0, Duration.ZERO); + } + + public static Throughput of(long numBytes, Duration duration) { + return new Throughput(numBytes, duration); + } + + public static Throughput bytesPerSecond(long numBytes) { + return new Throughput(numBytes, Duration.ofSeconds(1)); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputMovingWindow.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputMovingWindow.java new file mode 100644 index 0000000000..6afae99172 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputMovingWindow.java @@ -0,0 +1,98 @@ +/* + * 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.common.base.MoreObjects; +import java.time.Duration; +import java.time.Instant; +import java.util.Comparator; +import java.util.PriorityQueue; + +/** + * A simple moving window implementation which will keep a {@code window}s worth of Throughput + * values and allow querying for the aggregate avg over that time window. + */ +final class ThroughputMovingWindow { + + private final Duration window; + + private final PriorityQueue values; + + private ThroughputMovingWindow(Duration window) { + this.window = window; + this.values = new PriorityQueue<>(Entry.COMP); + } + + void add(Instant now, Throughput value) { + removeExpiredEntries(now); + values.add(new Entry(now, value)); + } + + Throughput avg(Instant now) { + removeExpiredEntries(now); + return values.stream() + .map(Entry::getValue) + .reduce( + Throughput.zero(), + (tp1, tp2) -> Throughput.of(tp1.getNumBytes() + tp2.getNumBytes(), window)); + } + + private void removeExpiredEntries(Instant now) { + Instant newMin = now.minus(window); + values.removeIf(e -> lteq(e.getAt(), newMin)); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("window", window) + .add("values.size()", values.size()) + .toString(); + } + + static ThroughputMovingWindow of(Duration window) { + return new ThroughputMovingWindow(window); + } + + private static boolean lteq(Instant a, Instant b) { + return a.equals(b) || a.isBefore(b); + } + + private static final class Entry { + private static final Comparator COMP = Comparator.comparing(e -> e.at); + private final Instant at; + private final Throughput value; + + private Entry(Instant at, Throughput value) { + this.at = at; + this.value = value; + } + + public Instant getAt() { + return at; + } + + public Throughput getValue() { + return value; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("at", at).add("value", value).toString(); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java new file mode 100644 index 0000000000..5ef6e37d10 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java @@ -0,0 +1,251 @@ +/* + * 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.common.base.MoreObjects; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import java.util.logging.Logger; + +/** + * Interface to mark a location in which throughput of byte movements can be recorded, and which can + * provide a decorated underlying channel. + */ +interface ThroughputSink { + + void recordThroughput(Record r); + + WritableByteChannel decorate(WritableByteChannel wbc); + + static void computeThroughput(Clock clock, ThroughputSink sink, long numBytes, IO io) + throws IOException { + boolean exception = false; + Instant begin = clock.instant(); + try { + io.apply(); + } catch (IOException e) { + exception = true; + throw e; + } finally { + Instant end = clock.instant(); + Record record = Record.of(numBytes, begin, end, exception); + sink.recordThroughput(record); + } + } + + @FunctionalInterface + interface IO { + void apply() throws IOException; + } + + static ThroughputSink logged(String prefix, Clock clock) { + return new LoggedThroughputSink(prefix, clock); + } + + static ThroughputSink windowed(ThroughputMovingWindow w, Clock clock) { + return new ThroughputMovingWindowThroughputSink(w, clock); + } + + static ThroughputSink tee(ThroughputSink a, ThroughputSink b) { + return new TeeThroughputSink(a, b); + } + + final class Record { + private final long numBytes; + private final Instant begin; + private final Instant end; + private final boolean exception; + + private Record(long numBytes, Instant begin, Instant end, boolean exception) { + this.numBytes = numBytes; + this.begin = begin; + this.end = end; + this.exception = exception; + } + + public long getNumBytes() { + return numBytes; + } + + public Instant getBegin() { + return begin; + } + + public Instant getEnd() { + return end; + } + + public Duration getDuration() { + return Duration.between(begin, end); + } + + public boolean isException() { + return exception; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Record)) { + return false; + } + Record record = (Record) o; + return numBytes == record.numBytes + && exception == record.exception + && Objects.equals(begin, record.begin) + && Objects.equals(end, record.end); + } + + @Override + public int hashCode() { + return Objects.hash(numBytes, begin, end, exception); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("numBytes", numBytes) + .add("begin", begin) + .add("end", end) + .add("exception", exception) + .toString(); + } + + public static Record of(long numBytes, Instant begin, Instant end, boolean exception) { + return new Record(numBytes, begin, end, exception); + } + } + + final class LoggedThroughputSink implements ThroughputSink { + private static final Logger LOGGER = Logger.getLogger(ThroughputSink.class.getName()); + + private final String prefix; + private final Clock clock; + + private LoggedThroughputSink(String prefix, Clock clock) { + this.prefix = prefix; + this.clock = clock; + } + + private static final double MiB = 1d / (1024 * 1024); + + @Override + public void recordThroughput(Record r) { + LOGGER.info( + () -> + String.format( + "{%s} (%01.03f MiB/s) %s", + prefix, + ((r.numBytes * MiB) + / (Duration.between(r.getBegin(), r.getEnd()).toMillis() / 1000d)), + r)); + } + + @Override + public WritableByteChannel decorate(WritableByteChannel wbc) { + return new ThroughputRecordingWritableByteChannel(wbc, this, clock); + } + } + + final class ThroughputRecordingWritableByteChannel implements WritableByteChannel { + private final WritableByteChannel delegate; + private final ThroughputSink sink; + private final Clock clock; + + private ThroughputRecordingWritableByteChannel( + WritableByteChannel delegate, ThroughputSink sink, Clock clock) { + this.delegate = delegate; + this.sink = sink; + this.clock = clock; + } + + @Override + public int write(ByteBuffer src) throws IOException { + boolean exception = false; + int remaining = src.remaining(); + Instant begin = clock.instant(); + try { + return delegate.write(src); + } catch (IOException e) { + exception = true; + throw e; + } finally { + Instant end = clock.instant(); + Record record = Record.of(remaining - src.remaining(), begin, end, exception); + sink.recordThroughput(record); + } + } + + @Override + public boolean isOpen() { + return delegate.isOpen(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + } + + final class TeeThroughputSink implements ThroughputSink { + private final ThroughputSink a; + private final ThroughputSink b; + + private TeeThroughputSink(ThroughputSink a, ThroughputSink b) { + this.a = a; + this.b = b; + } + + @Override + public void recordThroughput(Record r) { + a.recordThroughput(r); + b.recordThroughput(r); + } + + @Override + public WritableByteChannel decorate(WritableByteChannel wbc) { + return b.decorate(a.decorate(wbc)); + } + } + + final class ThroughputMovingWindowThroughputSink implements ThroughputSink { + private final ThroughputMovingWindow w; + private final Clock clock; + + private ThroughputMovingWindowThroughputSink(ThroughputMovingWindow w, Clock clock) { + this.w = w; + this.clock = clock; + } + + @Override + public synchronized void recordThroughput(Record r) { + w.add(r.end, Throughput.of(r.getNumBytes(), r.getDuration())); + } + + @Override + public WritableByteChannel decorate(WritableByteChannel wbc) { + return new ThroughputRecordingWritableByteChannel(wbc, this, clock); + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java new file mode 100644 index 0000000000..e37a4f0248 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java @@ -0,0 +1,62 @@ +/* + * 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 java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.function.UnaryOperator; + +final class TestClock extends Clock { + + private final Instant begin; + private final UnaryOperator next; + + private Instant now; + + private TestClock(Instant begin, UnaryOperator next) { + this.begin = begin; + this.next = next; + this.now = begin; + } + + @Override + public ZoneId getZone() { + throw new UnsupportedOperationException("TestClock.getZone()"); + } + + @Override + public Clock withZone(ZoneId zone) { + throw new UnsupportedOperationException("TestClock.withZone()"); + } + + @Override + public Instant instant() { + Instant ret = now; + now = next.apply(now); + return ret; + } + + public static TestClock tickBy(Instant begin, Duration d) { + return of(begin, i -> i.plus(d)); + } + + public static TestClock of(Instant begin, UnaryOperator next) { + return new TestClock(begin, next); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputMovingWindowPropertyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputMovingWindowPropertyTest.java new file mode 100644 index 0000000000..181f51aedf --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputMovingWindowPropertyTest.java @@ -0,0 +1,422 @@ +/* + * 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 static com.google.cloud.storage.ByteSizeConstants._5TiB; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.time.Instant.EPOCH; +import static java.time.Instant.ofEpochSecond; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.Combinators; +import net.jqwik.api.Example; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; +import net.jqwik.api.Tuple; +import net.jqwik.api.Tuple.Tuple1; +import net.jqwik.time.api.DateTimes; +import net.jqwik.time.api.Times; + +final class ThroughputMovingWindowPropertyTest { + + private static final double TOLERANCE = 0.001; + + @Example + void canned() { + test(CANNED_SCENARIO); + } + + @Example + void twoEntriesSameTimeDifferentThroughput() { + Duration ms = Duration.ofMillis(1); + ScenarioTimeline scenario = + new ScenarioTimeline( + ms, + ImmutableList.of( + new TimelineEntry(EPOCH, Throughput.of(1, ms), 1000.0), + new TimelineEntry(EPOCH, Throughput.of(0, ms), 1000.0))); + test(scenario); + } + + @Property + void test(@ForAll("Scenarios") ScenarioTimeline scenario) { + ThroughputMovingWindow window = ThroughputMovingWindow.of(scenario.d); + for (TimelineEntry timelineEntry : scenario.timelineEntries) { + window.add(timelineEntry.i, timelineEntry.t); + Throughput throughput = window.avg(timelineEntry.i); + assertWithMessage(timelineEntry.toString()) + .that(throughput.toBps()) + .isWithin(TOLERANCE) + .of(timelineEntry.expectedMovingAvgBytesPerSecond); + } + } + + @Provide("Scenarios") + static Arbitrary scenarioTimeline() { + return Times.durations() + .ofPrecision(ChronoUnit.MILLIS) + .between(Duration.ofMillis(1), Duration.ofMinutes(10)) + .flatMap( + d -> + Combinators.combine( + Arbitraries.just(d), + // pick an instant, then generate 1 to 100 values between it and d * 3 + DateTimes.instants() + .ofPrecision(ChronoUnit.MILLIS) + .flatMap( + i -> + DateTimes.instants() + .ofPrecision(ChronoUnit.MILLIS) + .between(i, i.plus(d.multipliedBy(3))) + .flatMap( + ii -> + Combinators.combine( + Arbitraries.just(ii), throughput()) + .as(Tuple::of)) + .list() + .ofMinSize(1) + .ofMaxSize(100))) + .as(Tuple::of)) + .map(ScenarioTimeline::create); + } + + static Arbitrary throughput() { + return Times.durations() + .ofPrecision(ChronoUnit.MILLIS) + .between(Duration.ofMillis(1), Duration.ofMinutes(10)) + .flatMap(d -> Arbitraries.longs().between(0, _5TiB).map(n -> Throughput.of(n, d))); + } + + private static final class ScenarioTimeline { + + private static final Comparator> COMP = + Comparator.comparing(Tuple1::get1); + private final Duration d; + private final List timelineEntries; + + private ScenarioTimeline(Duration d, List timelineEntries) { + this.d = d; + this.timelineEntries = timelineEntries; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("d", d) + .add("timelineEntries", timelineEntries) + .toString(); + } + + static ScenarioTimeline create( + Tuple.Tuple2>> tuples) { + + Duration d = tuples.get1(); + List> pairs = tuples.get2(); + + List> tmp = + pairs.stream().sorted(COMP).collect(Collectors.toList()); + + List>> windows = new ArrayList<>(); + int last = tmp.size() - 1; + for (int i = last; i >= 0; i--) { + List> window = new ArrayList<>(); + Tuple.Tuple2 t = tmp.get(i); + window.add(t); + Instant min = t.get1().minus(d); + for (int j = i - 1; j >= 0; j--) { + Tuple.Tuple2 r = tmp.get(j); + if (r.get1().isAfter(min)) { + window.add(r); + } + } + windows.add(ImmutableList.copyOf(window)); + } + + ImmutableList timelineEntries = + windows.stream() + .map( + w -> { + Tuple.Tuple2 max = w.get(0); + Throughput reduce = + w.stream() + .map(Tuple.Tuple2::get2) + .reduce(Throughput.zero(), Throughput::plus); + return new TimelineEntry( + max.get1(), max.get2(), Throughput.of(reduce.getNumBytes(), d).toBps()); + }) + .collect(ImmutableList.toImmutableList()); + return new ScenarioTimeline(d, timelineEntries.reverse()); + } + } + + private static final class TimelineEntry { + private final Instant i; + private final Throughput t; + private final double expectedMovingAvgBytesPerSecond; + + private TimelineEntry(Instant i, Throughput t, double expectedMovingAvgBytesPerSecond) { + this.i = i; + this.t = t; + this.expectedMovingAvgBytesPerSecond = expectedMovingAvgBytesPerSecond; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("i", i) + .add("t", t) + .add("tenSecMovingAvg", String.format("%,.03f", expectedMovingAvgBytesPerSecond)) + .toString(); + } + } + + private static final ScenarioTimeline CANNED_SCENARIO = + new ScenarioTimeline( + Duration.ofSeconds(10), + ImmutableList.builder() + .add(new TimelineEntry(ofEpochSecond(1), Throughput.bytesPerSecond(192), 19.2)) + .add(new TimelineEntry(ofEpochSecond(2), Throughput.bytesPerSecond(1185), 137.7)) + .add(new TimelineEntry(ofEpochSecond(3), Throughput.bytesPerSecond(1363), 274.)) + .add(new TimelineEntry(ofEpochSecond(4), Throughput.bytesPerSecond(234), 297.4)) + .add(new TimelineEntry(ofEpochSecond(5), Throughput.bytesPerSecond(1439), 441.3)) + .add(new TimelineEntry(ofEpochSecond(6), Throughput.bytesPerSecond(1269), 568.2)) + .add(new TimelineEntry(ofEpochSecond(7), Throughput.bytesPerSecond(692), 637.4)) + .add(new TimelineEntry(ofEpochSecond(8), Throughput.bytesPerSecond(667), 704.1)) + .add(new TimelineEntry(ofEpochSecond(9), Throughput.bytesPerSecond(1318), 835.9)) + .add(new TimelineEntry(ofEpochSecond(10), Throughput.bytesPerSecond(1125), 948.4)) + .add(new TimelineEntry(ofEpochSecond(11), Throughput.bytesPerSecond(1124), 1041.6)) + .add(new TimelineEntry(ofEpochSecond(12), Throughput.bytesPerSecond(3), 923.4)) + .add(new TimelineEntry(ofEpochSecond(13), Throughput.bytesPerSecond(185), 805.6)) + .add(new TimelineEntry(ofEpochSecond(14), Throughput.bytesPerSecond(726), 854.8)) + .add(new TimelineEntry(ofEpochSecond(15), Throughput.bytesPerSecond(630), 773.9)) + .add(new TimelineEntry(ofEpochSecond(16), Throughput.bytesPerSecond(874), 734.4)) + .add(new TimelineEntry(ofEpochSecond(17), Throughput.bytesPerSecond(1401), 805.3)) + .add(new TimelineEntry(ofEpochSecond(18), Throughput.bytesPerSecond(533), 791.9)) + .add(new TimelineEntry(ofEpochSecond(19), Throughput.bytesPerSecond(446), 704.7)) + .add(new TimelineEntry(ofEpochSecond(20), Throughput.bytesPerSecond(801), 672.3)) + .add(new TimelineEntry(ofEpochSecond(21), Throughput.bytesPerSecond(61), 566.)) + .add(new TimelineEntry(ofEpochSecond(22), Throughput.bytesPerSecond(1104), 676.1)) + .add(new TimelineEntry(ofEpochSecond(23), Throughput.bytesPerSecond(972), 754.8)) + .add(new TimelineEntry(ofEpochSecond(24), Throughput.bytesPerSecond(1310), 813.2)) + .add(new TimelineEntry(ofEpochSecond(25), Throughput.bytesPerSecond(408), 791.)) + .add(new TimelineEntry(ofEpochSecond(26), Throughput.bytesPerSecond(759), 779.5)) + .add(new TimelineEntry(ofEpochSecond(27), Throughput.bytesPerSecond(674), 706.8)) + .add(new TimelineEntry(ofEpochSecond(28), Throughput.bytesPerSecond(314), 684.9)) + .add(new TimelineEntry(ofEpochSecond(29), Throughput.bytesPerSecond(1311), 771.4)) + .add(new TimelineEntry(ofEpochSecond(30), Throughput.bytesPerSecond(449), 736.2)) + .add(new TimelineEntry(ofEpochSecond(31), Throughput.bytesPerSecond(1273), 857.4)) + .add(new TimelineEntry(ofEpochSecond(32), Throughput.bytesPerSecond(228), 769.8)) + .add(new TimelineEntry(ofEpochSecond(33), Throughput.bytesPerSecond(605), 733.1)) + .add(new TimelineEntry(ofEpochSecond(34), Throughput.bytesPerSecond(537), 655.8)) + .add(new TimelineEntry(ofEpochSecond(35), Throughput.bytesPerSecond(1498), 764.8)) + .add(new TimelineEntry(ofEpochSecond(36), Throughput.bytesPerSecond(694), 758.3)) + .add(new TimelineEntry(ofEpochSecond(37), Throughput.bytesPerSecond(155), 706.4)) + .add(new TimelineEntry(ofEpochSecond(38), Throughput.bytesPerSecond(983), 773.3)) + .add(new TimelineEntry(ofEpochSecond(39), Throughput.bytesPerSecond(1359), 778.1)) + .add(new TimelineEntry(ofEpochSecond(40), Throughput.bytesPerSecond(832), 816.4)) + .add(new TimelineEntry(ofEpochSecond(41), Throughput.bytesPerSecond(1041), 793.2)) + .add(new TimelineEntry(ofEpochSecond(42), Throughput.bytesPerSecond(1459), 916.3)) + .add(new TimelineEntry(ofEpochSecond(43), Throughput.bytesPerSecond(1128), 968.6)) + .add(new TimelineEntry(ofEpochSecond(44), Throughput.bytesPerSecond(1318), 1046.7)) + .add(new TimelineEntry(ofEpochSecond(45), Throughput.bytesPerSecond(620), 958.9)) + .add(new TimelineEntry(ofEpochSecond(46), Throughput.bytesPerSecond(1133), 1002.8)) + .add(new TimelineEntry(ofEpochSecond(47), Throughput.bytesPerSecond(568), 1044.1)) + .add(new TimelineEntry(ofEpochSecond(48), Throughput.bytesPerSecond(561), 1001.9)) + .add(new TimelineEntry(ofEpochSecond(49), Throughput.bytesPerSecond(1483), 1014.3)) + .add(new TimelineEntry(ofEpochSecond(50), Throughput.bytesPerSecond(1405), 1071.6)) + .add(new TimelineEntry(ofEpochSecond(51), Throughput.bytesPerSecond(435), 1011.)) + .add(new TimelineEntry(ofEpochSecond(52), Throughput.bytesPerSecond(664), 931.5)) + .add(new TimelineEntry(ofEpochSecond(53), Throughput.bytesPerSecond(1330), 951.7)) + .add(new TimelineEntry(ofEpochSecond(54), Throughput.bytesPerSecond(540), 873.9)) + .add(new TimelineEntry(ofEpochSecond(55), Throughput.bytesPerSecond(847), 896.6)) + .add(new TimelineEntry(ofEpochSecond(56), Throughput.bytesPerSecond(1231), 906.4)) + .add(new TimelineEntry(ofEpochSecond(57), Throughput.bytesPerSecond(1331), 982.7)) + .add(new TimelineEntry(ofEpochSecond(58), Throughput.bytesPerSecond(154), 942.)) + .add(new TimelineEntry(ofEpochSecond(59), Throughput.bytesPerSecond(801), 873.8)) + .add(new TimelineEntry(ofEpochSecond(60), Throughput.bytesPerSecond(499), 783.2)) + .add(new TimelineEntry(ofEpochSecond(61), Throughput.bytesPerSecond(766), 816.3)) + .add(new TimelineEntry(ofEpochSecond(62), Throughput.bytesPerSecond(1166), 866.5)) + .add(new TimelineEntry(ofEpochSecond(63), Throughput.bytesPerSecond(1408), 874.3)) + .add(new TimelineEntry(ofEpochSecond(64), Throughput.bytesPerSecond(1145), 934.8)) + .add(new TimelineEntry(ofEpochSecond(65), Throughput.bytesPerSecond(433), 893.4)) + .add(new TimelineEntry(ofEpochSecond(66), Throughput.bytesPerSecond(1256), 895.9)) + .add(new TimelineEntry(ofEpochSecond(67), Throughput.bytesPerSecond(847), 847.5)) + .add(new TimelineEntry(ofEpochSecond(68), Throughput.bytesPerSecond(1421), 974.2)) + .add(new TimelineEntry(ofEpochSecond(69), Throughput.bytesPerSecond(347), 928.8)) + .add(new TimelineEntry(ofEpochSecond(70), Throughput.bytesPerSecond(52), 884.1)) + .add(new TimelineEntry(ofEpochSecond(71), Throughput.bytesPerSecond(19), 809.4)) + .add(new TimelineEntry(ofEpochSecond(72), Throughput.bytesPerSecond(1191), 811.9)) + .add(new TimelineEntry(ofEpochSecond(73), Throughput.bytesPerSecond(104), 681.5)) + .add(new TimelineEntry(ofEpochSecond(74), Throughput.bytesPerSecond(640), 631.)) + .add(new TimelineEntry(ofEpochSecond(75), Throughput.bytesPerSecond(535), 641.2)) + .add(new TimelineEntry(ofEpochSecond(76), Throughput.bytesPerSecond(203), 535.9)) + .add(new TimelineEntry(ofEpochSecond(77), Throughput.bytesPerSecond(51), 456.3)) + .add(new TimelineEntry(ofEpochSecond(78), Throughput.bytesPerSecond(1117), 425.9)) + .add(new TimelineEntry(ofEpochSecond(79), Throughput.bytesPerSecond(1390), 530.2)) + .add(new TimelineEntry(ofEpochSecond(80), Throughput.bytesPerSecond(262), 551.2)) + .add(new TimelineEntry(ofEpochSecond(81), Throughput.bytesPerSecond(5), 549.8)) + .add(new TimelineEntry(ofEpochSecond(82), Throughput.bytesPerSecond(802), 510.9)) + .add(new TimelineEntry(ofEpochSecond(83), Throughput.bytesPerSecond(529), 553.4)) + .add(new TimelineEntry(ofEpochSecond(84), Throughput.bytesPerSecond(1261), 615.5)) + .add(new TimelineEntry(ofEpochSecond(85), Throughput.bytesPerSecond(1192), 681.2)) + .add(new TimelineEntry(ofEpochSecond(86), Throughput.bytesPerSecond(276), 688.5)) + .add(new TimelineEntry(ofEpochSecond(87), Throughput.bytesPerSecond(457), 729.1)) + .add(new TimelineEntry(ofEpochSecond(88), Throughput.bytesPerSecond(799), 697.3)) + .add(new TimelineEntry(ofEpochSecond(89), Throughput.bytesPerSecond(443), 602.6)) + .add(new TimelineEntry(ofEpochSecond(90), Throughput.bytesPerSecond(1281), 704.5)) + .add(new TimelineEntry(ofEpochSecond(91), Throughput.bytesPerSecond(97), 713.7)) + .add(new TimelineEntry(ofEpochSecond(92), Throughput.bytesPerSecond(895), 723.)) + .add(new TimelineEntry(ofEpochSecond(93), Throughput.bytesPerSecond(1338), 803.9)) + .add(new TimelineEntry(ofEpochSecond(94), Throughput.bytesPerSecond(554), 733.2)) + .add(new TimelineEntry(ofEpochSecond(95), Throughput.bytesPerSecond(302), 644.2)) + .add(new TimelineEntry(ofEpochSecond(96), Throughput.bytesPerSecond(518), 668.4)) + .add(new TimelineEntry(ofEpochSecond(97), Throughput.bytesPerSecond(502), 672.9)) + .add(new TimelineEntry(ofEpochSecond(98), Throughput.bytesPerSecond(517), 644.7)) + .add(new TimelineEntry(ofEpochSecond(99), Throughput.bytesPerSecond(172), 617.6)) + .add(new TimelineEntry(ofEpochSecond(100), Throughput.bytesPerSecond(909), 580.4)) + .add(new TimelineEntry(ofEpochSecond(101), Throughput.bytesPerSecond(1233), 694.)) + .add(new TimelineEntry(ofEpochSecond(102), Throughput.bytesPerSecond(189), 623.4)) + .add(new TimelineEntry(ofEpochSecond(103), Throughput.bytesPerSecond(244), 514.)) + .add(new TimelineEntry(ofEpochSecond(104), Throughput.bytesPerSecond(886), 547.2)) + .add(new TimelineEntry(ofEpochSecond(105), Throughput.bytesPerSecond(796), 596.6)) + .add(new TimelineEntry(ofEpochSecond(106), Throughput.bytesPerSecond(1072), 652.)) + .add(new TimelineEntry(ofEpochSecond(107), Throughput.bytesPerSecond(602), 662.)) + .add(new TimelineEntry(ofEpochSecond(108), Throughput.bytesPerSecond(507), 661.)) + .add(new TimelineEntry(ofEpochSecond(109), Throughput.bytesPerSecond(432), 687.)) + .add(new TimelineEntry(ofEpochSecond(110), Throughput.bytesPerSecond(661), 662.2)) + .add(new TimelineEntry(ofEpochSecond(111), Throughput.bytesPerSecond(1085), 647.4)) + .add(new TimelineEntry(ofEpochSecond(112), Throughput.bytesPerSecond(157), 644.2)) + .add(new TimelineEntry(ofEpochSecond(113), Throughput.bytesPerSecond(529), 672.7)) + .add(new TimelineEntry(ofEpochSecond(114), Throughput.bytesPerSecond(31), 587.2)) + .add(new TimelineEntry(ofEpochSecond(115), Throughput.bytesPerSecond(464), 554.)) + .add(new TimelineEntry(ofEpochSecond(116), Throughput.bytesPerSecond(1301), 576.9)) + .add(new TimelineEntry(ofEpochSecond(117), Throughput.bytesPerSecond(787), 595.4)) + .add(new TimelineEntry(ofEpochSecond(118), Throughput.bytesPerSecond(908), 635.5)) + .add(new TimelineEntry(ofEpochSecond(119), Throughput.bytesPerSecond(1316), 723.9)) + .add(new TimelineEntry(ofEpochSecond(120), Throughput.bytesPerSecond(764), 734.2)) + .add(new TimelineEntry(ofEpochSecond(121), Throughput.bytesPerSecond(1391), 764.8)) + .add(new TimelineEntry(ofEpochSecond(122), Throughput.bytesPerSecond(819), 831.)) + .add(new TimelineEntry(ofEpochSecond(123), Throughput.bytesPerSecond(219), 800.)) + .add(new TimelineEntry(ofEpochSecond(124), Throughput.bytesPerSecond(601), 857.)) + .add(new TimelineEntry(ofEpochSecond(125), Throughput.bytesPerSecond(1238), 934.4)) + .add(new TimelineEntry(ofEpochSecond(126), Throughput.bytesPerSecond(1392), 943.5)) + .add(new TimelineEntry(ofEpochSecond(127), Throughput.bytesPerSecond(499), 914.7)) + .add(new TimelineEntry(ofEpochSecond(128), Throughput.bytesPerSecond(1153), 939.2)) + .add(new TimelineEntry(ofEpochSecond(129), Throughput.bytesPerSecond(1219), 929.5)) + .add(new TimelineEntry(ofEpochSecond(130), Throughput.bytesPerSecond(519), 905.)) + .add(new TimelineEntry(ofEpochSecond(131), Throughput.bytesPerSecond(337), 799.6)) + .add(new TimelineEntry(ofEpochSecond(132), Throughput.bytesPerSecond(1065), 824.2)) + .add(new TimelineEntry(ofEpochSecond(133), Throughput.bytesPerSecond(789), 881.2)) + .add(new TimelineEntry(ofEpochSecond(134), Throughput.bytesPerSecond(32), 824.3)) + .add(new TimelineEntry(ofEpochSecond(135), Throughput.bytesPerSecond(893), 789.8)) + .add(new TimelineEntry(ofEpochSecond(136), Throughput.bytesPerSecond(1093), 759.9)) + .add(new TimelineEntry(ofEpochSecond(137), Throughput.bytesPerSecond(1218), 831.8)) + .add(new TimelineEntry(ofEpochSecond(138), Throughput.bytesPerSecond(159), 732.4)) + .add(new TimelineEntry(ofEpochSecond(139), Throughput.bytesPerSecond(407), 651.2)) + .add(new TimelineEntry(ofEpochSecond(140), Throughput.bytesPerSecond(615), 660.8)) + .add(new TimelineEntry(ofEpochSecond(141), Throughput.bytesPerSecond(1392), 766.3)) + .add(new TimelineEntry(ofEpochSecond(142), Throughput.bytesPerSecond(1431), 802.9)) + .add(new TimelineEntry(ofEpochSecond(143), Throughput.bytesPerSecond(270), 751.)) + .add(new TimelineEntry(ofEpochSecond(144), Throughput.bytesPerSecond(300), 777.8)) + .add(new TimelineEntry(ofEpochSecond(145), Throughput.bytesPerSecond(1402), 828.7)) + .add(new TimelineEntry(ofEpochSecond(146), Throughput.bytesPerSecond(308), 750.2)) + .add(new TimelineEntry(ofEpochSecond(147), Throughput.bytesPerSecond(125), 640.9)) + .add(new TimelineEntry(ofEpochSecond(148), Throughput.bytesPerSecond(467), 671.7)) + .add(new TimelineEntry(ofEpochSecond(149), Throughput.bytesPerSecond(1339), 764.9)) + .add(new TimelineEntry(ofEpochSecond(150), Throughput.bytesPerSecond(1146), 818.)) + .add(new TimelineEntry(ofEpochSecond(151), Throughput.bytesPerSecond(765), 755.3)) + .add(new TimelineEntry(ofEpochSecond(152), Throughput.bytesPerSecond(649), 677.1)) + .add(new TimelineEntry(ofEpochSecond(153), Throughput.bytesPerSecond(1318), 781.9)) + .add(new TimelineEntry(ofEpochSecond(154), Throughput.bytesPerSecond(199), 771.8)) + .add(new TimelineEntry(ofEpochSecond(155), Throughput.bytesPerSecond(923), 723.9)) + .add(new TimelineEntry(ofEpochSecond(156), Throughput.bytesPerSecond(430), 736.1)) + .add(new TimelineEntry(ofEpochSecond(157), Throughput.bytesPerSecond(158), 739.4)) + .add(new TimelineEntry(ofEpochSecond(158), Throughput.bytesPerSecond(187), 711.4)) + .add(new TimelineEntry(ofEpochSecond(159), Throughput.bytesPerSecond(442), 621.7)) + .add(new TimelineEntry(ofEpochSecond(160), Throughput.bytesPerSecond(82), 515.3)) + .add(new TimelineEntry(ofEpochSecond(161), Throughput.bytesPerSecond(951), 533.9)) + .add(new TimelineEntry(ofEpochSecond(162), Throughput.bytesPerSecond(976), 566.6)) + .add(new TimelineEntry(ofEpochSecond(163), Throughput.bytesPerSecond(1371), 571.9)) + .add(new TimelineEntry(ofEpochSecond(164), Throughput.bytesPerSecond(547), 606.7)) + .add(new TimelineEntry(ofEpochSecond(165), Throughput.bytesPerSecond(370), 551.4)) + .add(new TimelineEntry(ofEpochSecond(166), Throughput.bytesPerSecond(247), 533.1)) + .add(new TimelineEntry(ofEpochSecond(167), Throughput.bytesPerSecond(660), 583.3)) + .add(new TimelineEntry(ofEpochSecond(168), Throughput.bytesPerSecond(1222), 686.8)) + .add(new TimelineEntry(ofEpochSecond(169), Throughput.bytesPerSecond(130), 655.6)) + .add(new TimelineEntry(ofEpochSecond(170), Throughput.bytesPerSecond(512), 698.6)) + .add(new TimelineEntry(ofEpochSecond(171), Throughput.bytesPerSecond(873), 690.8)) + .add(new TimelineEntry(ofEpochSecond(172), Throughput.bytesPerSecond(18), 595.)) + .add(new TimelineEntry(ofEpochSecond(173), Throughput.bytesPerSecond(817), 539.6)) + .add(new TimelineEntry(ofEpochSecond(174), Throughput.bytesPerSecond(1090), 593.9)) + .add(new TimelineEntry(ofEpochSecond(175), Throughput.bytesPerSecond(1201), 677.)) + .add(new TimelineEntry(ofEpochSecond(176), Throughput.bytesPerSecond(1046), 756.9)) + .add(new TimelineEntry(ofEpochSecond(177), Throughput.bytesPerSecond(1075), 798.4)) + .add(new TimelineEntry(ofEpochSecond(178), Throughput.bytesPerSecond(679), 744.1)) + .add(new TimelineEntry(ofEpochSecond(179), Throughput.bytesPerSecond(1043), 835.4)) + .add(new TimelineEntry(ofEpochSecond(180), Throughput.bytesPerSecond(1206), 904.8)) + .add(new TimelineEntry(ofEpochSecond(181), Throughput.bytesPerSecond(701), 887.6)) + .add(new TimelineEntry(ofEpochSecond(182), Throughput.bytesPerSecond(849), 970.7)) + .add(new TimelineEntry(ofEpochSecond(183), Throughput.bytesPerSecond(457), 934.7)) + .add(new TimelineEntry(ofEpochSecond(184), Throughput.bytesPerSecond(400), 865.7)) + .add(new TimelineEntry(ofEpochSecond(185), Throughput.bytesPerSecond(1157), 861.3)) + .add(new TimelineEntry(ofEpochSecond(186), Throughput.bytesPerSecond(235), 780.2)) + .add(new TimelineEntry(ofEpochSecond(187), Throughput.bytesPerSecond(525), 725.2)) + .add(new TimelineEntry(ofEpochSecond(188), Throughput.bytesPerSecond(1415), 798.8)) + .add(new TimelineEntry(ofEpochSecond(189), Throughput.bytesPerSecond(796), 774.1)) + .add(new TimelineEntry(ofEpochSecond(190), Throughput.bytesPerSecond(428), 696.3)) + .add(new TimelineEntry(ofEpochSecond(191), Throughput.bytesPerSecond(417), 667.9)) + .add(new TimelineEntry(ofEpochSecond(192), Throughput.bytesPerSecond(436), 626.6)) + .add(new TimelineEntry(ofEpochSecond(193), Throughput.bytesPerSecond(781), 659.)) + .add(new TimelineEntry(ofEpochSecond(194), Throughput.bytesPerSecond(967), 715.7)) + .add(new TimelineEntry(ofEpochSecond(195), Throughput.bytesPerSecond(398), 639.8)) + .add(new TimelineEntry(ofEpochSecond(196), Throughput.bytesPerSecond(501), 666.4)) + .add(new TimelineEntry(ofEpochSecond(197), Throughput.bytesPerSecond(691), 683.)) + .add(new TimelineEntry(ofEpochSecond(198), Throughput.bytesPerSecond(1492), 690.7)) + .add(new TimelineEntry(ofEpochSecond(199), Throughput.bytesPerSecond(1493), 760.4)) + .add(new TimelineEntry(ofEpochSecond(200), Throughput.bytesPerSecond(5), 718.1)) + .add(new TimelineEntry(ofEpochSecond(201), Throughput.bytesPerSecond(679), 744.3)) + .add(new TimelineEntry(ofEpochSecond(202), Throughput.bytesPerSecond(1027), 803.4)) + .add(new TimelineEntry(ofEpochSecond(203), Throughput.bytesPerSecond(170), 742.3)) + .add(new TimelineEntry(ofEpochSecond(204), Throughput.bytesPerSecond(261), 671.7)) + .add(new TimelineEntry(ofEpochSecond(205), Throughput.bytesPerSecond(309), 662.8)) + .add(new TimelineEntry(ofEpochSecond(206), Throughput.bytesPerSecond(1483), 761.)) + .add(new TimelineEntry(ofEpochSecond(207), Throughput.bytesPerSecond(1154), 807.3)) + .add(new TimelineEntry(ofEpochSecond(208), Throughput.bytesPerSecond(857), 743.8)) + .add(new TimelineEntry(ofEpochSecond(209), Throughput.bytesPerSecond(792), 673.7)) + .add(new TimelineEntry(ofEpochSecond(210), Throughput.bytesPerSecond(819), 755.1)) + .add(new TimelineEntry(ofEpochSecond(211), Throughput.bytesPerSecond(763), 763.5)) + .add(new TimelineEntry(ofEpochSecond(212), Throughput.bytesPerSecond(386), 699.4)) + .add(new TimelineEntry(ofEpochSecond(213), Throughput.bytesPerSecond(789), 761.3)) + .add(new TimelineEntry(ofEpochSecond(214), Throughput.bytesPerSecond(1432), 878.4)) + .add(new TimelineEntry(ofEpochSecond(215), Throughput.bytesPerSecond(205), 868.)) + .add(new TimelineEntry(ofEpochSecond(216), Throughput.bytesPerSecond(905), 810.2)) + .add(new TimelineEntry(ofEpochSecond(217), Throughput.bytesPerSecond(1290), 823.8)) + .add(new TimelineEntry(ofEpochSecond(218), Throughput.bytesPerSecond(639), 802.)) + .add(new TimelineEntry(ofEpochSecond(219), Throughput.bytesPerSecond(1246), 847.4)) + .build()); +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputSinkTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputSinkTest.java new file mode 100644 index 0000000000..a7840f45c7 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputSinkTest.java @@ -0,0 +1,262 @@ +/* + * 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 static com.google.cloud.storage.TestUtils.assertAll; +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.storage.ThroughputSink.Record; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Assert; +import org.junit.Test; + +public final class ThroughputSinkTest { + + @Test + public void tee_record() { + AtomicReference r1 = new AtomicReference<>(null); + AtomicReference r2 = new AtomicReference<>(null); + ThroughputSink test = + ThroughputSink.tee( + new AbstractThroughputSink() { + @Override + public void recordThroughput(Record r) { + r1.compareAndSet(null, r); + } + }, + new AbstractThroughputSink() { + @Override + public void recordThroughput(Record r) { + r2.compareAndSet(null, r); + } + }); + + Record expected = Record.of(10, Instant.EPOCH, Instant.ofEpochSecond(1), false); + test.recordThroughput(expected); + + assertThat(r1.get()).isEqualTo(expected); + assertThat(r2.get()).isEqualTo(expected); + } + + @Test + public void tee_decorate() throws Exception { + AtomicReference b1 = new AtomicReference<>(null); + AtomicReference b2 = new AtomicReference<>(null); + AtomicReference b3 = new AtomicReference<>(null); + ThroughputSink test = + ThroughputSink.tee( + new AbstractThroughputSink() { + @Override + public WritableByteChannel decorate(WritableByteChannel wbc) { + return new WritableByteChannel() { + @Override + public int write(ByteBuffer src) throws IOException { + b1.compareAndSet(null, src.duplicate()); + return wbc.write(src); + } + + @Override + public boolean isOpen() { + return wbc.isOpen(); + } + + @Override + public void close() throws IOException { + wbc.close(); + } + }; + } + }, + new AbstractThroughputSink() { + @Override + public WritableByteChannel decorate(WritableByteChannel wbc) { + return new WritableByteChannel() { + @Override + public int write(ByteBuffer src) throws IOException { + ByteBuffer duplicate = src.duplicate(); + duplicate.position(src.limit()); + b2.compareAndSet(null, duplicate); + return wbc.write(src); + } + + @Override + public boolean isOpen() { + return wbc.isOpen(); + } + + @Override + public void close() throws IOException { + wbc.close(); + } + }; + } + }); + + AtomicBoolean callIsOpen = new AtomicBoolean(false); + AtomicBoolean callClose = new AtomicBoolean(false); + WritableByteChannel anon = + new WritableByteChannel() { + @Override + public int write(ByteBuffer src) { + int remaining = src.remaining(); + src.position(src.limit()); + b3.compareAndSet(null, src); + return remaining; + } + + @Override + public boolean isOpen() { + callIsOpen.compareAndSet(false, true); + return true; + } + + @Override + public void close() { + callClose.compareAndSet(false, true); + } + }; + + byte[] bytes = DataGenerator.base64Characters().genBytes(16); + + ByteBuffer expected1 = ByteBuffer.wrap(bytes); + ByteBuffer expected2 = ByteBuffer.wrap(bytes); + expected2.position(16); + + ByteBuffer buf = ByteBuffer.wrap(bytes); + try (WritableByteChannel decorated = test.decorate(anon)) { + if (decorated.isOpen()) { + decorated.write(buf); + } + } + + assertAll( + () -> assertThat(b1.get()).isEqualTo(expected1), + () -> assertThat(b2.get()).isEqualTo(expected2), + () -> assertThat(b3.get()).isSameInstanceAs(buf), + () -> assertThat(b3.get().hasRemaining()).isFalse(), + () -> assertThat(callIsOpen.get()).isTrue(), + () -> assertThat(callClose.get()).isTrue()); + } + + @Test + public void computeThroughput_noError() throws IOException { + // create a clock that will start at Epoch UTC, and will tick in one second increments + TestClock clock = TestClock.tickBy(Instant.EPOCH, Duration.ofSeconds(1)); + AtomicReference actual = new AtomicReference<>(null); + + ThroughputSink.computeThroughput( + clock, + new AbstractThroughputSink() { + @Override + public void recordThroughput(Record r) { + actual.compareAndSet(null, r); + } + }, + 300, + () -> {}); + + Record expected = Record.of(300, Instant.EPOCH, Instant.ofEpochSecond(1), false); + assertThat(actual.get()).isEqualTo(expected); + } + + @Test + public void computeThroughput_ioError() { + // create a clock that will start at Epoch UTC, and will tick in one second increments + TestClock clock = TestClock.tickBy(Instant.EPOCH, Duration.ofSeconds(1)); + AtomicReference actual = new AtomicReference<>(null); + + IOException ioException = + Assert.assertThrows( + IOException.class, + () -> + ThroughputSink.computeThroughput( + clock, + new AbstractThroughputSink() { + @Override + public void recordThroughput(Record r) { + actual.compareAndSet(null, r); + } + }, + 300, + () -> { + throw new IOException("kablamo!"); + })); + + Record expected = Record.of(300, Instant.EPOCH, Instant.ofEpochSecond(1), true); + assertThat(actual.get()).isEqualTo(expected); + + assertThat(ioException).hasMessageThat().isEqualTo("kablamo!"); + } + + @Test + public void windowed() throws IOException { + // create a clock that will start at Epoch UTC, and will tick in one second increments + TestClock clock = TestClock.tickBy(Instant.EPOCH, Duration.ofSeconds(1)); + + AtomicReference b3 = new AtomicReference<>(null); + WritableByteChannel anon = + new WritableByteChannel() { + @Override + public int write(ByteBuffer src) { + int remaining = src.remaining(); + src.position(src.limit()); + b3.compareAndSet(null, src); + return remaining; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void close() {} + }; + + Duration windowDuration = Duration.ofMinutes(1); + ThroughputMovingWindow window = ThroughputMovingWindow.of(windowDuration); + ThroughputSink sink = ThroughputSink.windowed(window, clock); + + int numBytes = 120; + ByteBuffer buf = DataGenerator.base64Characters().genByteBuffer(numBytes); + try (WritableByteChannel decorated = sink.decorate(anon)) { + decorated.write(buf); + } + + Throughput avg = window.avg(clock.instant()); + + assertThat(avg).isEqualTo(Throughput.of(numBytes, windowDuration)); + assertThat(avg).isEqualTo(Throughput.bytesPerSecond(2)); + } + + private abstract static class AbstractThroughputSink implements ThroughputSink { + + @Override + public void recordThroughput(Record r) {} + + @Override + public WritableByteChannel decorate(WritableByteChannel wbc) { + return null; + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputTest.java new file mode 100644 index 0000000000..e44d4c1dd5 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ThroughputTest.java @@ -0,0 +1,35 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static java.time.Duration.ofSeconds; + +import org.junit.Test; + +public final class ThroughputTest { + + @Test + public void a() { + assertThat(Throughput.bytesPerSecond(1).toBps()).isEqualTo(1); + } + + @Test + public void b() { + assertThat(Throughput.of(10, ofSeconds(10)).toBps()).isEqualTo(1); + } +} From 4dad2d5c3a81eda7190ad4f95316471e7fa30f66 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Mon, 31 Jul 2023 19:21:56 -0400 Subject: [PATCH 09/17] feat: implement BufferToDiskThenUpload BlobWriteSessionConfig (#2139) There are scenarios in which disk space is more plentiful than memory space. This new BlobWriteSessionConfig allows augmenting an instance of storage to prefer buffering to disk rather than keeping things in memory. Once the file on disk is closed, the entire file will then be uploaded to GCS. --- .../storage/BlobWriteSessionConfigs.java | 55 ++++ .../cloud/storage/BufferToDiskThenUpload.java | 236 ++++++++++++++++++ .../google/cloud/storage/GrpcStorageImpl.java | 3 +- .../google/cloud/storage/RecoveryFile.java | 100 ++++++++ .../cloud/storage/RecoveryFileManager.java | 106 ++++++++ .../google/cloud/storage/StorageInternal.java | 5 +- .../google/cloud/storage/ThroughputSink.java | 42 ++++ .../storage/BufferToDiskThenUploadTest.java | 87 +++++++ .../storage/RecoveryFileManagerTest.java | 146 +++++++++++ .../cloud/storage/SerializationTest.java | 70 +++--- .../storage/it/ITBlobWriteSessionTest.java | 12 + 11 files changed, 830 insertions(+), 32 deletions(-) create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFile.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFileManager.java create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/BufferToDiskThenUploadTest.java create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/RecoveryFileManagerTest.java diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java index cc5e691e6b..efaf569a87 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfigs.java @@ -19,6 +19,11 @@ import com.google.api.core.BetaApi; import com.google.cloud.storage.GrpcStorageOptions.GrpcStorageDefaults; import com.google.cloud.storage.Storage.BlobWriteOption; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; /** * Factory class to select and construct {@link BlobWriteSessionConfig}s. @@ -46,4 +51,54 @@ private BlobWriteSessionConfigs() {} public static DefaultBlobWriteSessionConfig getDefault() { return new DefaultBlobWriteSessionConfig(ByteSizeConstants._16MiB); } + + /** + * Create a new {@link BlobWriteSessionConfig} which will first buffer the content of the object + * to a temporary file under {@code java.io.tmpdir}. + * + *

Once the file on disk is closed, the entire file will then be uploaded to Google Cloud + * Storage. + * + * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...) + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + */ + @BetaApi + public static BlobWriteSessionConfig bufferToTempDirThenUpload() throws IOException { + return bufferToDiskThenUpload( + Paths.get(System.getProperty("java.io.tmpdir"), "google-cloud-storage")); + } + + /** + * Create a new {@link BlobWriteSessionConfig} which will first buffer the content of the object + * to a temporary file under the specified {@code path}. + * + *

Once the file on disk is closed, the entire file will then be uploaded to Google Cloud + * Storage. + * + * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...) + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + */ + @BetaApi + public static BufferToDiskThenUpload bufferToDiskThenUpload(Path path) throws IOException { + return bufferToDiskThenUpload(ImmutableList.of(path)); + } + + /** + * Create a new {@link BlobWriteSessionConfig} which will first buffer the content of the object + * to a temporary file under one of the specified {@code paths}. + * + *

Once the file on disk is closed, the entire file will then be uploaded to Google Cloud + * Storage. + * + *

The specifics of how the work is spread across multiple paths is undefined and subject to + * change. + * + * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...) + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + */ + @BetaApi + public static BufferToDiskThenUpload bufferToDiskThenUpload(Collection paths) + throws IOException { + return new BufferToDiskThenUpload(ImmutableList.copyOf(paths), false); + } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java new file mode 100644 index 0000000000..fb20747a8c --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java @@ -0,0 +1,236 @@ +/* + * 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.ApiFutures; +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.storage.Conversions.Decoder; +import com.google.cloud.storage.Storage.BlobWriteOption; +import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt; +import com.google.cloud.storage.UnifiedOpts.Opts; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.storage.v2.WriteObjectResponse; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Collector; +import javax.annotation.concurrent.Immutable; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * There are scenarios in which disk space is more plentiful than memory space. This new {@link + * BlobWriteSessionConfig} allows augmenting an instance of storage to produce {@link + * BlobWriteSession}s which will buffer to disk rather than holding things in memory. + * + *

Once the file on disk is closed, the entire file will then be uploaded to GCS. + * + * @see Storage#blobWriteSession(BlobInfo, BlobWriteOption...) + * @see GrpcStorageOptions.Builder#setBlobWriteSessionConfig(BlobWriteSessionConfig) + * @see BlobWriteSessionConfigs#bufferToDiskThenUpload(Path) + * @see BlobWriteSessionConfigs#bufferToDiskThenUpload(Collection) + */ +@Immutable +@BetaApi +public final class BufferToDiskThenUpload extends BlobWriteSessionConfig { + private static final long serialVersionUID = 9059242302276891867L; + + /** + * non-final because of {@link java.io.Serializable}, however this field is effectively final as + * it is immutable and there is not reference mutator method. + */ + @MonotonicNonNull private transient ImmutableList paths; + + private final boolean includeLoggingSink; + + /** Used for {@link java.io.Serializable} */ + @MonotonicNonNull private volatile ArrayList absolutePaths; + + @InternalApi + BufferToDiskThenUpload(ImmutableList paths, boolean includeLoggingSink) throws IOException { + this.paths = paths; + this.includeLoggingSink = includeLoggingSink; + } + + @VisibleForTesting + @InternalApi + BufferToDiskThenUpload withIncludeLoggingSink() throws IOException { + return new BufferToDiskThenUpload(paths, true); + } + + @InternalApi + @Override + WriterFactory createFactory(Clock clock) throws IOException { + Duration window = Duration.ofMinutes(10); + RecoveryFileManager recoveryFileManager = + RecoveryFileManager.of(paths, getRecoverVolumeSinkFactory(clock, window)); + ThroughputSink gcs = ThroughputSink.windowed(ThroughputMovingWindow.of(window), clock); + gcs = includeLoggingSink ? ThroughputSink.tee(ThroughputSink.logged("gcs", clock), gcs) : gcs; + return new Factory(recoveryFileManager, clock, gcs); + } + + private RecoveryFileManager.RecoverVolumeSinkFactory getRecoverVolumeSinkFactory( + Clock clock, Duration window) { + return path -> { + ThroughputSink windowed = ThroughputSink.windowed(ThroughputMovingWindow.of(window), clock); + if (includeLoggingSink) { + return ThroughputSink.tee( + ThroughputSink.logged(path.toAbsolutePath().toString(), clock), windowed); + } else { + return windowed; + } + }; + } + + private void writeObject(ObjectOutputStream out) throws IOException { + if (absolutePaths == null) { + synchronized (this) { + if (absolutePaths == null) { + absolutePaths = + paths.stream() + .map(Path::toAbsolutePath) + .map(Path::toString) + .collect( + Collector.of( + ArrayList::new, + ArrayList::add, + (left, right) -> { + left.addAll(right); + return left; + })); + } + } + } + out.defaultWriteObject(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + this.paths = absolutePaths.stream().map(Paths::get).collect(ImmutableList.toImmutableList()); + } + + private static final class Factory implements WriterFactory { + + private final RecoveryFileManager recoveryFileManager; + private final Clock clock; + private final ThroughputSink gcs; + + private Factory(RecoveryFileManager recoveryFileManager, Clock clock, ThroughputSink gcs) { + this.recoveryFileManager = recoveryFileManager; + this.clock = clock; + this.gcs = gcs; + } + + @InternalApi + @Override + public WritableByteChannelSession writeSession( + StorageInternal storage, + BlobInfo info, + Opts opts, + Decoder d) { + return new Factory.WriteToFileThenUpload( + storage, info, opts, recoveryFileManager.newRecoveryFile(info)); + } + + private final class WriteToFileThenUpload + implements WritableByteChannelSession { + + private final StorageInternal storage; + private final BlobInfo info; + private final Opts opts; + private final RecoveryFile rf; + private final SettableApiFuture result; + + private WriteToFileThenUpload( + StorageInternal storage, BlobInfo info, Opts opts, RecoveryFile rf) { + this.info = info; + this.opts = opts; + this.rf = rf; + this.storage = storage; + this.result = SettableApiFuture.create(); + } + + @Override + public ApiFuture openAsync() { + try { + ApiFuture f = ApiFutures.immediateFuture(rf.writer()); + return ApiFutures.transform( + f, Factory.WriteToFileThenUpload.Flusher::new, MoreExecutors.directExecutor()); + } catch (IOException e) { + throw StorageException.coalesce(e); + } + } + + @Override + public ApiFuture getResult() { + return result; + } + + private final class Flusher implements WritableByteChannel { + + private final WritableByteChannel delegate; + + private Flusher(WritableByteChannel delegate) { + this.delegate = delegate; + } + + @Override + public int write(ByteBuffer src) throws IOException { + return delegate.write(src); + } + + @Override + public boolean isOpen() { + return delegate.isOpen(); + } + + @Override + public void close() throws IOException { + delegate.close(); + try (RecoveryFile rf = Factory.WriteToFileThenUpload.this.rf) { + Path path = rf.getPath(); + long size = Files.size(path); + ThroughputSink.computeThroughput( + clock, + gcs, + size, + () -> { + BlobInfo blob = storage.internalCreateFrom(path, info, opts); + result.set(blob); + }); + } catch (StorageException | IOException e) { + result.setException(e); + throw e; + } + } + } + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index fdd67a7eb7..fad1f66e3a 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -152,7 +152,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; @BetaApi -final class GrpcStorageImpl extends BaseService implements StorageInternal { +final class GrpcStorageImpl extends BaseService + implements Storage, StorageInternal { private static final byte[] ZERO_BYTES = new byte[0]; private static final Set READ_OPS = ImmutableSet.of(StandardOpenOption.READ); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFile.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFile.java new file mode 100644 index 0000000000..d399ea9300 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFile.java @@ -0,0 +1,100 @@ +/* + * 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.common.base.MoreObjects; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Set; + +/** + * When uploading to GCS, there are times when memory buffers are not preferable. This class + * encapsulates the logic and lifecycle for a file written to local disk which can be used for + * upload recovery in the case an upload is interrupted. + */ +final class RecoveryFile implements AutoCloseable { + private static final Set writeOps = + ImmutableSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE); + private static final Set readOps = ImmutableSet.of(StandardOpenOption.READ); + + private final Path path; + private final ThroughputSink throughputSink; + private final Runnable onCloseCallback; + + RecoveryFile(Path path, ThroughputSink throughputSink, Runnable onCloseCallback) { + this.path = path; + this.throughputSink = throughputSink; + this.onCloseCallback = onCloseCallback; + } + + public Path getPath() { + return path; + } + + public Path touch() throws IOException { + return Files.createFile(path); + } + + public SeekableByteChannel reader() throws IOException { + return Files.newByteChannel(path, readOps); + } + + public WritableByteChannel writer() throws IOException { + return throughputSink.decorate(Files.newByteChannel(path, writeOps)); + } + + @Override + public void close() throws IOException { + Files.delete(path); + onCloseCallback.run(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("path", path) + .add("throughputSink", throughputSink) + .add("onCloseCallback", onCloseCallback) + .toString(); + } + + Unsafe unsafe() { + return new Unsafe(); + } + + final class Unsafe { + public Path touch() throws UnsafeIOException { + try { + return RecoveryFile.this.touch(); + } catch (IOException e) { + throw new UnsafeIOException(e); + } + } + } + + static final class UnsafeIOException extends RuntimeException { + private UnsafeIOException(IOException cause) { + super(cause); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFileManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFileManager.java new file mode 100644 index 0000000000..25303b5fa4 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFileManager.java @@ -0,0 +1,106 @@ +/* + * 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 static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class RecoveryFileManager { + + private final ImmutableList volumes; + /** Keep track of active info and file */ + private final Map files; + + /** + * Round-robin assign recovery files to the configured volumes. Use this index to keep track of + * which volume to assign to next. + */ + private int nextVolumeIndex; + + private RecoveryFileManager(List volumes) { + this.volumes = ImmutableList.copyOf(volumes); + this.files = Collections.synchronizedMap(new HashMap<>()); + this.nextVolumeIndex = 0; + } + + public RecoveryFile newRecoveryFile(BlobInfo info) { + int i = getNextVolumeIndex(); + RecoveryVolume v = volumes.get(i); + int hashCode = info.hashCode(); + String fileName = Base64.getUrlEncoder().encodeToString(Ints.toByteArray(hashCode)); + Path path = v.basePath.resolve(fileName); + RecoveryFile recoveryFile = new RecoveryFile(path, v.sink, () -> files.remove(info)); + files.put(info, recoveryFile); + return recoveryFile; + } + + private synchronized int getNextVolumeIndex() { + return nextVolumeIndex = (nextVolumeIndex + 1) % volumes.size(); + } + + static RecoveryFileManager of(List volumes) throws IOException { + return of(volumes, p -> ThroughputSink.nullSink()); + } + + static RecoveryFileManager of(List volumes, RecoverVolumeSinkFactory factory) + throws IOException { + checkArgument(!volumes.isEmpty(), "At least one volume must be specified"); + checkArgument( + volumes.stream().allMatch(p -> !Files.exists(p) || Files.isDirectory(p)), + "All provided volumes must either:\n1. Not yet exists\n2. Be directories"); + + for (Path v : volumes) { + if (!Files.exists(v)) { + Files.createDirectories(v); + } + } + ImmutableList recoveryVolumes = + volumes.stream() + .map(p -> RecoveryVolume.of(p, factory.apply(p))) + .collect(ImmutableList.toImmutableList()); + return new RecoveryFileManager(recoveryVolumes); + } + + @FunctionalInterface + interface RecoverVolumeSinkFactory { + ThroughputSink apply(Path p); + } + + static final class RecoveryVolume { + private final Path basePath; + private final ThroughputSink sink; + + private RecoveryVolume(Path basePath, ThroughputSink sink) { + this.basePath = basePath; + this.sink = sink; + } + + public static RecoveryVolume of(Path basePath, ThroughputSink sink) { + return new RecoveryVolume(basePath, sink); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java index deb8a05043..53fdc4e9a6 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageInternal.java @@ -21,9 +21,10 @@ import java.io.IOException; import java.nio.file.Path; -interface StorageInternal extends Storage { +interface StorageInternal { - Blob internalCreateFrom(Path path, BlobInfo info, Opts opts) throws IOException; + BlobInfo internalCreateFrom(Path path, BlobInfo info, Opts opts) + throws IOException; StorageWriteChannel internalWriter(BlobInfo info, Opts opts); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java index 5ef6e37d10..776629cf34 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ThroughputSink.java @@ -69,6 +69,10 @@ static ThroughputSink tee(ThroughputSink a, ThroughputSink b) { return new TeeThroughputSink(a, b); } + static ThroughputSink nullSink() { + return NullThroughputSink.INSTANCE; + } + final class Record { private final long numBytes; private final Instant begin; @@ -166,6 +170,11 @@ public void recordThroughput(Record r) { public WritableByteChannel decorate(WritableByteChannel wbc) { return new ThroughputRecordingWritableByteChannel(wbc, this, clock); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("prefix", prefix).add("clock", clock).toString(); + } } final class ThroughputRecordingWritableByteChannel implements WritableByteChannel { @@ -206,6 +215,15 @@ public boolean isOpen() { public void close() throws IOException { delegate.close(); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("delegate", delegate) + .add("sink", sink) + .add("clock", clock) + .toString(); + } } final class TeeThroughputSink implements ThroughputSink { @@ -227,6 +245,11 @@ public void recordThroughput(Record r) { public WritableByteChannel decorate(WritableByteChannel wbc) { return b.decorate(a.decorate(wbc)); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("a", a).add("b", b).toString(); + } } final class ThroughputMovingWindowThroughputSink implements ThroughputSink { @@ -247,5 +270,24 @@ public synchronized void recordThroughput(Record r) { public WritableByteChannel decorate(WritableByteChannel wbc) { return new ThroughputRecordingWritableByteChannel(wbc, this, clock); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("w", w).add("clock", clock).toString(); + } + } + + final class NullThroughputSink implements ThroughputSink { + private static final NullThroughputSink INSTANCE = new NullThroughputSink(); + + private NullThroughputSink() {} + + @Override + public void recordThroughput(Record r) {} + + @Override + public WritableByteChannel decorate(WritableByteChannel wbc) { + return wbc; + } } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BufferToDiskThenUploadTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BufferToDiskThenUploadTest.java new file mode 100644 index 0000000000..522be99b2c --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BufferToDiskThenUploadTest.java @@ -0,0 +1,87 @@ +/* + * 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 static com.google.cloud.storage.TestUtils.xxd; +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory; +import com.google.cloud.storage.Conversions.Decoder; +import com.google.cloud.storage.UnifiedOpts.ObjectTargetOpt; +import com.google.cloud.storage.UnifiedOpts.Opts; +import com.google.storage.v2.WriteObjectResponse; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; + +public final class BufferToDiskThenUploadTest { + + private static final Decoder + WRITE_OBJECT_RESPONSE_BLOB_INFO_DECODER = + Conversions.grpc().blobInfo().compose(WriteObjectResponse::getResource); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TestName testName = new TestName(); + + @Test + public void happyPath() throws IOException { + Path tempDir = temporaryFolder.newFolder(testName.getMethodName()).toPath(); + + BufferToDiskThenUpload btdtu = + BlobWriteSessionConfigs.bufferToDiskThenUpload(tempDir).withIncludeLoggingSink(); + TestClock clock = TestClock.tickBy(Instant.EPOCH, Duration.ofSeconds(1)); + WriterFactory factory = btdtu.createFactory(clock); + + BlobInfo blobInfo = BlobInfo.newBuilder("bucket", "object").build(); + AtomicReference actualBytes = new AtomicReference<>(null); + WritableByteChannelSession writeSession = + factory.writeSession( + new StorageInternal() { + @Override + public BlobInfo internalCreateFrom( + Path path, BlobInfo info, Opts opts) throws IOException { + byte[] actual = Files.readAllBytes(path); + actualBytes.compareAndSet(null, actual); + return info; + } + + @Override + public StorageWriteChannel internalWriter(BlobInfo info, Opts opts) { + return null; + } + }, + blobInfo, + Opts.empty(), + WRITE_OBJECT_RESPONSE_BLOB_INFO_DECODER); + + byte[] bytes = DataGenerator.base64Characters().genBytes(128); + try (WritableByteChannel open = writeSession.open()) { + open.write(ByteBuffer.wrap(bytes)); + } + String xxdActual = xxd(actualBytes.get()); + String xxdExpected = xxd(bytes); + assertThat(xxdActual).isEqualTo(xxdExpected); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/RecoveryFileManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/RecoveryFileManagerTest.java new file mode 100644 index 0000000000..abf68893bb --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/RecoveryFileManagerTest.java @@ -0,0 +1,146 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; + +public final class RecoveryFileManagerTest { + private static final int _128KiB = 128 * 1024; + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule public final TestName testName = new TestName(); + + private final TestClock clock = TestClock.tickBy(Instant.EPOCH, Duration.ofSeconds(1)); + + @Test + public void happyPath() throws IOException { + Path tempDir = temporaryFolder.newFolder(testName.getMethodName()).toPath(); + RecoveryFileManager rfm = + RecoveryFileManager.of( + ImmutableList.of(tempDir), + path -> ThroughputSink.logged(path.toAbsolutePath().toString(), clock)); + + BlobInfo info = BlobInfo.newBuilder("bucket", "object").build(); + try (RecoveryFile recoveryFile = rfm.newRecoveryFile(info)) { + + byte[] bytes = DataGenerator.base64Characters().genBytes(_128KiB); + try (WritableByteChannel writer = recoveryFile.writer()) { + writer.write(ByteBuffer.wrap(bytes)); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (SeekableByteChannel r = recoveryFile.reader(); + WritableByteChannel w = Channels.newChannel(baos)) { + long copy = ByteStreams.copy(r, w); + assertThat(copy).isEqualTo(_128KiB); + } + + assertThat(baos.toByteArray()).isEqualTo(bytes); + } + + try (Stream stream = Files.list(tempDir)) { + boolean b = stream.anyMatch(Objects::nonNull); + assertThat(b).isFalse(); + } + } + + @Test + public void argValidation_nonEmpty() { + IllegalArgumentException iae = + assertThrows( + IllegalArgumentException.class, () -> RecoveryFileManager.of(ImmutableList.of())); + + assertThat(iae).hasMessageThat().isNotEmpty(); + } + + @Test + public void argValidation_fileInsteadOfDirectory() throws IOException { + Path tempDir = temporaryFolder.newFile(testName.getMethodName()).toPath(); + + IllegalArgumentException iae = + assertThrows( + IllegalArgumentException.class, + () -> RecoveryFileManager.of(ImmutableList.of(tempDir))); + + assertThat(iae).hasMessageThat().isNotEmpty(); + } + + @Test + public void argValidation_directoryDoesNotExistIsCreated() throws IOException { + Path tempDir = temporaryFolder.newFolder(testName.getMethodName()).toPath(); + + Path subPathA = tempDir.resolve("sub/path/a"); + + assertThat(Files.exists(subPathA)).isFalse(); + RecoveryFileManager rfm = RecoveryFileManager.of(ImmutableList.of(subPathA)); + assertThat(Files.exists(subPathA)).isTrue(); + } + + @Test + public void fileAssignmentIsRoundRobin() throws IOException { + Path tempDir1 = temporaryFolder.newFolder(testName.getMethodName() + "1").toPath(); + Path tempDir2 = temporaryFolder.newFolder(testName.getMethodName() + "2").toPath(); + Path tempDir3 = temporaryFolder.newFolder(testName.getMethodName() + "3").toPath(); + RecoveryFileManager rfm = + RecoveryFileManager.of(ImmutableList.of(tempDir1, tempDir2, tempDir3)); + + BlobInfo info1 = BlobInfo.newBuilder("bucket", "object1").build(); + BlobInfo info2 = BlobInfo.newBuilder("bucket", "object2").build(); + BlobInfo info3 = BlobInfo.newBuilder("bucket", "object3").build(); + try (RecoveryFile recoveryFile1 = rfm.newRecoveryFile(info1); + RecoveryFile recoveryFile2 = rfm.newRecoveryFile(info2); + RecoveryFile recoveryFile3 = rfm.newRecoveryFile(info3)) { + + ImmutableSet paths = + Stream.of(recoveryFile1, recoveryFile2, recoveryFile3) + .map(rf -> rf.unsafe().touch()) + .map(Path::toAbsolutePath) + .collect(ImmutableSet.toImmutableSet()); + + ImmutableSet parentDirs = + Stream.of(recoveryFile1, recoveryFile2, recoveryFile3) + .map(RecoveryFile::getPath) + .map(Path::getParent) + .collect(ImmutableSet.toImmutableSet()); + + assertThat(paths).hasSize(3); + assertThat(parentDirs).isEqualTo(ImmutableSet.of(tempDir1, tempDir2, tempDir3)); + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java index 87b88a78b8..ae3a140f52 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java @@ -170,35 +170,47 @@ protected Serializable[] serializableObjects() { .add(UnifiedOpts.md5MatchExtractor()) .build(); - return new Serializable[] { - ACL_DOMAIN, - ACL_GROUP, - ACL_PROJECT_, - ACL_USER, - ACL_RAW, - ACL, - BLOB_INFO, - BLOB, - BUCKET_INFO, - BUCKET, - ORIGIN, - CORS, - PAGE_RESULT, - BLOB_LIST_OPTIONS, - BLOB_SOURCE_OPTIONS, - BLOB_TARGET_OPTIONS, - BUCKET_LIST_OPTIONS, - BUCKET_SOURCE_OPTIONS, - BUCKET_TARGET_OPTIONS, - STORAGE_EXCEPTION, - optionsDefault1, - optionsDefault2, - optionsHttp1, - optionsHttp2, - optionsGrpc1, - optionsGrpc2, - serializableOpts - }; + try { + GrpcStorageOptions grpcStorageOptionsBufferToTemp = + StorageOptions.grpc() + .setCredentials(NoCredentials.getInstance()) + .setProjectId("project1") + .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bufferToTempDirThenUpload()) + .build(); + + return new Serializable[] { + ACL_DOMAIN, + ACL_GROUP, + ACL_PROJECT_, + ACL_USER, + ACL_RAW, + ACL, + BLOB_INFO, + BLOB, + BUCKET_INFO, + BUCKET, + ORIGIN, + CORS, + PAGE_RESULT, + BLOB_LIST_OPTIONS, + BLOB_SOURCE_OPTIONS, + BLOB_TARGET_OPTIONS, + BUCKET_LIST_OPTIONS, + BUCKET_SOURCE_OPTIONS, + BUCKET_TARGET_OPTIONS, + STORAGE_EXCEPTION, + optionsDefault1, + optionsDefault2, + optionsHttp1, + optionsHttp2, + optionsGrpc1, + optionsGrpc2, + serializableOpts, + grpcStorageOptionsBufferToTemp + }; + } catch (IOException ioe) { + throw new AssertionError(ioe); + } } @Override diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java index 1bb24fb975..1113d0231a 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteSessionTest.java @@ -61,6 +61,18 @@ public void allDefaults() throws Exception { doTest(storage); } + @Test + public void bufferToTempDirThenUpload() throws Exception { + GrpcStorageOptions options = + ((GrpcStorageOptions) storage.getOptions()) + .toBuilder() + .setBlobWriteSessionConfig(BlobWriteSessionConfigs.bufferToTempDirThenUpload()) + .build(); + try (Storage s = options.getService()) { + doTest(s); + } + } + @Test public void overrideDefaultBufferSize() throws Exception { GrpcStorageOptions options = From cdf4e261991c5e645df8c1b4b517a4d112f8704b Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 2 Aug 2023 19:00:50 +0200 Subject: [PATCH 10/17] chore(deps): update dependency com.google.cloud:libraries-bom to v26.21.0 (#2144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update dependency com.google.cloud:libraries-bom to v26.21.0 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- README.md | 4 ++-- samples/native-image-sample/pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1aa0a611ae..560db1b690 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file: com.google.cloud libraries-bom - 26.19.0 + 26.21.0 pom import @@ -50,7 +50,7 @@ If you are using Maven without the BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies: ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.19.0') +implementation platform('com.google.cloud:libraries-bom:26.21.0') implementation 'com.google.cloud:google-cloud-storage' ``` diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml index d2ab7237c8..654259dcff 100644 --- a/samples/native-image-sample/pom.xml +++ b/samples/native-image-sample/pom.xml @@ -29,7 +29,7 @@ com.google.cloud libraries-bom - 26.19.0 + 26.21.0 pom import diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 22fdfdb195..e12e2124ff 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -31,7 +31,7 @@ com.google.cloud libraries-bom - 26.19.0 + 26.21.0 pom import From f00f84339c96101b3f80ea79f1d6e06848e54615 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:58:12 -0700 Subject: [PATCH 11/17] build(deps): bump certifi from 2023.5.7 to 2023.7.22 in /synthtool/gcp/templates/java_library/.kokoro (#1837) (#2143) build(deps): bump certifi Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Source-Link: https://github.com/googleapis/synthtool/commit/d85e1d678a829da6f2f5664392a6cd8e95ba8341 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-java:latest@sha256:3a95f1b9b1102865ca551b76be51d2bdb850900c4db2f6d79269e7af81ac8f84 Signed-off-by: dependabot[bot] Co-authored-by: Owl Bot Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/requirements.txt | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index d5500ef442..fa335912bd 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest - digest: sha256:46d2d262cd285c638656c8bde468011b723dc0c7ffd6a5ecc2650fe639c82e8f -# created: 2023-07-24T14:21:17.707234503Z + digest: sha256:3a95f1b9b1102865ca551b76be51d2bdb850900c4db2f6d79269e7af81ac8f84 +# created: 2023-07-27T18:37:44.251188775Z diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 32989051e7..a73256ab80 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -12,9 +12,9 @@ cachetools==5.3.1 \ --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \ --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b # via google-auth -certifi==2023.5.7 \ - --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ - --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 +certifi==2023.7.22 \ + --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ + --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 # via requests cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ @@ -485,6 +485,5 @@ zipp==3.16.1 \ # via importlib-metadata # WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # setuptools From 330e795040592e5df22d44fb5216ad7cf2448e81 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Wed, 2 Aug 2023 17:40:25 -0400 Subject: [PATCH 12/17] fix: update grpc default metadata projection to include acl same as json (#2150) --- .../google/cloud/storage/GrpcStorageImpl.java | 14 +- .../storage/it/ITBlobWriteChannelTest.java | 1 - .../ITDefaultProjectionCompatibilityTest.java | 166 ++++++++++++++++++ 3 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDefaultProjectionCompatibilityTest.java diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index fad1f66e3a..95ddad874f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -164,6 +164,11 @@ final class GrpcStorageImpl extends BaseService StandardOpenOption.TRUNCATE_EXISTING); private static final BucketSourceOption[] EMPTY_BUCKET_SOURCE_OPTIONS = new BucketSourceOption[0]; + private static final Opts ALL_BLOB_FIELDS = + Opts.from(UnifiedOpts.fields(ImmutableSet.copyOf(BlobField.values()))); + private static final Opts ALL_BUCKET_FIELDS = + Opts.from(UnifiedOpts.fields(ImmutableSet.copyOf(BucketField.values()))); + final StorageClient storageClient; final WriterFactory writerFactory; final GrpcConversions codecs; @@ -420,7 +425,7 @@ public Blob get(BlobId blob) { @Override public Page list(BucketListOption... options) { - Opts opts = Opts.unwrap(options).prepend(defaultOpts); + Opts opts = Opts.unwrap(options).prepend(defaultOpts).prepend(ALL_BUCKET_FIELDS); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); ListBucketsRequest request = @@ -448,7 +453,7 @@ public Page list(BucketListOption... options) { @Override public Page list(String bucket, BlobListOption... options) { - Opts opts = Opts.unwrap(options).prepend(defaultOpts); + Opts opts = Opts.unwrap(options).prepend(defaultOpts).prepend(ALL_BLOB_FIELDS); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); ListObjectsRequest.Builder builder = @@ -1958,7 +1963,8 @@ private Object updateObject(UpdateObjectRequest req) { @Nullable private Blob internalBlobGet(BlobId blob, Opts unwrap) { - Opts opts = unwrap.resolveFrom(blob).prepend(defaultOpts); + Opts opts = + unwrap.resolveFrom(blob).prepend(defaultOpts).prepend(ALL_BLOB_FIELDS); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); GetObjectRequest.Builder builder = @@ -1983,7 +1989,7 @@ private Blob internalBlobGet(BlobId blob, Opts unwrap) { @Nullable private Bucket internalBucketGet(String bucket, Opts unwrap) { - Opts opts = unwrap.prepend(defaultOpts); + Opts opts = unwrap.prepend(defaultOpts).prepend(ALL_BUCKET_FIELDS); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); GetBucketRequest.Builder builder = diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteChannelTest.java index 338f2d9be7..63aac4f56c 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteChannelTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobWriteChannelTest.java @@ -145,7 +145,6 @@ public void testWriteChannelExistingBlob() throws IOException { @Test public void changeChunkSizeAfterWrite() throws IOException { BlobInfo info = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build(); - System.out.println("info = " + info); int _512KiB = 512 * 1024; byte[] bytes = DataGenerator.base64Characters().genBytes(_512KiB + 13); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDefaultProjectionCompatibilityTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDefaultProjectionCompatibilityTest.java new file mode 100644 index 0000000000..32d0f31d9c --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITDefaultProjectionCompatibilityTest.java @@ -0,0 +1,166 @@ +/* + * 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.it; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.storage.Acl; +import com.google.cloud.storage.Acl.Entity; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.Storage.BlobListOption; +import com.google.cloud.storage.Storage.BucketListOption; +import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.it.runner.StorageITRunner; +import com.google.cloud.storage.it.runner.annotations.Backend; +import com.google.cloud.storage.it.runner.annotations.Inject; +import com.google.cloud.storage.it.runner.annotations.SingleBackend; +import com.google.cloud.storage.it.runner.annotations.StorageFixture; +import com.google.cloud.storage.it.runner.registry.ObjectsFixture; +import com.google.common.base.MoreObjects; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(StorageITRunner.class) +@SingleBackend(Backend.PROD) +public final class ITDefaultProjectionCompatibilityTest { + + @Inject + @StorageFixture(Transport.HTTP) + public Storage http; + + @Inject + @StorageFixture(Transport.GRPC) + public Storage grpc; + + @Inject public BucketInfo bucket; + + @Inject public ObjectsFixture objectsFixture; + + @Test + public void objectMetadata_includesAcls() { + Blob httpBlob = http.get(objectsFixture.getInfo1().getBlobId()); + Blob grpcBlob = grpc.get(objectsFixture.getInfo1().getBlobId()); + + assertThat(extractFromBlob(grpcBlob)).isEqualTo(extractFromBlob(httpBlob)); + } + + @Test + public void listObjectMetadata_includesAcls() { + String bucketName = bucket.getName(); + BlobListOption prefix = BlobListOption.prefix(objectsFixture.getInfo1().getBlobId().getName()); + List httpBlob = http.list(bucketName, prefix).streamAll().collect(Collectors.toList()); + List grpcBlob = grpc.list(bucketName, prefix).streamAll().collect(Collectors.toList()); + + List a = extractFromBlobs(httpBlob); + List b = extractFromBlobs(grpcBlob); + + assertThat(a).isEqualTo(b); + } + + @Test + public void bucketMetadata_includesAcls() { + Bucket httpBucket = http.get(bucket.getName()); + Bucket grpcBucket = grpc.get(bucket.getName()); + + assertThat(extractFromBucket(httpBucket)).isEqualTo(extractFromBucket(grpcBucket)); + } + + @Test + public void listBucketMetadata_includesAcls() { + BucketListOption prefix = BucketListOption.prefix(bucket.getName()); + List httpBucket = http.list(prefix).streamAll().collect(Collectors.toList()); + List grpcBucket = grpc.list(prefix).streamAll().collect(Collectors.toList()); + + List a = extractFromBuckets(httpBucket); + List b = extractFromBuckets(grpcBucket); + + assertThat(a).isEqualTo(b); + } + + @NonNull + private static List extractFromBlobs(List httpBlob) { + return httpBlob.stream() + .map(ITDefaultProjectionCompatibilityTest::extractFromBlob) + .collect(Collectors.toList()); + } + + @NonNull + private static AclRelatedFields extractFromBlob(Blob b) { + return new AclRelatedFields(b.getOwner(), b.getAcl(), null); + } + + @NonNull + private static List extractFromBuckets(List httpBucket) { + return httpBucket.stream() + .map(ITDefaultProjectionCompatibilityTest::extractFromBucket) + .collect(Collectors.toList()); + } + + @NonNull + private static AclRelatedFields extractFromBucket(Bucket b) { + return new AclRelatedFields(b.getOwner(), b.getAcl(), null); + } + + private static final class AclRelatedFields { + @Nullable private final Entity owner; + @Nullable private final List acls; + @Nullable private final List defaultAcls; + + private AclRelatedFields( + @Nullable Entity owner, @Nullable List acls, @Nullable List defaultAcls) { + this.owner = owner; + this.acls = acls; + this.defaultAcls = defaultAcls; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AclRelatedFields)) { + return false; + } + AclRelatedFields that = (AclRelatedFields) o; + return Objects.equals(owner, that.owner) + && Objects.equals(acls, that.acls) + && Objects.equals(defaultAcls, that.defaultAcls); + } + + @Override + public int hashCode() { + return Objects.hash(owner, acls, defaultAcls); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("owner", owner) + .add("acls", acls) + .add("defaultAcls", defaultAcls) + .toString(); + } + } +} From 4d8dd89586b71c293e31b3b860b4bd022bded9fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 14:57:11 -0700 Subject: [PATCH 13/17] build(deps): bump cryptography from 41.0.2 to 41.0.3 in /.kokoro (#2152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build(deps): bump cryptography from 41.0.2 to 41.0.3 in /.kokoro Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.2 to 41.0.3. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/41.0.2...41.0.3) --- updated-dependencies: - dependency-name: cryptography dependency-type: indirect ... Signed-off-by: dependabot[bot] * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Owl Bot From a1c8905f2c20a765f16b57b124a47d865adf37e6 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 3 Aug 2023 00:22:07 +0200 Subject: [PATCH 14/17] test(deps): update cross product test dependencies (#2137) --- google-cloud-storage/pom.xml | 6 +++--- pom.xml | 2 +- samples/install-without-bom/pom.xml | 2 +- samples/native-image-sample/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index aeb9589e20..247b7672dd 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -16,7 +16,7 @@ google-cloud-storage - 1.105.18 + 1.106.0 5.10.0 @@ -173,13 +173,13 @@ com.google.api.grpc proto-google-cloud-kms-v1 - 0.115.0 + 0.116.0 test com.google.cloud google-cloud-kms - 2.24.0 + 2.25.0 test diff --git a/pom.xml b/pom.xml index ad7e2756ca..a22b9d932f 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ com.google.cloud google-cloud-pubsub - 1.123.18 + 1.124.0 test diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index f3d8b89067..b7aee8a148 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -61,7 +61,7 @@ com.google.cloud google-cloud-pubsub - 1.123.18 + 1.124.0 test diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml index 654259dcff..dbee63dc12 100644 --- a/samples/native-image-sample/pom.xml +++ b/samples/native-image-sample/pom.xml @@ -61,7 +61,7 @@ com.google.cloud google-cloud-pubsub - 1.123.18 + 1.124.0 test diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 89306564e1..6ea60e12f7 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -52,7 +52,7 @@ com.google.cloud google-cloud-pubsub - 1.123.18 + 1.124.0 test diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index e12e2124ff..7850435c4b 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -72,7 +72,7 @@ com.google.cloud google-cloud-pubsub - 1.123.18 + 1.124.0 test From eba8b6a235919a27d1f6dadf770140c7d143aa1a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 3 Aug 2023 00:25:12 +0200 Subject: [PATCH 15/17] deps: update dependency com.google.cloud:google-cloud-shared-dependencies to v3.14.0 (#2151) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a22b9d932f..53edf53d54 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ UTF-8 github google-cloud-storage-parent - 3.13.1 + 3.14.0 From 68ad8e7357097e3dd161c2ab5f7a42a060a3702c Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Thu, 3 Aug 2023 16:08:20 -0400 Subject: [PATCH 16/17] fix: possible NPE when HttpStorageOptions deserialized (#2153) Java can dedupe objects in a cyclic traversal, retryingDepsAdapter doesn't need to be transient. b/294399427 --- .../cloud/storage/HttpStorageOptions.java | 4 +-- .../cloud/storage/SerializationTest.java | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java index b525cf75b8..f21860e7f7 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java @@ -52,7 +52,7 @@ public class HttpStorageOptions extends StorageOptions { private static final String DEFAULT_HOST = "https://storage.googleapis.com"; private final HttpRetryAlgorithmManager retryAlgorithmManager; - private final transient RetryDependenciesAdapter retryDepsAdapter; + private final RetryDependenciesAdapter retryDepsAdapter; private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -334,7 +334,7 @@ public ServiceRpc create(StorageOptions options) { * We don't yet want to make HttpStorageOptions itself implement {@link RetryingDependencies} but * we do need use it in a couple places, for those we create this adapter. */ - private final class RetryDependenciesAdapter implements RetryingDependencies { + private final class RetryDependenciesAdapter implements RetryingDependencies, Serializable { private RetryDependenciesAdapter() {} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java index ae3a140f52..2493b59a3a 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java @@ -40,9 +40,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Base64; import java.util.Collections; @@ -213,6 +215,29 @@ protected Serializable[] serializableObjects() { } } + @Test + public void avoidNpeHttpStorageOptions_retryDeps() throws IOException, ClassNotFoundException { + HttpStorageOptions optionsHttp1 = + StorageOptions.http() + .setProjectId("http1") + .setCredentials(NoCredentials.getInstance()) + .build(); + + assertThat(optionsHttp1.asRetryDependencies()).isNotNull(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(optionsHttp1); + } + + byte[] byteArray = baos.toByteArray(); + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArray))) { + Object o = ois.readObject(); + HttpStorageOptions hso = (HttpStorageOptions) o; + assertThat(hso.asRetryDependencies()).isNotNull(); + } + } + @Override @SuppressWarnings("resource") protected Restorable[] restorableObjects() { From e1ce76fe8c6c5d6fc0d40d5c2c64e256294c6416 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:35:30 -0700 Subject: [PATCH 17/17] chore(main): release 2.26.0 (#2140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(main): release 2.26.0 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Owl Bot --- CHANGELOG.md | 20 ++++++++++++++++++++ gapic-google-cloud-storage-v2/pom.xml | 4 ++-- google-cloud-storage-bom/pom.xml | 10 +++++----- google-cloud-storage/pom.xml | 4 ++-- grpc-google-cloud-storage-v2/pom.xml | 4 ++-- pom.xml | 10 +++++----- proto-google-cloud-storage-v2/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 8 ++++---- 9 files changed, 43 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a4c0aa01..95878d8e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [2.26.0](https://github.com/googleapis/java-storage/compare/v2.25.0...v2.26.0) (2023-08-03) + + +### Features + +* Implement BufferToDiskThenUpload BlobWriteSessionConfig ([#2139](https://github.com/googleapis/java-storage/issues/2139)) ([4dad2d5](https://github.com/googleapis/java-storage/commit/4dad2d5c3a81eda7190ad4f95316471e7fa30f66)) +* Introduce new BlobWriteSession ([#2123](https://github.com/googleapis/java-storage/issues/2123)) ([e0191b5](https://github.com/googleapis/java-storage/commit/e0191b518e50a49fae0691894b50f0c5f33fc6af)) + + +### Bug Fixes + +* **grpc:** Return error if credentials are detected to be null ([#2142](https://github.com/googleapis/java-storage/issues/2142)) ([b61a976](https://github.com/googleapis/java-storage/commit/b61a9764a9d953d2b214edb2b543b8df42fbfa06)) +* Possible NPE when HttpStorageOptions deserialized ([#2153](https://github.com/googleapis/java-storage/issues/2153)) ([68ad8e7](https://github.com/googleapis/java-storage/commit/68ad8e7357097e3dd161c2ab5f7a42a060a3702c)) +* Update grpc default metadata projection to include acl same as json ([#2150](https://github.com/googleapis/java-storage/issues/2150)) ([330e795](https://github.com/googleapis/java-storage/commit/330e795040592e5df22d44fb5216ad7cf2448e81)) + + +### Dependencies + +* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.14.0 ([#2151](https://github.com/googleapis/java-storage/issues/2151)) ([eba8b6a](https://github.com/googleapis/java-storage/commit/eba8b6a235919a27d1f6dadf770140c7d143aa1a)) + ## [2.25.0](https://github.com/googleapis/java-storage/compare/v2.24.0...v2.25.0) (2023-07-24) diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml index b5d90a4d78..d36bf76753 100644 --- a/gapic-google-cloud-storage-v2/pom.xml +++ b/gapic-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc gapic-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha gapic-google-cloud-storage-v2 GRPC library for gapic-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.25.1-SNAPSHOT + 2.26.0 diff --git a/google-cloud-storage-bom/pom.xml b/google-cloud-storage-bom/pom.xml index 522b0c8082..7934baabe5 100644 --- a/google-cloud-storage-bom/pom.xml +++ b/google-cloud-storage-bom/pom.xml @@ -19,7 +19,7 @@ 4.0.0 com.google.cloud google-cloud-storage-bom - 2.25.1-SNAPSHOT + 2.26.0 pom com.google.cloud @@ -69,22 +69,22 @@ com.google.cloud google-cloud-storage - 2.25.1-SNAPSHOT + 2.26.0 com.google.api.grpc gapic-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha com.google.api.grpc grpc-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha com.google.api.grpc proto-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 247b7672dd..3ca2b6182e 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-storage - 2.25.1-SNAPSHOT + 2.26.0 jar Google Cloud Storage https://github.com/googleapis/java-storage @@ -12,7 +12,7 @@ com.google.cloud google-cloud-storage-parent - 2.25.1-SNAPSHOT + 2.26.0 google-cloud-storage diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml index a55f3c2326..3e58accabb 100644 --- a/grpc-google-cloud-storage-v2/pom.xml +++ b/grpc-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha grpc-google-cloud-storage-v2 GRPC library for grpc-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.25.1-SNAPSHOT + 2.26.0 diff --git a/pom.xml b/pom.xml index 53edf53d54..2c6d277fbe 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-storage-parent pom - 2.25.1-SNAPSHOT + 2.26.0 Storage Parent https://github.com/googleapis/java-storage @@ -76,7 +76,7 @@ com.google.cloud google-cloud-storage - 2.25.1-SNAPSHOT + 2.26.0 com.google.apis @@ -117,17 +117,17 @@ com.google.api.grpc proto-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha com.google.api.grpc grpc-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha com.google.api.grpc gapic-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha com.google.cloud diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml index 0a67a9eba6..397784370c 100644 --- a/proto-google-cloud-storage-v2/pom.xml +++ b/proto-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-storage-v2 - 2.25.1-alpha-SNAPSHOT + 2.26.0-alpha proto-google-cloud-storage-v2 PROTO library for proto-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.25.1-SNAPSHOT + 2.26.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 6ea60e12f7..11305d2f7f 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-storage - 2.25.1-SNAPSHOT + 2.26.0 diff --git a/versions.txt b/versions.txt index 3aafb4560a..5628eb9eaf 100644 --- a/versions.txt +++ b/versions.txt @@ -1,7 +1,7 @@ # Format: # module:released-version:current-version -google-cloud-storage:2.25.0:2.25.1-SNAPSHOT -gapic-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT -grpc-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT -proto-google-cloud-storage-v2:2.25.0-alpha:2.25.1-alpha-SNAPSHOT +google-cloud-storage:2.26.0:2.26.0 +gapic-google-cloud-storage-v2:2.26.0-alpha:2.26.0-alpha +grpc-google-cloud-storage-v2:2.26.0-alpha:2.26.0-alpha +proto-google-cloud-storage-v2:2.26.0-alpha:2.26.0-alpha pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy