Repository: googleapis/java-storage
Pull request #2206 (2023-09-14 17:49:15)

chore: add AsyncAppendingQueue
__Pre-Work__

When performing a parallel composite upload the various object parts need to be stitched together in sequence.

AsyncAppendingQueue allows enqueuing elements that result in some value, and can then be compacted once a certain number of elements are reached (or complete).




Patch:
diff --git a/.kokoro/release/bump_snapshot.cfg b/.kokoro/release/bump_snapshot.cfg
deleted file mode 100644
index 50ad5ed1..00000000
--- a/.kokoro/release/bump_snapshot.cfg
+++ /dev/null
@@ -1,53 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Build logs will be here
-action {
-  define_artifacts {
-    regex: "**/*sponge_log.xml"
-  }
-}
-
-# Download trampoline resources.
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
-
-# Use the trampoline script to run in docker.
-build_file: "java-storage/.kokoro/trampoline.sh"
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/release/bump_snapshot.sh"
-}
-
-# tokens used by release-please to keep an up-to-date release PR.
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-key-release-please"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-token-release-please"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-url-release-please"
-    }
-  }
-}
diff --git a/.kokoro/release/bump_snapshot.sh b/.kokoro/release/bump_snapshot.sh
deleted file mode 100755
index 798d3045..00000000
--- a/.kokoro/release/bump_snapshot.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-
-# Copyright 2019 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
-#
-#     https://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.
-
-set -eo pipefail
-
-export NPM_CONFIG_PREFIX=/home/node/.npm-global
-
-if [ -f ${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please ]; then
-  # Groom the snapshot release PR immediately after publishing a release
-  npx release-please release-pr --token=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-token-release-please \
-    --repo-url=googleapis/java-storage \
-    --package-name="storage" \
-    --api-url=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please \
-    --proxy-key=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-key-release-please \
-    --snapshot \
-    --release-type=java-auth-yoshi
-fi
diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg
deleted file mode 100644
index 93a278ff..00000000
--- a/.kokoro/release/common.cfg
+++ /dev/null
@@ -1,49 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Download trampoline resources.
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
-
-# Use the trampoline script to run in docker.
-build_file: "java-storage/.kokoro/trampoline.sh"
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "maven-gpg-keyring"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "maven-gpg-passphrase"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "maven-gpg-pubkeyring"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "sonatype-credentials"
-    }
-  }
-}
diff --git a/.kokoro/release/common.sh b/.kokoro/release/common.sh
deleted file mode 100755
index 7f78ee41..00000000
--- a/.kokoro/release/common.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# Get secrets from keystore and set and environment variables
-setup_environment_secrets() {
-  export GPG_PASSPHRASE=$(cat ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-passphrase)
-  export GPG_TTY=$(tty)
-  export GPG_HOMEDIR=/gpg
-  mkdir $GPG_HOMEDIR
-  mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-pubkeyring $GPG_HOMEDIR/pubring.gpg
-  mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-keyring $GPG_HOMEDIR/secring.gpg
-  export SONATYPE_USERNAME=$(cat ${KOKORO_KEYSTORE_DIR}/70247_sonatype-credentials | cut -f1 -d'|')
-  export SONATYPE_PASSWORD=$(cat ${KOKORO_KEYSTORE_DIR}/70247_sonatype-credentials | cut -f2 -d'|')
-}
-
-create_settings_xml_file() {
-  echo "<settings>
-  <servers>
-    <server>
-      <id>ossrh</id>
-      <username>${SONATYPE_USERNAME}</username>
-      <password>${SONATYPE_PASSWORD}</password>
-    </server>
-    <server>
-      <id>sonatype-nexus-staging</id>
-      <username>${SONATYPE_USERNAME}</username>
-      <password>${SONATYPE_PASSWORD}</password>
-    </server>
-    <server>
-      <id>sonatype-nexus-snapshots</id>
-      <username>${SONATYPE_USERNAME}</username>
-      <password>${SONATYPE_PASSWORD}</password>
-    </server>
-  </servers>
-</settings>" > $1
-}
\ No newline at end of file
diff --git a/.kokoro/release/drop.cfg b/.kokoro/release/drop.cfg
deleted file mode 100644
index 4f78a715..00000000
--- a/.kokoro/release/drop.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/release/drop.sh"
-}
diff --git a/.kokoro/release/drop.sh b/.kokoro/release/drop.sh
deleted file mode 100755
index 742ec1a8..00000000
--- a/.kokoro/release/drop.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# STAGING_REPOSITORY_ID must be set
-if [ -z "${STAGING_REPOSITORY_ID}" ]; then
-  echo "Missing STAGING_REPOSITORY_ID environment variable"
-  exit 1
-fi
-
-source $(dirname "$0")/common.sh
-pushd $(dirname "$0")/../../
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-mvn nexus-staging:drop -B \
-  --settings=settings.xml \
-  -DstagingRepositoryId=${STAGING_REPOSITORY_ID}
diff --git a/.kokoro/release/promote.cfg b/.kokoro/release/promote.cfg
deleted file mode 100644
index 6625c8c4..00000000
--- a/.kokoro/release/promote.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/release/promote.sh"
-}
diff --git a/.kokoro/release/promote.sh b/.kokoro/release/promote.sh
deleted file mode 100755
index 3cac3d8a..00000000
--- a/.kokoro/release/promote.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# STAGING_REPOSITORY_ID must be set
-if [ -z "${STAGING_REPOSITORY_ID}" ]; then
-  echo "Missing STAGING_REPOSITORY_ID environment variable"
-  exit 1
-fi
-
-source $(dirname "$0")/common.sh
-
-pushd $(dirname "$0")/../../
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-mvn nexus-staging:release -B \
-  -DperformRelease=true \
-  --settings=settings.xml \
-  -DstagingRepositoryId=${STAGING_REPOSITORY_ID}
diff --git a/.kokoro/release/publish_javadoc.cfg b/.kokoro/release/publish_javadoc.cfg
deleted file mode 100644
index ab50da64..00000000
--- a/.kokoro/release/publish_javadoc.cfg
+++ /dev/null
@@ -1,23 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/doc-templates/"
-
-env_vars: {
-  key: "STAGING_BUCKET"
-  value: "docs-staging"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/release/publish_javadoc.sh"
-}
-
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "docuploader_service_account"
-    }
-  }
-}
diff --git a/.kokoro/release/publish_javadoc.sh b/.kokoro/release/publish_javadoc.sh
deleted file mode 100755
index 9ab06072..00000000
--- a/.kokoro/release/publish_javadoc.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-# Copyright 2019 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.
-
-set -eo pipefail
-
-if [[ -z "${CREDENTIALS}" ]]; then
-  CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account
-fi
-
-if [[ -z "${STAGING_BUCKET}" ]]; then
-  echo "Need to set STAGING_BUCKET environment variable"
-  exit 1
-fi
-
-# work from the git root directory
-pushd $(dirname "$0")/../../
-
-# install docuploader package
-python3 -m pip install --require-hashes -r .kokoro/requirements.txt
-
-# compile all packages
-mvn clean install -B -q -DskipTests=true
-
-export NAME=google-cloud-storage
-export VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3)
-
-# build the docs
-mvn site -B -q
-
-pushd target/site/apidocs
-
-# create metadata
-python3 -m docuploader create-metadata \
-  --name ${NAME} \
-  --version ${VERSION} \
-  --language java
-
-# upload docs
-python3 -m docuploader upload . \
-  --credentials ${CREDENTIALS} \
-  --staging-bucket ${STAGING_BUCKET}
diff --git a/.kokoro/release/publish_javadoc11.cfg b/.kokoro/release/publish_javadoc11.cfg
deleted file mode 100644
index 6aa8afa6..00000000
--- a/.kokoro/release/publish_javadoc11.cfg
+++ /dev/null
@@ -1,30 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# cloud-rad production
-env_vars: {
-  key: "STAGING_BUCKET_V2"
-  value: "docs-staging-v2"
-}
-
-# Configure the docker image for kokoro-trampoline
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java11"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/release/publish_javadoc11.sh"
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "docuploader_service_account"
-    }
-  }
-}
-
-# Downloads docfx doclet resource. This will be in ${KOKORO_GFILE_DIR}/<doclet name>
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/docfx"
diff --git a/.kokoro/release/publish_javadoc11.sh b/.kokoro/release/publish_javadoc11.sh
deleted file mode 100755
index 8bbbf08b..00000000
--- a/.kokoro/release/publish_javadoc11.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash
-# Copyright 2021 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.
-
-set -eo pipefail
-
-if [[ -z "${CREDENTIALS}" ]]; then
-  CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account
-fi
-
-if [[ -z "${STAGING_BUCKET_V2}" ]]; then
-  echo "Need to set STAGING_BUCKET_V2 environment variable"
-  exit 1
-fi
-
-# work from the git root directory
-pushd $(dirname "$0")/../../
-
-# install docuploader package
-python3 -m pip install --require-hashes -r .kokoro/requirements.txt
-
-# compile all packages
-mvn clean install -B -q -DskipTests=true
-
-export NAME=google-cloud-storage
-export VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3)
-
-# cloud RAD generation
-mvn clean javadoc:aggregate -B -q -P docFX
-# include CHANGELOG
-cp CHANGELOG.md target/docfx-yml/history.md
-
-pushd target/docfx-yml
-
-# create metadata
-python3 -m docuploader create-metadata \
- --name ${NAME} \
- --version ${VERSION} \
- --xrefs devsite://java/gax \
- --xrefs devsite://java/google-cloud-core \
- --xrefs devsite://java/api-common \
- --xrefs devsite://java/proto-google-common-protos \
- --xrefs devsite://java/google-api-client \
- --xrefs devsite://java/google-http-client \
- --xrefs devsite://java/protobuf \
- --language java
-
-# upload yml to production bucket
-python3 -m docuploader upload . \
- --credentials ${CREDENTIALS} \
- --staging-bucket ${STAGING_BUCKET_V2} \
- --destination-prefix docfx
diff --git a/.kokoro/release/snapshot.cfg b/.kokoro/release/snapshot.cfg
deleted file mode 100644
index aecfe50e..00000000
--- a/.kokoro/release/snapshot.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/release/snapshot.sh"
-}
\ No newline at end of file
diff --git a/.kokoro/release/snapshot.sh b/.kokoro/release/snapshot.sh
deleted file mode 100755
index 1f55b770..00000000
--- a/.kokoro/release/snapshot.sh
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/bin/bash
-# Copyright 2019 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.
-
-set -eo pipefail
-
-source $(dirname "$0")/common.sh
-MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml
-pushd $(dirname "$0")/../../
-
-# ensure we're trying to push a snapshot (no-result returns non-zero exit code)
-grep SNAPSHOT versions.txt
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-mvn clean deploy -B \
-  --settings ${MAVEN_SETTINGS_FILE} \
-  -DperformRelease=true \
-  -Dgpg.executable=gpg \
-  -Dgpg.passphrase=${GPG_PASSPHRASE} \
-  -Dgpg.homedir=${GPG_HOMEDIR}
diff --git a/.kokoro/release/stage.cfg b/.kokoro/release/stage.cfg
deleted file mode 100644
index 8dc93be4..00000000
--- a/.kokoro/release/stage.cfg
+++ /dev/null
@@ -1,19 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-storage/.kokoro/release/stage.sh"
-}
-
-# Need to save the properties file
-action {
-  define_artifacts {
-    regex: "github/java-storage/target/nexus-staging/staging/*.properties"
-    strip_prefix: "github/java-storage"
-  }
-}
-
-env_vars: {
-  key: "SECRET_MANAGER_KEYS"
-  value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem"
-}
diff --git a/.kokoro/release/stage.sh b/.kokoro/release/stage.sh
deleted file mode 100755
index 61e714d6..00000000
--- a/.kokoro/release/stage.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# Start the releasetool reporter
-requirementsFile=$(realpath $(dirname "${BASH_SOURCE[0]}")/../requirements.txt)
-python3 -m pip install --require-hashes -r $requirementsFile
-python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script
-
-source $(dirname "$0")/common.sh
-source $(dirname "$0")/../common.sh
-MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml
-pushd $(dirname "$0")/../../
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-# attempt to stage 3 times with exponential backoff (starting with 10 seconds)
-retry_with_backoff 3 10 \
-  mvn clean deploy -B \
-    --settings ${MAVEN_SETTINGS_FILE} \
-    -DskipTests=true \
-    -Dclirr.skip=true \
-    -DperformRelease=true \
-    -Dgpg.executable=gpg \
-    -Dgpg.passphrase=${GPG_PASSPHRASE} \
-    -Dgpg.homedir=${GPG_HOMEDIR}
-
-if [[ -n "${AUTORELEASE_PR}" ]]
-then
-  mvn nexus-staging:release -B \
-    -DperformRelease=true \
-    --settings=settings.xml
-fi
diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in
deleted file mode 100644
index 2092cc74..00000000
--- a/.kokoro/requirements.in
+++ /dev/null
@@ -1,6 +0,0 @@
-gcp-docuploader
-gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x
-wheel
-setuptools
-typing-extensions
-click<8.1.0
\ No newline at end of file
diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt
deleted file mode 100644
index a73256ab..00000000
--- a/.kokoro/requirements.txt
+++ /dev/null
@@ -1,489 +0,0 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
-#     pip-compile requirements.in --generate-hashes --upgrade
-#
-attrs==23.1.0 \
-    --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
-    --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
-    # via gcp-releasetool
-cachetools==5.3.1 \
-    --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \
-    --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b
-    # via google-auth
-certifi==2023.7.22 \
-    --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
-    --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
-    # via requests
-cffi==1.15.1 \
-    --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
-    --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
-    --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
-    --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
-    --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
-    --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
-    --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
-    --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
-    --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
-    --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
-    --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
-    --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
-    --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
-    --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
-    --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
-    --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
-    --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
-    --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
-    --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
-    --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
-    --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
-    --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
-    --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
-    --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
-    --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
-    --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
-    --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
-    --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
-    --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
-    --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
-    --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
-    --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
-    --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
-    --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
-    --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
-    --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
-    --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
-    --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
-    --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
-    --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
-    --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
-    --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
-    --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
-    --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
-    --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
-    --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
-    --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
-    --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
-    --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
-    --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
-    --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
-    --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
-    --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
-    --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
-    --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
-    --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
-    --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
-    --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
-    --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
-    --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
-    --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
-    --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
-    --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
-    --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
-    # via cryptography
-charset-normalizer==3.2.0 \
-    --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
-    --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
-    --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
-    --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
-    --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
-    --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
-    --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
-    --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
-    --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
-    --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
-    --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
-    --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
-    --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
-    --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
-    --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
-    --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
-    --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
-    --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
-    --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
-    --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
-    --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
-    --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
-    --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
-    --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
-    --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
-    --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
-    --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
-    --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
-    --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
-    --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
-    --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
-    --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
-    --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
-    --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
-    --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
-    --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
-    --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
-    --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
-    --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
-    --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
-    --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
-    --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
-    --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
-    --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
-    --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
-    --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
-    --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
-    --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
-    --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
-    --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
-    --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
-    --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
-    --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
-    --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
-    --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
-    --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
-    --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
-    --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
-    --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
-    --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
-    --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
-    --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
-    --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
-    --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
-    --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
-    --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
-    --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
-    --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
-    --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
-    --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
-    --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
-    --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
-    --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
-    --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
-    --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
-    # via requests
-click==8.0.4 \
-    --hash=sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1 \
-    --hash=sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb
-    # via
-    #   -r requirements.in
-    #   gcp-docuploader
-    #   gcp-releasetool
-colorlog==6.7.0 \
-    --hash=sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662 \
-    --hash=sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5
-    # via gcp-docuploader
-cryptography==41.0.2 \
-    --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
-    --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
-    --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
-    --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
-    --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
-    --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
-    --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
-    --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
-    --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
-    --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
-    --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
-    --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
-    --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
-    --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
-    --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
-    --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
-    --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
-    --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
-    --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
-    --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
-    --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
-    --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
-    --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
-    # via
-    #   gcp-releasetool
-    #   secretstorage
-gcp-docuploader==0.6.5 \
-    --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \
-    --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea
-    # via -r requirements.in
-gcp-releasetool==1.16.0 \
-    --hash=sha256:27bf19d2e87aaa884096ff941aa3c592c482be3d6a2bfe6f06afafa6af2353e3 \
-    --hash=sha256:a316b197a543fd036209d0caba7a8eb4d236d8e65381c80cbc6d7efaa7606d63
-    # via -r requirements.in
-google-api-core==2.11.1 \
-    --hash=sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a \
-    --hash=sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a
-    # via
-    #   google-cloud-core
-    #   google-cloud-storage
-google-auth==2.22.0 \
-    --hash=sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce \
-    --hash=sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873
-    # via
-    #   gcp-releasetool
-    #   google-api-core
-    #   google-cloud-core
-    #   google-cloud-storage
-google-cloud-core==2.3.3 \
-    --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
-    --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
-    # via google-cloud-storage
-google-cloud-storage==2.10.0 \
-    --hash=sha256:934b31ead5f3994e5360f9ff5750982c5b6b11604dc072bc452c25965e076dc7 \
-    --hash=sha256:9433cf28801671de1c80434238fb1e7e4a1ba3087470e90f70c928ea77c2b9d7
-    # via gcp-docuploader
-google-crc32c==1.5.0 \
-    --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
-    --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
-    --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
-    --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
-    --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
-    --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
-    --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
-    --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
-    --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
-    --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
-    --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
-    --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
-    --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
-    --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
-    --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
-    --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
-    --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
-    --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
-    --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
-    --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
-    --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
-    --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
-    --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
-    --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
-    --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
-    --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
-    --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
-    --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
-    --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
-    --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
-    --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
-    --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
-    --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
-    --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
-    --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
-    --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
-    --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
-    --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
-    --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
-    --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
-    --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
-    --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
-    --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
-    --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
-    --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
-    --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
-    --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
-    --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
-    --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
-    --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
-    --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
-    --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
-    --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
-    --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
-    --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
-    --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
-    --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
-    --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
-    --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
-    --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
-    --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
-    --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
-    --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
-    --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
-    --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
-    --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
-    --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
-    --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
-    # via google-resumable-media
-google-resumable-media==2.5.0 \
-    --hash=sha256:218931e8e2b2a73a58eb354a288e03a0fd5fb1c4583261ac6e4c078666468c93 \
-    --hash=sha256:da1bd943e2e114a56d85d6848497ebf9be6a14d3db23e9fc57581e7c3e8170ec
-    # via google-cloud-storage
-googleapis-common-protos==1.59.1 \
-    --hash=sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e \
-    --hash=sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a
-    # via google-api-core
-idna==3.4 \
-    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
-    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
-    # via requests
-importlib-metadata==6.8.0 \
-    --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
-    --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
-    # via keyring
-jaraco-classes==3.3.0 \
-    --hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \
-    --hash=sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621
-    # via keyring
-jeepney==0.8.0 \
-    --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \
-    --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755
-    # via
-    #   keyring
-    #   secretstorage
-jinja2==3.1.2 \
-    --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
-    --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
-    # via gcp-releasetool
-keyring==24.2.0 \
-    --hash=sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6 \
-    --hash=sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509
-    # via gcp-releasetool
-markupsafe==2.1.3 \
-    --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
-    --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
-    --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
-    --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
-    --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
-    --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
-    --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
-    --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
-    --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
-    --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
-    --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
-    --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
-    --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
-    --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
-    --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
-    --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
-    --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
-    --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
-    --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
-    --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
-    --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
-    --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
-    --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
-    --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
-    --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
-    --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
-    --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
-    --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
-    --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
-    --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
-    --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
-    --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
-    --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
-    --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
-    --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
-    --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
-    --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
-    --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
-    --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
-    --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
-    --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
-    --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
-    --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
-    --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
-    --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
-    --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
-    --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
-    --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
-    --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
-    --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2
-    # via jinja2
-more-itertools==9.1.0 \
-    --hash=sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d \
-    --hash=sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3
-    # via jaraco-classes
-packaging==23.1 \
-    --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
-    --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
-    # via gcp-releasetool
-protobuf==3.20.3 \
-    --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \
-    --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \
-    --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \
-    --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \
-    --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \
-    --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \
-    --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \
-    --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \
-    --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \
-    --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \
-    --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \
-    --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \
-    --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \
-    --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \
-    --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \
-    --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \
-    --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \
-    --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \
-    --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \
-    --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \
-    --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \
-    --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee
-    # via
-    #   gcp-docuploader
-    #   gcp-releasetool
-    #   google-api-core
-pyasn1==0.5.0 \
-    --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \
-    --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde
-    # via
-    #   pyasn1-modules
-    #   rsa
-pyasn1-modules==0.3.0 \
-    --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \
-    --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d
-    # via google-auth
-pycparser==2.21 \
-    --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
-    --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
-    # via cffi
-pyjwt==2.7.0 \
-    --hash=sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1 \
-    --hash=sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074
-    # via gcp-releasetool
-pyperclip==1.8.2 \
-    --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
-    # via gcp-releasetool
-python-dateutil==2.8.2 \
-    --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
-    --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
-    # via gcp-releasetool
-requests==2.31.0 \
-    --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
-    --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
-    # via
-    #   gcp-releasetool
-    #   google-api-core
-    #   google-cloud-storage
-rsa==4.9 \
-    --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
-    --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
-    # via google-auth
-secretstorage==3.3.3 \
-    --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \
-    --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99
-    # via keyring
-six==1.16.0 \
-    --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
-    --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
-    # via
-    #   gcp-docuploader
-    #   google-auth
-    #   python-dateutil
-typing-extensions==4.7.1 \
-    --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \
-    --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2
-    # via -r requirements.in
-urllib3==1.26.16 \
-    --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \
-    --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14
-    # via
-    #   google-auth
-    #   requests
-wheel==0.40.0 \
-    --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
-    --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
-    # via -r requirements.in
-zipp==3.16.1 \
-    --hash=sha256:0b37c326d826d5ca35f2b9685cd750292740774ef16190008b00a0227c256fe0 \
-    --hash=sha256:857b158da2cbf427b376da1c24fd11faecbac5a4ac7523c3607f8a01f94c2ec0
-    # via importlib-metadata
-
-# WARNING: The following packages were not pinned, but pip requires them to be
-# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
-# setuptools
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df498926..fdd78ac8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
 # Changelog
 
+## [2.27.1](https://github.com/googleapis/java-storage/compare/v2.27.0...v2.27.1) (2023-09-25)
+
+
+### Bug Fixes
+
+* Consider Storage#delete(BlobId) idempotent when id has generation ([#2222](https://github.com/googleapis/java-storage/issues/2222)) ([453dd63](https://github.com/googleapis/java-storage/commit/453dd63e63d899197b25b58452c08fa0b41f75b0))
+* Update RecoveryFileManager to allow distinct files for multiple invocations of equivalent info ([#2207](https://github.com/googleapis/java-storage/issues/2207)) ([44e9dd5](https://github.com/googleapis/java-storage/commit/44e9dd558bb979f0c7607ad24f07e6d03a641c66))
+
+
+### Dependencies
+
+* Update dependency com.google.apis:google-api-services-storage to v1-rev20230914-2.0.0 ([#2213](https://github.com/googleapis/java-storage/issues/2213)) ([0af39d6](https://github.com/googleapis/java-storage/commit/0af39d6be333dd6993785491f14e1bd8942573ea))
+* Update dependency com.google.apis:google-api-services-storage to v1-rev20230922-2.0.0 ([#2223](https://github.com/googleapis/java-storage/issues/2223)) ([844fe80](https://github.com/googleapis/java-storage/commit/844fe803af925c7501910a5ea89f9ddadc18b5b9))
+* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.16.0 ([#2219](https://github.com/googleapis/java-storage/issues/2219)) ([7401f21](https://github.com/googleapis/java-storage/commit/7401f2173f60f34506d5d65396b3baadd52de5cc))
+* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.16.1 ([#2225](https://github.com/googleapis/java-storage/issues/2225)) ([f092c4e](https://github.com/googleapis/java-storage/commit/f092c4ebbbbf378511c6e1d663c4656dc03a0724))
+* Update dependency org.graalvm.buildtools:native-maven-plugin to v0.9.27 ([#2210](https://github.com/googleapis/java-storage/issues/2210)) ([3c77530](https://github.com/googleapis/java-storage/commit/3c77530392dac2eca04585506e839d3215d44f0c))
+* Update gapic-generator-java to 2.26.0 ([#2224](https://github.com/googleapis/java-storage/issues/2224)) ([a105736](https://github.com/googleapis/java-storage/commit/a105736535429418740e8e8d72e92e5c705575f1))
+
 ## [2.27.0](https://github.com/googleapis/java-storage/compare/v2.26.1...v2.27.0) (2023-09-12)
 
 
diff --git a/README.md b/README.md
index 5f5c116d..ff400065 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.27.0</version>
+  <version>2.27.1</version>
 </dependency>
 
 ```
@@ -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.27.0'
+implementation 'com.google.cloud:google-cloud-storage:2.27.1'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.27.0"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.27.1"
 ```
 <!-- {x-version-update-end} -->
 
@@ -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.27.0
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.27.1
 [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/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml
index 0634bca9..d8ac02f3 100644
--- a/gapic-google-cloud-storage-v2/pom.xml
+++ b/gapic-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>gapic-google-cloud-storage-v2</artifactId>
-  <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+  <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
   <name>gapic-google-cloud-storage-v2</name>
   <description>GRPC library for gapic-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/google-cloud-storage-bom/pom.xml b/google-cloud-storage-bom/pom.xml
index cf66bbb6..e4fe923a 100644
--- a/google-cloud-storage-bom/pom.xml
+++ b/google-cloud-storage-bom/pom.xml
@@ -19,7 +19,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-bom</artifactId>
-  <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>pom</packaging>
   <parent>
     <groupId>com.google.cloud</groupId>
@@ -69,22 +69,22 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+        <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
     </dependencies>
   </dependencyManagement>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 399d47bb..a495058a 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Storage</name>
   <url>https://github.com/googleapis/java-storage</url>
@@ -12,7 +12,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiFutureUtils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiFutureUtils.java
new file mode 100644
index 00000000..f6dbbce9
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiFutureUtils.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutureCallback;
+import com.google.api.core.ApiFutures;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A set of utility methods for working with {@link com.google.api.core.ApiFuture ApiFutures} that
+ * aren't already provided by {@link com.google.api.core.ApiFutures}
+ */
+final class ApiFutureUtils {
+
+  private ApiFutureUtils() {}
+
+  /**
+   * Similar to {@link com.google.api.gax.rpc.ApiExceptions#callAndTranslateApiException(ApiFuture)}
+   * except that it doesn't add a suppressed exception.
+   *
+   * <p>This should only be used in a context in which awaiting the future is on a thread that
+   * already contains the stack frames which originated the work.
+   */
+  static <T> T await(ApiFuture<T> future) {
+    try {
+      return Futures.getUnchecked(future);
+    } catch (UncheckedExecutionException exception) {
+      if (exception.getCause() instanceof RuntimeException) {
+        throw (RuntimeException) exception.getCause();
+      }
+      throw exception;
+    }
+  }
+
+  static <T> ApiFuture<T> just(T value) {
+    return ApiFutures.immediateFuture(value);
+  }
+
+  /** @see SmugglingException */
+  static <T> ApiFuture<List<T>> quietAllAsList(List<ApiFuture<T>> futures) {
+    List<ApiFuture<T>> pending =
+        futures.stream().map(ApiFutureUtils::smuggleThrowable).collect(Collectors.toList());
+    ApiFuture<List<T>> futureValues = ApiFutures.allAsList(pending);
+    return unwrapSmuggling(futureValues);
+  }
+
+  @FunctionalInterface
+  interface OnSuccessApiFutureCallback<T> extends ApiFutureCallback<T> {
+    @Override
+    default void onFailure(Throwable t) {
+      // noop
+    }
+  }
+
+  @FunctionalInterface
+  interface OnFailureApiFutureCallback<T> extends ApiFutureCallback<T> {
+    @Override
+    default void onSuccess(T result) {
+      // noop
+    }
+  }
+
+  private static <T> ApiFuture<T> smuggleThrowable(ApiFuture<T> future) {
+    return ApiFutures.catchingAsync(
+        future,
+        Throwable.class,
+        throwable -> ApiFutures.immediateFailedFuture(new SmugglingException(throwable)),
+        MoreExecutors.directExecutor());
+  }
+
+  private static <T> ApiFuture<T> unwrapSmuggling(ApiFuture<T> future) {
+    return ApiFutures.catchingAsync(
+        future,
+        SmugglingException.class,
+        smuggled -> ApiFutures.immediateFailedFuture(smuggled.smuggledCause),
+        MoreExecutors.directExecutor());
+  }
+
+  /**
+   * Guava's AggregateFuture attempts to help let you know when multiple futures fail while
+   * resolving via {@link com.google.common.util.concurrent.Futures#allAsList(Iterable)}.
+   *
+   * <p>This is detrimental to our use case, because we don't want to be spamming our customers with
+   * error message they can do nothing about. In an effort to prevent this spam we define a custom
+   * exception wrapper class that is able to smuggle the context we care about past the detection
+   * mechanism (instances are added to a ConcurrentHashSet).
+   *
+   * <p>To accomplish this smuggling, we abuse the following:
+   *
+   * <ol>
+   *   <li>hashCode is hardcoded to a constant value
+   *   <li>equals(Object) returns true if the classes are equal
+   *   <li>we define our own field to carry the cause (guava looks at the cause too)
+   * </ol>
+   *
+   * For our purposes we don't need to distinguish between different instances of our exception, as
+   * we track error at a different level.
+   */
+  private static final class SmugglingException extends RuntimeException {
+    private final Throwable smuggledCause;
+
+    private SmugglingException(Throwable smuggledCause) {
+      super("");
+      this.smuggledCause = smuggledCause;
+    }
+
+    public int hashCode() {
+      return 1;
+    }
+
+    public boolean equals(Object other) {
+      return other.getClass().equals(this.getClass());
+    }
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/AsyncAppendingQueue.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/AsyncAppendingQueue.java
new file mode 100644
index 00000000..644dd014
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/AsyncAppendingQueue.java
@@ -0,0 +1,218 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.api.core.ApiFunction;
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutureCallback;
+import com.google.api.core.ApiFutures;
+import com.google.api.core.SettableApiFuture;
+import com.google.api.gax.rpc.ApiExceptions;
+import com.google.cloud.storage.ApiFutureUtils.OnFailureApiFutureCallback;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.PriorityQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/**
+ * Define a queue where enqueued items are async tasks. When a limit is reached, compact all values
+ * into a new value.
+ */
+final class AsyncAppendingQueue<@NonNull T> implements AutoCloseable {
+
+  private enum State {
+    OPEN,
+    CLOSING,
+    CLOSED;
+
+    boolean isOpen() {
+      return this == OPEN;
+    }
+  }
+
+  private final Executor exec;
+  private final int maxElementsPerCompact;
+  private final ApiFunction<ImmutableList<T>, T> compactFunction;
+  private final AtomicInteger orderSequence;
+  private final SettableApiFuture<T> finalResult;
+  private final PriorityQueue<Element<T>> queue;
+  private final AtomicReference<Throwable> shortCircuitFailure;
+  private final ApiFutureCallback<T> shortCircuitRegistrationCallback;
+
+  private volatile State state;
+
+  private AsyncAppendingQueue(
+      Executor exec, int maxElementsPerCompact, ApiFunction<ImmutableList<T>, T> compactFunction) {
+    this.exec = exec;
+    this.maxElementsPerCompact = maxElementsPerCompact;
+    this.compactFunction = compactFunction;
+    this.orderSequence = new AtomicInteger(0);
+    this.finalResult = SettableApiFuture.create();
+    this.queue = new PriorityQueue<>(maxElementsPerCompact, Element.COMP);
+    this.state = State.OPEN;
+    this.shortCircuitFailure = new AtomicReference<>(null);
+    this.shortCircuitRegistrationCallback =
+        (OnFailureApiFutureCallback<T>)
+            throwable -> {
+              if (state.isOpen()) {
+                shortCircuitFailure.compareAndSet(null, throwable);
+              }
+            };
+  }
+
+  synchronized AsyncAppendingQueue<T> append(ApiFuture<T> value) throws ShortCircuitException {
+    checkState(state.isOpen(), "already closed");
+    Throwable throwable = shortCircuitFailure.get();
+    if (throwable != null) {
+      ShortCircuitException shortCircuitException = new ShortCircuitException(throwable);
+      finalResult.cancel(true);
+      throw shortCircuitException;
+    }
+    checkNotNull(value, "value must not be null");
+
+    Element<T> newElement = newElement(value);
+    queue.offer(newElement);
+    boolean isFull = queue.size() == maxElementsPerCompact;
+    if (isFull) {
+      Element<T> compact = compact(exec);
+      queue.offer(compact);
+    }
+    return this;
+  }
+
+  ApiFuture<T> getResult() {
+    return finalResult;
+  }
+
+  T await() {
+    return ApiExceptions.callAndTranslateApiException(finalResult);
+  }
+
+  @Override
+  public synchronized void close() {
+    if (!state.isOpen()) {
+      return;
+    }
+    state = State.CLOSING;
+
+    if (queue.isEmpty()) {
+      NoSuchElementException neverAppendedTo = new NoSuchElementException("Never appended to");
+      finalResult.setException(neverAppendedTo);
+      throw neverAppendedTo;
+    } else {
+      Element<T> transform = compact(exec);
+
+      ApiFutures.addCallback(
+          transform.getValue(),
+          new ApiFutureCallback<T>() {
+            @Override
+            public void onFailure(Throwable err) {
+              finalResult.setException(err);
+            }
+
+            @Override
+            public void onSuccess(T t) {
+              finalResult.set(t);
+            }
+          },
+          exec);
+    }
+    state = State.CLOSED;
+  }
+
+  @NonNull
+  private Element<T> newElement(ApiFuture<T> value) {
+    ApiFutures.addCallback(value, shortCircuitRegistrationCallback, MoreExecutors.directExecutor());
+    return new Element<>(orderSequence.getAndIncrement(), value);
+  }
+
+  @NonNull
+  private Element<T> compact(Executor executor) {
+    ArrayList<Element<T>> elements = new ArrayList<>();
+    Element<T> peek = queue.peek();
+    checkState(peek != null, "attempt to compact empty queue");
+    int order = peek.getOrder();
+
+    Element<T> curr;
+    while ((curr = queue.poll()) != null) {
+      elements.add(curr);
+    }
+
+    List<ApiFuture<T>> pending =
+        elements.stream().map(Element::getValue).collect(Collectors.toList());
+    ApiFuture<List<T>> futureTs = ApiFutureUtils.quietAllAsList(pending);
+    ApiFuture<T> transform =
+        ApiFutures.transform(
+            futureTs, ts -> compactFunction.apply(ImmutableList.copyOf(ts)), executor);
+    return new Element<>(order, transform);
+  }
+
+  public static <T> AsyncAppendingQueue<T> of(
+      Executor exec, int maxElementsPerCompact, ApiFunction<ImmutableList<T>, T> compactFunction) {
+    checkNotNull(exec, "exec must be non-null");
+    checkArgument(maxElementsPerCompact > 1, "maxElementsPerCompact must be > 1");
+    checkNotNull(compactFunction, "compactFunction must be non-null");
+    return new AsyncAppendingQueue<>(exec, maxElementsPerCompact, compactFunction);
+  }
+
+  static final class ShortCircuitException extends RuntimeException {
+    private ShortCircuitException(Throwable instigator) {
+      super("Short Circuiting due to previously failed future", instigator);
+    }
+  }
+
+  /**
+   * The order in which elements are compacted is important. Define a class that allows defining an
+   * order property for use by the {@link PriorityQueue}
+   */
+  private static final class Element<T> {
+    private static final Comparator<Element<?>> COMP = Comparator.comparing(Element::getOrder);
+    private final int order;
+    private final ApiFuture<T> value;
+
+    public Element(int order, ApiFuture<T> value) {
+      this.order = order;
+      this.value = value;
+    }
+
+    public int getOrder() {
+      return order;
+    }
+
+    public ApiFuture<T> getValue() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("order", order).add("value", value).toString();
+    }
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
index f55c2459..703cd152 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
@@ -96,6 +96,9 @@ final class GrpcRetryAlgorithmManager implements Serializable {
   }
 
   public ResultRetryAlgorithm<?> getFor(DeleteObjectRequest req) {
+    if (req.getGeneration() > 0 || req.hasIfGenerationMatch()) {
+      return retryStrategy.getIdempotentHandler();
+    }
     return retryStrategy.getNonidempotentHandler();
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java
index 7cc053ea..22cac9a7 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java
@@ -203,6 +203,7 @@ final class HttpRetryAlgorithmManager implements Serializable {
   public ResultRetryAlgorithm<?> getForObjectsDelete(
       StorageObject pb, Map<StorageRpc.Option, ?> optionsMap) {
     return optionsMap.containsKey(StorageRpc.Option.IF_GENERATION_MATCH)
+            || (pb.getGeneration() != null && pb.getGeneration() > 0)
         ? retryStrategy.getIdempotentHandler()
         : retryStrategy.getNonidempotentHandler();
   }
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
index 4926c899..1ec06b5f 100644
--- 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
@@ -19,8 +19,12 @@ 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 com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Base64;
@@ -28,6 +32,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 final class RecoveryFileManager {
 
@@ -35,6 +40,8 @@ final class RecoveryFileManager {
   /** Keep track of active info and file */
   private final Map<BlobInfo, RecoveryFile> files;
 
+  private final HashFunction hashFunction;
+
   /**
    * Round-robin assign recovery files to the configured volumes. Use this index to keep track of
    * which volume to assign to next.
@@ -45,13 +52,18 @@ final class RecoveryFileManager {
     this.volumes = ImmutableList.copyOf(volumes);
     this.files = Collections.synchronizedMap(new HashMap<>());
     this.nextVolumeIndex = 0;
+    this.hashFunction = Hashing.goodFastHash(64);
   }
 
+  @SuppressWarnings("UnstableApiUsage")
   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));
+    UUID uuid = UUID.randomUUID();
+    String string = uuid.toString();
+    Hasher hasher = hashFunction.newHasher();
+    HashCode hash = hasher.putString(string, StandardCharsets.UTF_8).hash();
+    String fileName = Base64.getUrlEncoder().encodeToString(hash.asBytes());
     Path path = v.basePath.resolve(fileName);
     RecoveryFile recoveryFile = new RecoveryFile(path, v.sink, () -> files.remove(info));
     files.put(info, recoveryFile);
diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml
index cb9e0f30..b1705189 100644
--- a/grpc-google-cloud-storage-v2/pom.xml
+++ b/grpc-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-v2</artifactId>
-  <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+  <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
   <name>grpc-google-cloud-storage-v2</name>
   <description>GRPC library for grpc-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/grpc-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageGrpc.java b/grpc-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageGrpc.java
index 979f20d9..e3ea6ae4 100644
--- a/grpc-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageGrpc.java
+++ b/grpc-google-cloud-storage-v2/src/main/java/com/google/storage/v2/StorageGrpc.java
@@ -49,7 +49,7 @@ public final class StorageGrpc {
 
   private StorageGrpc() {}
 
-  public static final String SERVICE_NAME = "google.storage.v2.Storage";
+  public static final java.lang.String SERVICE_NAME = "google.storage.v2.Storage";
 
   // Static method descriptors that strictly reflect the proto.
   private static volatile io.grpc.MethodDescriptor<
@@ -3851,9 +3851,9 @@ public final class StorageGrpc {
 
   private static final class StorageMethodDescriptorSupplier extends StorageBaseDescriptorSupplier
       implements io.grpc.protobuf.ProtoMethodDescriptorSupplier {
-    private final String methodName;
+    private final java.lang.String methodName;
 
-    StorageMethodDescriptorSupplier(String methodName) {
+    StorageMethodDescriptorSupplier(java.lang.String methodName) {
       this.methodName = methodName;
     }
 
diff --git a/owlbot.py b/owlbot.py
index 6f17975f..c6f9044a 100644
--- a/owlbot.py
+++ b/owlbot.py
@@ -35,6 +35,8 @@ java.common_templates(excludes=[
   '.kokoro/presubmit/samples.cfg',
   '.kokoro/presubmit/graalvm-native.cfg',
   '.kokoro/presubmit/graalvm-native-17.cfg',
+  '.kokoro/requirements.in',
+  '.kokoro/requirements.txt',
   '.github/workflows/auto-release.yaml',
   'CONTRIBUTING.md',
   'renovate.json'
diff --git a/pom.xml b/pom.xml
index 206fc7e9..f0d472b1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -54,7 +54,7 @@
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <github.global.server>github</github.global.server>
     <site.installationModule>google-cloud-storage-parent</site.installationModule>
-    <google.cloud.shared-dependencies.version>3.15.0</google.cloud.shared-dependencies.version>
+    <google.cloud.shared-dependencies.version>3.16.1</google.cloud.shared-dependencies.version>
   </properties>
 
   <dependencyManagement>
@@ -76,12 +76,12 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.apis</groupId>
         <artifactId>google-api-services-storage</artifactId>
-        <version>v1-rev20230914-2.0.0</version>
+        <version>v1-rev20230922-2.0.0</version>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
@@ -117,17 +117,17 @@
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml
index 9624dcdb..30d748ce 100644
--- a/proto-google-cloud-storage-v2/pom.xml
+++ b/proto-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-v2</artifactId>
-  <version>2.27.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+  <version>2.27.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
   <name>proto-google-cloud-storage-v2</name>
   <description>PROTO library for proto-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index bb3fc342..98d6dad8 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.27.0</version>
+      <version>2.27.1</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 0aaf987a..7519dbfc 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
 
     <dependency>
diff --git a/storage-shared-benchmarking/pom.xml b/storage-shared-benchmarking/pom.xml
index fd028ad6..6f5b832e 100644
--- a/storage-shared-benchmarking/pom.xml
+++ b/storage-shared-benchmarking/pom.xml
@@ -10,7 +10,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
 
   <properties>
@@ -31,7 +31,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.27.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.27.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       <classifier>tests</classifier>
     </dependency>
     <dependency>
diff --git a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java
index 0407cf70..137fee51 100644
--- a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java
+++ b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/StorageSharedBenchmarkingUtils.java
@@ -28,9 +28,12 @@ class StorageSharedBenchmarkingUtils {
         created.getBlobId(), Storage.BlobSourceOption.generationMatch(created.getGeneration()));
   }
 
-  public static double calculateThroughput(long size, Duration elapsedTime) {
-    return size >= StorageSharedBenchmarkingUtils.SSB_SIZE_THRESHOLD_BYTES
-        ? size / 1024 / 1024 / (elapsedTime.toNanos())
-        : size / 1024 / (elapsedTime.toNanos());
+  public static double calculateThroughput(double size, Duration elapsedTime) {
+    double adjustedSize =
+        size >= StorageSharedBenchmarkingUtils.SSB_SIZE_THRESHOLD_BYTES
+            ? (size / 1024D) / 1024D
+            : size / 1024D;
+    double throughput = adjustedSize / (elapsedTime.toMillis() / 1000D);
+    return throughput;
   }
 }
diff --git a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java
index 9833edee..fc96f709 100644
--- a/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java
+++ b/storage-shared-benchmarking/src/main/java/com/google/cloud/storage/benchmarking/W1R3.java
@@ -58,9 +58,9 @@ final class W1R3 implements Callable<String> {
   @Override
   public String call() throws Exception {
     // Create the file to be uploaded and fill it with data
+
     try (TmpFile file = DataGenerator.base64Characters().tempFile(tempDirectory, objectSize)) {
       BlobInfo blob = BlobInfo.newBuilder(bucketName, file.toString()).build();
-
       // Get the start time
       Clock clock = Clock.systemDefaultZone();
       Instant startTime = clock.instant();
@@ -71,7 +71,7 @@ final class W1R3 implements Callable<String> {
           generateCloudMonitoringResult(
                   "WRITE",
                   StorageSharedBenchmarkingUtils.calculateThroughput(
-                      created.getSize().longValue(), elapsedTimeUpload),
+                      created.getSize().doubleValue(), elapsedTimeUpload),
                   created)
               .formatAsCustomMetric());
       for (int i = 0; i <= StorageSharedBenchmarkingUtils.DEFAULT_NUMBER_OF_READS; i++) {
@@ -84,7 +84,7 @@ final class W1R3 implements Callable<String> {
               generateCloudMonitoringResult(
                       "READ[" + i + "]",
                       StorageSharedBenchmarkingUtils.calculateThroughput(
-                          created.getSize().longValue(), elapsedTimeDownload),
+                          created.getSize().doubleValue(), elapsedTimeDownload),
                       created)
                   .formatAsCustomMetric());
         }
diff --git a/versions.txt b/versions.txt
index 4544b72d..913cf539 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,7 +1,7 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:2.27.0:2.27.1-SNAPSHOT
-gapic-google-cloud-storage-v2:2.27.0-alpha:2.27.1-alpha-SNAPSHOT
-grpc-google-cloud-storage-v2:2.27.0-alpha:2.27.1-alpha-SNAPSHOT
-proto-google-cloud-storage-v2:2.27.0-alpha:2.27.1-alpha-SNAPSHOT
+google-cloud-storage:2.27.1:2.27.2-SNAPSHOT
+gapic-google-cloud-storage-v2:2.27.1-alpha:2.27.2-alpha-SNAPSHOT
+grpc-google-cloud-storage-v2:2.27.1-alpha:2.27.2-alpha-SNAPSHOT
+proto-google-cloud-storage-v2:2.27.1-alpha:2.27.2-alpha-SNAPSHOT


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ApiFutureUtilsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ApiFutureUtilsTest.java
new file mode 100644
index 00000000..5ed14f2c
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ApiFutureUtilsTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.ApiFutureUtils.await;
+import static com.google.cloud.storage.ApiFutureUtils.just;
+import static com.google.cloud.storage.TestUtils.assertAll;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.SettableApiFuture;
+import com.google.cloud.testing.junit4.StdErrCaptureRule;
+import com.google.cloud.testing.junit4.StdOutCaptureRule;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+
+public final class ApiFutureUtilsTest {
+
+  @Rule public final StdOutCaptureRule stdOut = new StdOutCaptureRule();
+  @Rule public final StdErrCaptureRule stdErr = new StdErrCaptureRule();
+
+  @Test
+  public void quietAllAsList_returnsFirstFailureAndDoesNotLogLaterExceptions() throws Exception {
+
+    // define a couple futures that will fail later
+    SettableApiFuture<String> b = SettableApiFuture.create();
+    SettableApiFuture<String> c = SettableApiFuture.create();
+
+    ImmutableList<ApiFuture<String>> futures = ImmutableList.of(just("a"), b, c, just("d"));
+
+    ApiFuture<List<String>> all = ApiFutureUtils.quietAllAsList(futures);
+
+    b.setException(new Kaboom());
+    c.setException(new RuntimeException());
+
+    assertAll(
+        () -> assertThrows(Kaboom.class, () -> await(all)),
+        () -> assertWithMessage("stdout").that(stdOut.getCapturedOutputAsUtf8String()).isEmpty(),
+        () -> assertWithMessage("stderr").that(stdErr.getCapturedOutputAsUtf8String()).isEmpty());
+  }
+
+  private static final class Kaboom extends RuntimeException {
+    private Kaboom() {
+      super("Kaboom!!!");
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/AsyncAppendingQueueTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/AsyncAppendingQueueTest.java
new file mode 100644
index 00000000..d4c107ef
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/AsyncAppendingQueueTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.api.core.ApiFunction;
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutures;
+import com.google.api.core.SettableApiFuture;
+import com.google.api.gax.rpc.ApiExceptions;
+import com.google.cloud.storage.AsyncAppendingQueue.ShortCircuitException;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.NoSuchElementException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public final class AsyncAppendingQueueTest {
+
+  private static ExecutorService exec;
+
+  @BeforeClass
+  public static void beforeClass() {
+    ThreadFactory threadFactory =
+        new ThreadFactoryBuilder().setDaemon(true).setNameFormat("test-aaqt-%d").build();
+    exec = Executors.newCachedThreadPool(threadFactory);
+  }
+
+  @AfterClass
+  public static void afterClass() {
+    if (exec != null) {
+      exec.shutdownNow();
+    }
+  }
+
+  @Test
+  public void attemptingToAppendAfterClose_errors() {
+    Executor exec = MoreExecutors.newDirectExecutorService();
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 3, AsyncAppendingQueueTest::agg);
+
+    q.append(ApiFutures.immediateFuture("a"));
+    q.close();
+
+    IllegalStateException iae =
+        assertThrows(IllegalStateException.class, () -> q.append(ApiFutures.immediateFuture("b")));
+
+    assertThat(iae).hasMessageThat().contains("closed");
+  }
+
+  @Test
+  public void getResultPendingUntilClose()
+      throws ExecutionException, InterruptedException, TimeoutException {
+    Executor exec = MoreExecutors.newDirectExecutorService();
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 3, AsyncAppendingQueueTest::agg);
+
+    q.append(ApiFutures.immediateFuture("a"));
+    ApiFuture<String> result = q.getResult();
+    assertThrows(TimeoutException.class, () -> result.get(3, TimeUnit.MILLISECONDS));
+    q.close();
+    String s = result.get(10, TimeUnit.MILLISECONDS);
+
+    assertThat(s).isEqualTo("a");
+  }
+
+  @Test
+  public void getResultAlwaysReturnsTheSameFuture() {
+    Executor exec = MoreExecutors.newDirectExecutorService();
+    try (AsyncAppendingQueue<String> q =
+        AsyncAppendingQueue.of(exec, 3, AsyncAppendingQueueTest::agg)) {
+
+      q.append(ApiFutures.immediateFuture("a"));
+      ApiFuture<String> result1 = q.getResult();
+      ApiFuture<String> result2 = q.getResult();
+
+      assertThat(result1).isSameInstanceAs(result2);
+    }
+  }
+
+  @Test
+  public void closingWithoutAppending_throwNoSuchElementException() {
+    Executor exec = MoreExecutors.newDirectExecutorService();
+    //noinspection resource
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 3, AsyncAppendingQueueTest::agg);
+
+    ApiFuture<String> result = q.getResult();
+    NoSuchElementException nse1 = assertThrows(NoSuchElementException.class, q::close);
+    NoSuchElementException nse2 =
+        assertThrows(
+            NoSuchElementException.class, () -> ApiExceptions.callAndTranslateApiException(result));
+
+    assertThat(nse1).hasMessageThat().contains("Never appended to");
+    assertThat(nse2).hasMessageThat().contains("Never appended to");
+  }
+
+  @SuppressWarnings("resource")
+  @Test
+  public void factoryMethodMustNotAccept_nullExecutor() {
+    assertThrows(NullPointerException.class, () -> AsyncAppendingQueue.of(null, 5, null));
+  }
+
+  @SuppressWarnings("resource")
+  @Test
+  public void factoryMethodMustNotAccept_maxElementsPerCompact_lte_1() {
+    Executor exec = MoreExecutors.newDirectExecutorService();
+    assertThrows(IllegalArgumentException.class, () -> AsyncAppendingQueue.of(exec, 1, null));
+    assertThrows(IllegalArgumentException.class, () -> AsyncAppendingQueue.of(exec, 0, null));
+    assertThrows(IllegalArgumentException.class, () -> AsyncAppendingQueue.of(exec, -10, null));
+  }
+
+  @Test
+  public void happyPath() throws Exception {
+    int arity = 2;
+    ApiFuture<String> result;
+
+    try (AsyncAppendingQueue<String> q =
+        AsyncAppendingQueue.of(exec, arity, AsyncAppendingQueueTest::agg)) {
+
+      q.append(immediate("a"))
+          .append(immediate("b"))
+          .append(immediate("c"))
+          .append(immediate("d"))
+          .append(immediate("e"))
+          .append(immediate("f"))
+          .append(immediate("g"));
+
+      result = q.getResult();
+    }
+    assertThat(result).isNotNull();
+    String s = result.get();
+    assertThat(s).isEqualTo("abcdefg");
+  }
+
+  @Test
+  public void appendShouldShortCircuit() {
+    Executor exec = MoreExecutors.newDirectExecutorService();
+
+    AtomicInteger aggCounter = new AtomicInteger(0);
+    ApiFunction<ImmutableList<String>, String> agg =
+        ss -> {
+          aggCounter.getAndIncrement();
+          return agg(ss);
+        };
+
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 5, agg);
+    q.append(immediate("a")).append(immediate("b")).append(immediate("c"));
+
+    q.append(ApiFutures.immediateFailedFuture(new Kaboom()));
+
+    assertThrows(ShortCircuitException.class, () -> q.append(immediate("d")));
+    assertThrows(CancellationException.class, q::await);
+
+    q.close();
+
+    assertThat(aggCounter.get()).isEqualTo(0);
+  }
+
+  @Test
+  public void resultFailureIfLastAppendFutureFails() {
+
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 2, AsyncAppendingQueueTest::agg);
+    SettableApiFuture<String> d = SettableApiFuture.create();
+    q.append(immediate("a"))
+        .append(immediate("b"))
+        .append(immediate("b"))
+        .append(immediate("c"))
+        .append(d);
+
+    q.close();
+    d.setException(new Kaboom());
+    assertThrows(Kaboom.class, q::await);
+  }
+
+  @Test
+  public void resultFailureIfFinalCompactFails() {
+    ApiFunction<ImmutableList<String>, String> agg =
+        ss -> {
+          if (ss.equals(ImmutableList.of("abc", "d"))) {
+            throw new Kaboom();
+          } else {
+            return agg(ss);
+          }
+        };
+
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 2, agg);
+    q.append(immediate("a")).append(immediate("b")).append(immediate("c")).append(immediate("d"));
+
+    q.close();
+    assertThrows(Kaboom.class, q::await);
+  }
+
+  @Test
+  public void append_multipleFailingFuturesWillAlwaysReturnTheFirstFailure() {
+
+    // define a couple futures that will fail later
+    SettableApiFuture<String> b = SettableApiFuture.create();
+    SettableApiFuture<String> c = SettableApiFuture.create();
+
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 6, AsyncAppendingQueueTest::agg);
+    q.append(immediate("a")).append(b).append(c).append(immediate("d"));
+
+    b.setException(new Kaboom());
+    c.setException(new RuntimeException());
+    q.close();
+
+    assertThrows(Kaboom.class, q::await);
+  }
+
+  @Test
+  public void shortCircuitOnlyHappensBeforeClose_affirmative() {
+    Executor exec = MoreExecutors.newDirectExecutorService();
+
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 5, AsyncAppendingQueueTest::agg);
+    q.append(immediate("a"));
+    q.append(ApiFutures.immediateFailedFuture(new Kaboom()));
+
+    assertThrows(ShortCircuitException.class, () -> q.append(immediate("d")));
+    assertThrows(CancellationException.class, q::await);
+
+    q.close();
+  }
+
+  @Test
+  public void shortCircuitOnlyHappensBeforeClose_negative() {
+
+    AsyncAppendingQueue<String> q = AsyncAppendingQueue.of(exec, 5, AsyncAppendingQueueTest::agg);
+    q.append(immediate("a"));
+    SettableApiFuture<String> d = SettableApiFuture.create();
+    q.append(d);
+
+    q.close();
+    d.setException(new Kaboom());
+
+    assertThrows(Kaboom.class, q::await);
+  }
+
+  static String agg(ImmutableList<String> ss) {
+    return ss.stream().reduce("", String::concat);
+  }
+
+  static ApiFuture<String> immediate(String s) {
+    return ApiFutures.immediateFuture(s);
+  }
+
+  private static final class Kaboom extends RuntimeException {
+    private Kaboom() {
+      super("Kaboom!!!");
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest.java
new file mode 100644
index 00000000..240ae1d5
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest.java
@@ -0,0 +1,848 @@
+/*
+ * 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.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 com.google.api.core.ApiFuture;
+import com.google.api.core.SettableApiFuture;
+import com.google.api.gax.grpc.GrpcCallContext;
+import com.google.api.gax.retrying.ResultRetryAlgorithm;
+import com.google.api.gax.retrying.TimedAttemptSettings;
+import com.google.api.gax.rpc.ApiExceptions;
+import com.google.api.gax.rpc.UnavailableException;
+import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel;
+import com.google.cloud.storage.SyncAndUploadUnbufferedWritableByteChannel.Alg;
+import com.google.cloud.storage.SyncAndUploadUnbufferedWritableByteChannel.RequestStream;
+import com.google.cloud.storage.SyncAndUploadUnbufferedWritableByteChannel.ResponseStream;
+import com.google.cloud.storage.UnbufferedWritableByteChannelSession.UnbufferedWritableByteChannel;
+import com.google.cloud.storage.UnifiedOpts.Opts;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+import com.google.protobuf.ByteString;
+import com.google.storage.v2.Object;
+import com.google.storage.v2.QueryWriteStatusRequest;
+import com.google.storage.v2.QueryWriteStatusResponse;
+import com.google.storage.v2.StartResumableWriteRequest;
+import com.google.storage.v2.StartResumableWriteResponse;
+import com.google.storage.v2.StorageClient;
+import com.google.storage.v2.StorageGrpc.StorageImplBase;
+import com.google.storage.v2.WriteObjectRequest;
+import com.google.storage.v2.WriteObjectResponse;
+import io.grpc.Status;
+import io.grpc.Status.Code;
+import io.grpc.stub.StreamObserver;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+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.RandomDistribution;
+import net.jqwik.api.Tuple;
+import net.jqwik.api.arbitraries.IntegerArbitrary;
+import net.jqwik.api.lifecycle.AfterContainer;
+import net.jqwik.api.lifecycle.AfterProperty;
+import net.jqwik.api.lifecycle.BeforeContainer;
+import net.jqwik.api.lifecycle.BeforeProperty;
+import net.jqwik.api.lifecycle.BeforeTry;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+public class ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest {
+
+  private static Path tmpFolder;
+  private static RecoveryFileManager recoveryFileManager;
+  private FailureInducingStorageImpl failureInducingStorage;
+  private FakeServer server;
+  private GrpcStorageImpl storage;
+
+  @BeforeContainer
+  static void beforeContainer() throws IOException {
+    tmpFolder =
+        Files.createTempDirectory(
+            ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest.class.getSimpleName());
+    recoveryFileManager = RecoveryFileManager.of(ImmutableList.of(tmpFolder));
+  }
+
+  @AfterContainer
+  static void afterContainer() throws IOException {
+    if (tmpFolder != null) {
+      Files.walkFileTree(
+          tmpFolder,
+          new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                throws IOException {
+              Files.deleteIfExists(file);
+              return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path dir, IOException exc)
+                throws IOException {
+              Files.deleteIfExists(dir);
+              return FileVisitResult.CONTINUE;
+            }
+          });
+    }
+  }
+
+  @BeforeProperty
+  void beforeProperty() throws IOException {
+    failureInducingStorage = new FailureInducingStorageImpl();
+    server = FakeServer.of(failureInducingStorage);
+    storage = (GrpcStorageImpl) server.getGrpcStorageOptions().getService();
+  }
+
+  @AfterProperty
+  void afterProperty() throws Exception {
+    // use try-with-resources to do the close dance
+    try (AutoCloseable ignore1 = server;
+        AutoCloseable ignore2 = storage) {
+      storage = null;
+      server = null;
+    }
+  }
+
+  @BeforeTry
+  void beforeTry() {
+    failureInducingStorage.reset();
+  }
+
+  @Example
+  void emptyObject() throws Exception {
+    Scenario scenario = Scenario.of("empty", 0, 0, 256, 256, FailuresQueue.empty());
+    testUploads(scenario);
+  }
+
+  @Example
+  void requestStream_halfClosedToUnavailable_positive() {
+    UnavailableException unavailableException =
+        assertThrows(
+            UnavailableException.class,
+            () ->
+                RequestStream.halfClosedToUnavailable(
+                    () -> {
+                      throw new IllegalStateException("asdf half-closed fdsa");
+                    }));
+    assertThat(unavailableException)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("asdf half-closed fdsa");
+  }
+
+  @Example
+  void requestStream_halfClosedToUnavailable_negative() {
+    IllegalStateException illegalStateException =
+        assertThrows(
+            IllegalStateException.class,
+            () ->
+                RequestStream.halfClosedToUnavailable(
+                    () -> {
+                      throw new IllegalStateException("blah");
+                    }));
+    assertThat(illegalStateException).hasMessageThat().isEqualTo("blah");
+  }
+
+  @Example
+  void alg_shouldSetResultFutureIfNotRetryable() {
+    SettableApiFuture<WriteObjectResponse> resultFuture = SettableApiFuture.create();
+    Alg alg =
+        new Alg((ResultRetryAlgorithmAdapter) (prevThrowable, prevResponse) -> false, resultFuture);
+
+    ForcedFailure ff = new ForcedFailure("should not be retried");
+    boolean shouldRetry = alg.shouldRetry(ff, null);
+    assertThat(shouldRetry).isFalse();
+    assertThat(resultFuture.isDone()).isTrue();
+    ExecutionException runtimeException = assertThrows(ExecutionException.class, resultFuture::get);
+    assertThat(runtimeException).hasCauseThat().hasMessageThat().isEqualTo("should not be retried");
+  }
+
+  @Example
+  void alg_shouldNotSetResultFutureIfRetryable() {
+    SettableApiFuture<WriteObjectResponse> resultFuture = SettableApiFuture.create();
+    Alg alg =
+        new Alg((ResultRetryAlgorithmAdapter) (prevThrowable, prevResponse) -> true, resultFuture);
+
+    ForcedFailure ff = new ForcedFailure("can be retried");
+    boolean shouldRetry = alg.shouldRetry(ff, null);
+    assertThat(shouldRetry).isTrue();
+    assertThat(resultFuture.isDone()).isFalse();
+  }
+
+  @Example
+  void responseStream_onComplete_lastMessageWithResourceMustResolveResultFuture()
+      throws ExecutionException, InterruptedException {
+    SettableApiFuture<WriteObjectResponse> resultFuture = SettableApiFuture.create();
+
+    ResponseStream responseStream = new ResponseStream(resultFuture);
+
+    Object fake = Object.newBuilder().setName("fake").build();
+    WriteObjectResponse response = WriteObjectResponse.newBuilder().setResource(fake).build();
+    responseStream.onNext(response);
+
+    assertThat(resultFuture.isDone()).isFalse();
+    responseStream.onCompleted();
+    assertThat(resultFuture.isDone()).isTrue();
+    assertThat(resultFuture.get()).isEqualTo(response);
+  }
+
+  @Example
+  void responseStream_onComplete_lastMessageWithoutResourceDoesNotResolveResultFuture() {
+    SettableApiFuture<WriteObjectResponse> resultFuture = SettableApiFuture.create();
+
+    ResponseStream responseStream = new ResponseStream(resultFuture);
+
+    WriteObjectResponse response = WriteObjectResponse.newBuilder().setPersistedSize(3).build();
+    responseStream.onNext(response);
+
+    assertThat(resultFuture.isDone()).isFalse();
+    responseStream.onCompleted();
+    assertThat(resultFuture.isDone()).isFalse();
+  }
+
+  @Example
+  void responseStream_await_yields_onComplete() throws ExecutionException, InterruptedException {
+    SettableApiFuture<WriteObjectResponse> resultFuture = SettableApiFuture.create();
+
+    ResponseStream responseStream = new ResponseStream(resultFuture);
+
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+    try {
+      Future<String> submit =
+          executorService.submit(
+              () -> {
+                responseStream.await();
+                return "Success";
+              });
+
+      Object fake = Object.newBuilder().setName("fake").build();
+      WriteObjectResponse response = WriteObjectResponse.newBuilder().setResource(fake).build();
+      responseStream.onNext(response);
+      responseStream.onCompleted();
+
+      assertThat(submit.get()).isEqualTo("Success");
+    } finally {
+      executorService.shutdownNow();
+    }
+  }
+
+  @Example
+  void responseStream_await_yields_onError() throws ExecutionException, InterruptedException {
+    SettableApiFuture<WriteObjectResponse> resultFuture = SettableApiFuture.create();
+
+    ResponseStream responseStream = new ResponseStream(resultFuture);
+
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+    try {
+      Future<String> submit =
+          executorService.submit(
+              () -> {
+                try {
+                  responseStream.await();
+                } catch (ForcedFailure ff) {
+                  return "Success";
+                }
+                return "Fail";
+              });
+
+      RuntimeException re = new ForcedFailure("error");
+      responseStream.onError(re);
+
+      assertThat(submit.get()).isEqualTo("Success");
+    } finally {
+      executorService.shutdownNow();
+    }
+  }
+
+  //  25 tries leads to ~0m:30s of runtime
+  // 250 tries leads to ~6m:00s of runtime
+  @Property(tries = 25)
+  void testUploads(@ForAll("scenario") Scenario s) throws Exception {
+
+    StorageClient storageClient = storage.storageClient;
+    BlobInfo info = BlobInfo.newBuilder("buck", s.objectName).build();
+    try (RecoveryFile rf = s.recoveryFileManager.newRecoveryFile(info)) {
+      SettableApiFuture<WriteObjectResponse> resultFuture = SettableApiFuture.create();
+
+      ApiFuture<ResumableWrite> f =
+          storage.startResumableWrite(
+              GrpcCallContext.createDefault(), storage.getWriteObjectRequest(info, Opts.empty()));
+      ResumableWrite resumableWrite = ApiExceptions.callAndTranslateApiException(f);
+
+      UploadCtx uploadCtx =
+          failureInducingStorage.data.get(UploadId.of(resumableWrite.getRes().getUploadId()));
+
+      uploadCtx.failuresQueue = s.failuresQueue;
+
+      SyncAndUploadUnbufferedWritableByteChannel syncAndUpload =
+          new SyncAndUploadUnbufferedWritableByteChannel(
+              storageClient.writeObjectCallable(),
+              storageClient.queryWriteStatusCallable(),
+              resultFuture,
+              s.chunkSegmenter,
+              TestUtils.defaultRetryingDeps(),
+              StorageRetryStrategy.getDefaultStorageRetryStrategy().getIdempotentHandler(),
+              new WriteCtx<>(resumableWrite),
+              rf,
+              s.copyBuffer);
+      try (BufferedWritableByteChannel w = s.buffered(syncAndUpload)) {
+        for (ByteString dataFrame : s.dataFrames) {
+          w.write(dataFrame.asReadOnlyByteBuffer());
+        }
+      }
+
+      WriteObjectResponse response = resultFuture.get(1, TimeUnit.SECONDS);
+      assertThat(response.hasResource()).isTrue();
+      Object resource = response.getResource();
+
+      ByteString actual =
+          uploadCtx.parts.stream()
+              .filter(WriteObjectRequest::hasChecksummedData)
+              .map(wor -> wor.getChecksummedData().getContent())
+              .reduce(ByteString.empty(), ByteString::concat);
+      ByteString expected = s.dataFrames.stream().reduce(ByteString.empty(), ByteString::concat);
+      assertAll(
+          () -> assertThat(uploadCtx.getLength()).isEqualTo(s.objectSize),
+          () -> assertThat(resource.getSize()).isEqualTo(s.objectSize),
+          () -> assertThat(xxd(actual)).isEqualTo(xxd(expected)));
+    }
+  }
+
+  static List<ByteString> dataFrames(long length, int segmentLength) {
+    // todo: rethink this
+    Random rand = new Random(length);
+    ArrayList<ByteString> segments = new ArrayList<>();
+
+    int i = 0;
+    for (; i < length; i += segmentLength) {
+      long remaining = length - i;
+      int size = Math.toIntExact(Math.min(remaining, segmentLength));
+      byte[] bytes = DataGenerator.rand(rand).genBytes(size);
+      if (size > 4) {
+        byte[] byteArray = Ints.toByteArray(i);
+        ByteString offset = ByteString.copyFrom(byteArray);
+        ByteString concat = offset.concat(ByteString.copyFrom(bytes, 4, bytes.length - 4));
+        segments.add(concat);
+      } else {
+        segments.add(ByteString.copyFrom(bytes));
+      }
+    }
+
+    return ImmutableList.copyOf(segments);
+  }
+
+  @Provide("scenario")
+  static Arbitrary<Scenario> scenarioArbitrary() {
+    // 1. choose an alignment quantum
+    return alignmentQuantumArbitrary()
+        .flatMap(
+            quantum ->
+                Combinators.combine(
+                        Arbitraries.just(quantum),
+                        // 2. choose a segment size between 1 and 8 times the quantum
+                        ints().between(1, 8).map(mult -> quantum * mult))
+                    .as(Tuple::of))
+        .flatMap(
+            t -> {
+              int segmentSize = t.get2();
+              return Combinators.combine(
+                      Arbitraries.just(t.get1()),
+                      Arbitraries.just(segmentSize),
+                      // 3. choose an object size between 0 and 32 time segment size
+                      //   this helps keep the maximum number of rights relatively low and
+                      //   proportional with the size of the object
+                      ints().between(0, 32 * segmentSize))
+                  .as(Tuple::of);
+            })
+        .flatMap(
+            t -> {
+              int quantum = t.get1();
+              int objectSize = t.get3();
+              // if the object isn't 0 bytes, set our min write size to be 1
+              int minWriteSize = Math.min(1, objectSize);
+
+              // determine how many quantum will make up the full object
+              // we want to align failures to quantum boundaries like GCS does
+              int quantumCount = objectSize / quantum;
+              return Combinators.combine(
+                      Arbitraries.just(quantum),
+                      Arbitraries.just(t.get2()),
+                      Arbitraries.just(objectSize),
+                      ints().between(minWriteSize, objectSize),
+                      // 4. generate between 0 and 3 failure offsets
+                      ints()
+                          .between(0, quantumCount)
+                          .map(i -> FailureOffset.of((long) i * quantum))
+                          .list()
+                          .ofMinSize(0)
+                          .ofMaxSize(3)
+                          .map(FailuresQueue::new))
+                  .as(Tuple::of);
+            })
+        .map(
+            t -> {
+              // 5. Construct our scenario from the generated values
+              int quantum = t.get1();
+              int segmentSize = t.get2();
+              int objectSize = t.get3();
+              int writeSize = t.get4();
+              return Scenario.of(
+                  String.format("object-%d", t.hashCode()),
+                  objectSize,
+                  writeSize,
+                  segmentSize,
+                  quantum,
+                  t.get5());
+            })
+        // The way we're defining things there aren't critical edge cases. Let jqwik know, so it
+        // can be smarter about generation, evaluation and shrinking
+        .withoutEdgeCases();
+  }
+
+  static Arbitrary<Integer> alignmentQuantumArbitrary() {
+    // 16..256KiB
+    return ints().between(4, 18).map(i -> Math.toIntExact((long) Math.pow(2, i)));
+  }
+
+  @NonNull
+  private static IntegerArbitrary ints() {
+    return Arbitraries.integers().withDistribution(RandomDistribution.uniform());
+  }
+
+  private static String fmt(int i) {
+    return String.format("% 10d (0x%08x)", i, i);
+  }
+
+  private static final class Scenario {
+    private final String toString;
+    private final String objectName;
+    private final long objectSize;
+    private final ChunkSegmenter chunkSegmenter;
+    private final BufferHandle bufferHandle;
+    private final BufferHandle copyBuffer;
+    private final FailuresQueue failuresQueue;
+    private final RecoveryFileManager recoveryFileManager;
+    private final List<ByteString> dataFrames;
+
+    private Scenario(
+        String toString,
+        String objectName,
+        long objectSize,
+        ChunkSegmenter chunkSegmenter,
+        BufferHandle bufferHandle,
+        BufferHandle copyBuffer,
+        FailuresQueue failuresQueue,
+        RecoveryFileManager recoveryFileManager,
+        List<ByteString> dataFrames) {
+      this.toString = toString;
+      this.objectName = objectName;
+      this.objectSize = objectSize;
+      this.chunkSegmenter = chunkSegmenter;
+      this.bufferHandle = bufferHandle;
+      this.copyBuffer = copyBuffer;
+      this.failuresQueue = failuresQueue;
+      this.recoveryFileManager = recoveryFileManager;
+      this.dataFrames = dataFrames;
+    }
+
+    BufferedWritableByteChannel buffered(UnbufferedWritableByteChannel c) {
+      return StorageByteChannels.writable()
+          .createSynchronized(new DefaultBufferedWritableByteChannel(bufferHandle, c));
+    }
+
+    public static Scenario of(
+        String objectName,
+        long objectSize,
+        int writeSize,
+        int segmentSize,
+        int quantum,
+        FailuresQueue failuresQueue) {
+
+      List<FailureOffset> nonQuantumAligned =
+          failuresQueue.statuses.stream()
+              .filter(f -> f.getOffset() % quantum != 0)
+              .collect(Collectors.toList());
+      assertWithMessage("Failure offsets not quantum aligned (quantum=%s)", fmt(quantum))
+          .that(nonQuantumAligned)
+          .isEmpty();
+      List<ByteString> dataFrames = dataFrames(objectSize, writeSize);
+      return new Scenario(
+          MoreObjects.toStringHelper(Scenario.class)
+              .add("\n  objectName", objectName)
+              .add("\n  objectSize", objectSize)
+              .add("\n  writeSize", writeSize)
+              .add("\n  segmentSize", segmentSize)
+              .add("\n  quantum", quantum)
+              .add("\n  dataFrames.size()", dataFrames.size())
+              .add("\n  failuresQueue", failuresQueue)
+              .addValue("\n")
+              .toString(),
+          objectName,
+          objectSize,
+          new ChunkSegmenter(Hasher.noop(), ByteStringStrategy.copy(), segmentSize, quantum),
+          BufferHandle.allocate(segmentSize),
+          BufferHandle.allocate(segmentSize),
+          failuresQueue,
+          ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest.recoveryFileManager,
+          dataFrames);
+    }
+
+    @Override
+    public String toString() {
+      return toString;
+    }
+  }
+
+  private static final class UploadId {
+    private final String id;
+
+    private UploadId(String id) {
+      this.id = id;
+    }
+
+    private static UploadId of(String id) {
+      return new UploadId(id);
+    }
+
+    @Override
+    public boolean equals(java.lang.Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof UploadId)) {
+        return false;
+      }
+      UploadId uploadId = (UploadId) o;
+      return Objects.equals(id, uploadId.id);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("id", id).toString();
+    }
+  }
+
+  private static final class UploadCtx {
+    private final UploadId id;
+    private final StartResumableWriteRequest req;
+
+    private final List<WriteObjectRequest> parts;
+
+    private FailuresQueue failuresQueue;
+    private long length;
+
+    private UploadCtx(UploadId id, StartResumableWriteRequest req) {
+      this.id = id;
+      this.req = req;
+      this.parts = Collections.synchronizedList(new ArrayList<>());
+    }
+
+    public static UploadCtx of(UploadId id, StartResumableWriteRequest req) {
+      return new UploadCtx(id, req);
+    }
+
+    UploadId getId() {
+      return id;
+    }
+
+    StartResumableWriteRequest getReq() {
+      return req;
+    }
+
+    void addPart(WriteObjectRequest req) {
+      length += req.getChecksummedData().getContent().size();
+      parts.add(req);
+    }
+
+    long getLength() {
+      return length;
+    }
+
+    boolean finishWrite() {
+      if (!parts.isEmpty()) {
+        return parts.get(parts.size() - 1).getFinishWrite();
+      } else {
+        return false;
+      }
+    }
+
+    public Code consume(WriteObjectRequest req) {
+      if (failuresQueue != null) {
+        FailureOffset peek = failuresQueue.pending.peekFirst();
+        if (peek != null) {
+          if (req.hasChecksummedData()) {
+            long writeOffset = req.getWriteOffset();
+            ByteString content = req.getChecksummedData().getContent();
+            int size = content.size();
+            boolean applies = writeOffset <= peek.offset && peek.offset < writeOffset + size;
+            if (applies) {
+              int subLength = Math.toIntExact(Math.subtractExact(peek.offset, writeOffset));
+              ByteString substring = content.substring(0, subLength);
+              WriteObjectRequest.Builder b = req.toBuilder();
+              b.getChecksummedDataBuilder().setContent(substring);
+              b.clearFinishWrite();
+              failuresQueue.pending.pop();
+              length += substring.size();
+              parts.add(b.build());
+              return peek.getStatus();
+            }
+          }
+        }
+      }
+      addPart(req);
+      return Code.OK;
+    }
+  }
+
+  private static final class FailuresQueue {
+
+    private final List<FailureOffset> statuses;
+
+    private final Deque<FailureOffset> pending;
+
+    private FailuresQueue(List<FailureOffset> statuses) {
+      this.statuses = ImmutableList.sortedCopyOf(FailureOffset.COMP, statuses);
+      ArrayDeque<FailureOffset> tmp = new ArrayDeque<>();
+      this.statuses.forEach(tmp::addLast);
+      this.pending = tmp;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("statuses", statuses).toString();
+    }
+
+    static FailuresQueue empty() {
+      return new FailuresQueue(ImmutableList.of());
+    }
+  }
+
+  private static final class FailureOffset implements Comparable<FailureOffset> {
+    private static final Comparator<FailureOffset> COMP =
+        Comparator.comparing(FailureOffset::getOffset);
+    private final long offset;
+    private final Status.Code status;
+
+    private FailureOffset(long offset, Code status) {
+      this.offset = offset;
+      this.status = status;
+    }
+
+    public long getOffset() {
+      return offset;
+    }
+
+    public Code getStatus() {
+      return status;
+    }
+
+    @Override
+    public int compareTo(FailureOffset o) {
+      return COMP.compare(this, o);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("offset", offset)
+          .add("status", status)
+          .toString();
+    }
+
+    private static FailureOffset of(long offset) {
+      return new FailureOffset(offset, Code.INTERNAL);
+    }
+  }
+
+  private static final class FailureInducingStorageImpl extends StorageImplBase {
+
+    private final Map<UploadId, UploadCtx> data;
+
+    public FailureInducingStorageImpl() {
+      this.data = Collections.synchronizedMap(new HashMap<>());
+    }
+
+    void reset() {
+      data.clear();
+    }
+
+    @Override
+    public void startResumableWrite(
+        StartResumableWriteRequest request,
+        StreamObserver<StartResumableWriteResponse> responseObserver) {
+      UploadId id = UploadId.of(UUID.randomUUID().toString());
+      data.put(id, UploadCtx.of(id, request));
+      StartResumableWriteResponse startResumableWriteResponse =
+          StartResumableWriteResponse.newBuilder().setUploadId(id.id).build();
+      responseObserver.onNext(startResumableWriteResponse);
+      responseObserver.onCompleted();
+    }
+
+    @Override
+    public StreamObserver<WriteObjectRequest> writeObject(
+        StreamObserver<WriteObjectResponse> responseObserver) {
+      return new FailureInducingWriteObjectRequestObserver(responseObserver, data);
+    }
+
+    @Override
+    public void queryWriteStatus(
+        QueryWriteStatusRequest queryWriteStatusRequest,
+        StreamObserver<QueryWriteStatusResponse> responseObserver) {
+      UploadId uploadId = UploadId.of(queryWriteStatusRequest.getUploadId());
+      UploadCtx ctx;
+      if (data.containsKey(uploadId)) {
+        ctx = data.get(uploadId);
+      } else {
+        responseObserver.onError(Code.NOT_FOUND.toStatus().asRuntimeException());
+        return;
+      }
+      QueryWriteStatusResponse.Builder b = QueryWriteStatusResponse.newBuilder();
+      if (ctx.finishWrite()) {
+        b.setResource(
+            ctx.getReq()
+                .getWriteObjectSpec()
+                .getResource()
+                .toBuilder()
+                .setSize(ctx.getLength())
+                .setGeneration(1)
+                .setMetageneration(1)
+                .build());
+      } else {
+        b.setPersistedSize(ctx.getLength());
+      }
+      QueryWriteStatusResponse queryWriteStatusResponse = b.build();
+      responseObserver.onNext(queryWriteStatusResponse);
+      responseObserver.onCompleted();
+    }
+  }
+
+  private static final class FailureInducingWriteObjectRequestObserver
+      implements StreamObserver<WriteObjectRequest> {
+    private final StreamObserver<WriteObjectResponse> responseObserver;
+    private final Map<UploadId, UploadCtx> data;
+
+    private UploadCtx ctx;
+    private boolean errored;
+
+    public FailureInducingWriteObjectRequestObserver(
+        StreamObserver<WriteObjectResponse> responseObserver, Map<UploadId, UploadCtx> data) {
+      this.data = data;
+      this.responseObserver = responseObserver;
+      this.ctx = null;
+      this.errored = false;
+    }
+
+    @Override
+    public void onNext(WriteObjectRequest writeObjectRequest) {
+      if (ctx == null) {
+        UploadId uploadId = UploadId.of(writeObjectRequest.getUploadId());
+        if (data.containsKey(uploadId)) {
+          ctx = data.get(uploadId);
+        } else {
+          errored = true;
+          responseObserver.onError(Code.NOT_FOUND.toStatus().asRuntimeException());
+          return;
+        }
+      }
+      Status.Code ret = ctx.consume(writeObjectRequest);
+      if (ret != Code.OK) {
+        errored = true;
+        responseObserver.onError(ret.toStatus().asRuntimeException());
+      }
+    }
+
+    @Override
+    public void onError(Throwable throwable) {
+      if (errored) {
+        return;
+      }
+      responseObserver.onError(throwable);
+    }
+
+    @Override
+    public void onCompleted() {
+      if (errored) {
+        return;
+      }
+      WriteObjectResponse resp =
+          WriteObjectResponse.newBuilder()
+              .setResource(
+                  ctx.getReq()
+                      .getWriteObjectSpec()
+                      .getResource()
+                      .toBuilder()
+                      .setSize(ctx.getLength())
+                      .setGeneration(1)
+                      .setMetageneration(1)
+                      .build())
+              .build();
+      responseObserver.onNext(resp);
+      responseObserver.onCompleted();
+    }
+  }
+
+  @FunctionalInterface
+  private interface ResultRetryAlgorithmAdapter extends ResultRetryAlgorithm<java.lang.Object> {
+
+    @Override
+    default TimedAttemptSettings createNextAttempt(
+        Throwable prevThrowable, java.lang.Object prevResponse, TimedAttemptSettings prevSettings) {
+      return null;
+    }
+  }
+
+  private static final class ForcedFailure extends RuntimeException {
+    public ForcedFailure(String message) {
+      super(message);
+    }
+  }
+}
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
index abf68893..584ab68b 100644
--- 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
@@ -16,7 +16,10 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.TestUtils.assertAll;
+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 com.google.common.collect.ImmutableList;
@@ -33,6 +36,7 @@ import java.nio.file.Path;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.Objects;
+import java.util.Random;
 import java.util.stream.Stream;
 import org.junit.Rule;
 import org.junit.Test;
@@ -143,4 +147,40 @@ public final class RecoveryFileManagerTest {
       assertThat(parentDirs).isEqualTo(ImmutableSet.of(tempDir1, tempDir2, tempDir3));
     }
   }
+
+  @Test
+  public void multipleRecoveryFilesForEqualBlobInfoAreAbleToExistConcurrently() throws Exception {
+    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 rf1 = rfm.newRecoveryFile(info);
+        RecoveryFile rf2 = rfm.newRecoveryFile(info); ) {
+
+      Random rand = new Random(467123);
+      byte[] bytes1 = DataGenerator.rand(rand).genBytes(7);
+      byte[] bytes2 = DataGenerator.rand(rand).genBytes(41);
+      try (WritableByteChannel writer = rf1.writer()) {
+        writer.write(ByteBuffer.wrap(bytes1));
+      }
+      try (WritableByteChannel writer = rf2.writer()) {
+        writer.write(ByteBuffer.wrap(bytes2));
+      }
+
+      byte[] actual1 = ByteStreams.toByteArray(Files.newInputStream(rf1.getPath()));
+      byte[] actual2 = ByteStreams.toByteArray(Files.newInputStream(rf2.getPath()));
+
+      String expected1 = xxd(bytes1);
+      String expected2 = xxd(bytes2);
+
+      String xxd1 = xxd(actual1);
+      String xxd2 = xxd(actual2);
+      assertAll(
+          () -> assertWithMessage("rf1 should contain bytes1").that(xxd1).isEqualTo(expected1),
+          () -> assertWithMessage("rf2 should contain bytes2").that(xxd2).isEqualTo(expected2));
+    }
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMappings.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMappings.java
index cc14b21e..dbd0e925 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMappings.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMappings.java
@@ -1126,8 +1126,9 @@ final class RpcMethodMappings {
                     (ctx, c) ->
                         ctx.map(
                             state -> {
-                              boolean success =
-                                  ctx.getStorage().delete(state.getBlob().getBlobId());
+                              BlobId id = state.getBlob().getBlobId();
+                              BlobId idWithoutGeneration = BlobId.of(id.getBucket(), id.getName());
+                              boolean success = ctx.getStorage().delete(idWithoutGeneration);
                               assertTrue(success);
                               return state.with(success);
                             }))
@@ -1169,7 +1170,17 @@ final class RpcMethodMappings {
         a.add(
             RpcMethodMapping.newBuilder(67, objects.delete)
                 .withApplicable(not(TestRetryConformance::isPreconditionsProvided))
-                .withTest((ctx, c) -> ctx.peek(state -> state.getBlob().delete()))
+                .withTest(
+                    (ctx, c) ->
+                        ctx.peek(
+                            state -> {
+                              Blob blob = state.getBlob();
+                              Blob blobWithoutGeneration =
+                                  blob.toBuilder()
+                                      .setBlobId(BlobId.of(blob.getBucket(), blob.getName()))
+                                      .build();
+                              blobWithoutGeneration.delete();
+                            }))
                 .build());
         a.add(
             RpcMethodMapping.newBuilder(68, objects.delete)