This is an automated email from the ASF dual-hosted git repository.
shunping pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new edd3bfca48e Fix flaky GcsIOIntegrationTest and handle GCS bucket
creation race conditions (#39130)
edd3bfca48e is described below
commit edd3bfca48e50b6fcd97bf48ff74434daa17dbec
Author: Shunping Huang <[email protected]>
AuthorDate: Mon Jun 29 09:37:49 2026 -0400
Fix flaky GcsIOIntegrationTest and handle GCS bucket creation race
conditions (#39130)
* Fix flaky GcsIOIntegrationTest and handle GCS bucket creation race
conditions
* Modify get_or_create_default_gcs_bucket in gcsio.py to catch Conflict
(409)
exceptions, resolving to the existing bucket on concurrent creation
requests.
* Add unit tests for default bucket creation conflict handling in
gcsio_test.py.
* Modify test_create_default_bucket in gcsio_integration_test.py to generate
unique bucket names with a UUID prefix and test parameters to eliminate
parallel
test thread collisions.
* Fix lints
* Trigger postcommit tests.
* Address reviews
---
.github/trigger_files/beam_PostCommit_Python.json | 2 +-
sdks/python/apache_beam/io/gcp/gcsio.py | 13 ++++++-
.../apache_beam/io/gcp/gcsio_integration_test.py | 21 ++++++------
sdks/python/apache_beam/io/gcp/gcsio_test.py | 40 ++++++++++++++++++++++
4 files changed, 63 insertions(+), 13 deletions(-)
diff --git a/.github/trigger_files/beam_PostCommit_Python.json
b/.github/trigger_files/beam_PostCommit_Python.json
index 2e255c8f3cf..2bb052d5f71 100644
--- a/.github/trigger_files/beam_PostCommit_Python.json
+++ b/.github/trigger_files/beam_PostCommit_Python.json
@@ -1,5 +1,5 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run.",
"pr": "37345",
- "modification": 52
+ "modification": 53
}
diff --git a/sdks/python/apache_beam/io/gcp/gcsio.py
b/sdks/python/apache_beam/io/gcp/gcsio.py
index 7cc68fd1e4a..179525de2cb 100644
--- a/sdks/python/apache_beam/io/gcp/gcsio.py
+++ b/sdks/python/apache_beam/io/gcp/gcsio.py
@@ -35,6 +35,7 @@ import time
from typing import Optional
from typing import Union
+from google.api_core.exceptions import Conflict
from google.api_core.exceptions import RetryError
from google.cloud import storage
from google.cloud.exceptions import NotFound
@@ -143,7 +144,17 @@ def get_or_create_default_gcs_bucket(options):
'Creating default GCS bucket for project %s: gs://%s',
project,
bucket_name)
- return gcs.create_bucket(bucket_name, project, location=region)
+ try:
+ return gcs.create_bucket(bucket_name, project, location=region)
+ except Conflict:
+ bucket = gcs.get_bucket(bucket_name)
+ if bucket:
+ _validate_bucket_project(
+ bucket,
+ project,
+ credentials=getattr(gcs.client, '_credentials', None))
+ return bucket
+ raise
def create_storage_client(pipeline_options, use_credentials=True):
diff --git a/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py
b/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py
index f5da9b60dbd..a68b1179faf 100644
--- a/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py
+++ b/sdks/python/apache_beam/io/gcp/gcsio_integration_test.py
@@ -33,6 +33,7 @@ import time
import unittest
import uuid
import zlib
+from hashlib import blake2b
import mock
import pytest
@@ -207,19 +208,17 @@ class GcsIOIntegrationTest(unittest.TestCase):
# requires this option unset.
google_cloud_options.dataflow_kms_key = None
- import random
- from hashlib import blake2b
-
- # Add a random number to avoid collision if multiple test instances
- # are run at the same time. To avoid too many dangling buckets if bucket
- # removal fails, we limit the max number of possible bucket names in this
- # test to 1000.
- overridden_bucket_name = 'gcsio-it-%d-%s-%s-%d' % (
- random.randint(0, 999),
+ # Add a unique uuid and the parameterized test options to the bucket name
+ # to avoid collisions when multiple parameterized instances run in parallel
+ # or concurrent CI jobs run at the same time.
+ overridden_bucket_name = 'gcsio-it-%s-%s-%s-%d-%s-%s' % (
+ uuid.uuid4().hex[:6],
google_cloud_options.region,
blake2b(google_cloud_options.project.encode('utf8'),
- digest_size=4).hexdigest(),
- int(time.time()))
+ digest_size=2).hexdigest(),
+ int(time.time()),
+ '1' if self.no_gcsio_throttling_counter else '0',
+ '1' if self.enable_gcsio_blob_generation else '0')
mock_default_gcs_bucket_name.return_value = overridden_bucket_name
diff --git a/sdks/python/apache_beam/io/gcp/gcsio_test.py
b/sdks/python/apache_beam/io/gcp/gcsio_test.py
index 9c4414175e4..ddb94f9c17f 100644
--- a/sdks/python/apache_beam/io/gcp/gcsio_test.py
+++ b/sdks/python/apache_beam/io/gcp/gcsio_test.py
@@ -487,6 +487,46 @@ class TestGCSIO(unittest.TestCase):
self.assertEqual(bucket, mock_bucket)
mock_crm_class.assert_not_called()
+ @mock.patch('google.cloud.resourcemanager_v3.ProjectsClient')
+ @mock.patch('apache_beam.io.gcp.gcsio.GcsIO')
+ def test_get_or_create_default_gcs_bucket_conflict(
+ self, mock_gcsio_class, mock_crm_class):
+ mock_gcsio = mock_gcsio_class.return_value
+ mock_bucket = mock.Mock()
+ mock_bucket.project_number = 123456789
+ mock_gcsio.get_bucket.side_effect = [None, mock_bucket]
+
+ from google.api_core.exceptions import Conflict
+ mock_gcsio.create_bucket.side_effect = Conflict("Already owned by you")
+
+ mock_crm_client = mock_crm_class.return_value
+ mock_project_info = mock.Mock()
+ mock_project_info.name = 'projects/123456789'
+ mock_crm_client.get_project.return_value = mock_project_info
+
+ options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1')
+ bucket = gcsio.get_or_create_default_gcs_bucket(options)
+
+ self.assertEqual(bucket, mock_bucket)
+ self.assertEqual(mock_gcsio.get_bucket.call_count, 2)
+ mock_gcsio.create_bucket.assert_called_once()
+
+ @mock.patch('google.cloud.resourcemanager_v3.ProjectsClient')
+ @mock.patch('apache_beam.io.gcp.gcsio.GcsIO')
+ def test_get_or_create_default_gcs_bucket_conflict_reraise(
+ self, mock_gcsio_class, mock_crm_class):
+ mock_gcsio = mock_gcsio_class.return_value
+ mock_gcsio.get_bucket.side_effect = [None, None]
+
+ from google.api_core.exceptions import Conflict
+ mock_gcsio.create_bucket.side_effect = Conflict("Bucket name unavailable")
+
+ options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1')
+ with self.assertRaises(Conflict):
+ gcsio.get_or_create_default_gcs_bucket(options)
+
+ self.assertEqual(mock_gcsio.get_bucket.call_count, 2)
+
def test_exists(self):
file_name = 'gs://gcsio-test/dummy_file'
file_size = 1234