Skip to content

Commit edd3bfc

Browse files
authored
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
1 parent 31cd92f commit edd3bfc

4 files changed

Lines changed: 63 additions & 13 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run.",
33
"pr": "37345",
4-
"modification": 52
4+
"modification": 53
55
}

sdks/python/apache_beam/io/gcp/gcsio.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from typing import Optional
3636
from typing import Union
3737

38+
from google.api_core.exceptions import Conflict
3839
from google.api_core.exceptions import RetryError
3940
from google.cloud import storage
4041
from google.cloud.exceptions import NotFound
@@ -143,7 +144,17 @@ def get_or_create_default_gcs_bucket(options):
143144
'Creating default GCS bucket for project %s: gs://%s',
144145
project,
145146
bucket_name)
146-
return gcs.create_bucket(bucket_name, project, location=region)
147+
try:
148+
return gcs.create_bucket(bucket_name, project, location=region)
149+
except Conflict:
150+
bucket = gcs.get_bucket(bucket_name)
151+
if bucket:
152+
_validate_bucket_project(
153+
bucket,
154+
project,
155+
credentials=getattr(gcs.client, '_credentials', None))
156+
return bucket
157+
raise
147158

148159

149160
def create_storage_client(pipeline_options, use_credentials=True):

sdks/python/apache_beam/io/gcp/gcsio_integration_test.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import unittest
3434
import uuid
3535
import zlib
36+
from hashlib import blake2b
3637

3738
import mock
3839
import pytest
@@ -207,19 +208,17 @@ def test_create_default_bucket(self, mock_default_gcs_bucket_name):
207208
# requires this option unset.
208209
google_cloud_options.dataflow_kms_key = None
209210

210-
import random
211-
from hashlib import blake2b
212-
213-
# Add a random number to avoid collision if multiple test instances
214-
# are run at the same time. To avoid too many dangling buckets if bucket
215-
# removal fails, we limit the max number of possible bucket names in this
216-
# test to 1000.
217-
overridden_bucket_name = 'gcsio-it-%d-%s-%s-%d' % (
218-
random.randint(0, 999),
211+
# Add a unique uuid and the parameterized test options to the bucket name
212+
# to avoid collisions when multiple parameterized instances run in parallel
213+
# or concurrent CI jobs run at the same time.
214+
overridden_bucket_name = 'gcsio-it-%s-%s-%s-%d-%s-%s' % (
215+
uuid.uuid4().hex[:6],
219216
google_cloud_options.region,
220217
blake2b(google_cloud_options.project.encode('utf8'),
221-
digest_size=4).hexdigest(),
222-
int(time.time()))
218+
digest_size=2).hexdigest(),
219+
int(time.time()),
220+
'1' if self.no_gcsio_throttling_counter else '0',
221+
'1' if self.enable_gcsio_blob_generation else '0')
223222

224223
mock_default_gcs_bucket_name.return_value = overridden_bucket_name
225224

sdks/python/apache_beam/io/gcp/gcsio_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,46 @@ def test_get_or_create_default_gcs_bucket_ownership_mock_project_number(
487487
self.assertEqual(bucket, mock_bucket)
488488
mock_crm_class.assert_not_called()
489489

490+
@mock.patch('google.cloud.resourcemanager_v3.ProjectsClient')
491+
@mock.patch('apache_beam.io.gcp.gcsio.GcsIO')
492+
def test_get_or_create_default_gcs_bucket_conflict(
493+
self, mock_gcsio_class, mock_crm_class):
494+
mock_gcsio = mock_gcsio_class.return_value
495+
mock_bucket = mock.Mock()
496+
mock_bucket.project_number = 123456789
497+
mock_gcsio.get_bucket.side_effect = [None, mock_bucket]
498+
499+
from google.api_core.exceptions import Conflict
500+
mock_gcsio.create_bucket.side_effect = Conflict("Already owned by you")
501+
502+
mock_crm_client = mock_crm_class.return_value
503+
mock_project_info = mock.Mock()
504+
mock_project_info.name = 'projects/123456789'
505+
mock_crm_client.get_project.return_value = mock_project_info
506+
507+
options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1')
508+
bucket = gcsio.get_or_create_default_gcs_bucket(options)
509+
510+
self.assertEqual(bucket, mock_bucket)
511+
self.assertEqual(mock_gcsio.get_bucket.call_count, 2)
512+
mock_gcsio.create_bucket.assert_called_once()
513+
514+
@mock.patch('google.cloud.resourcemanager_v3.ProjectsClient')
515+
@mock.patch('apache_beam.io.gcp.gcsio.GcsIO')
516+
def test_get_or_create_default_gcs_bucket_conflict_reraise(
517+
self, mock_gcsio_class, mock_crm_class):
518+
mock_gcsio = mock_gcsio_class.return_value
519+
mock_gcsio.get_bucket.side_effect = [None, None]
520+
521+
from google.api_core.exceptions import Conflict
522+
mock_gcsio.create_bucket.side_effect = Conflict("Bucket name unavailable")
523+
524+
options = SampleOptions(DEFAULT_GCP_PROJECT, 'us-central1')
525+
with self.assertRaises(Conflict):
526+
gcsio.get_or_create_default_gcs_bucket(options)
527+
528+
self.assertEqual(mock_gcsio.get_bucket.call_count, 2)
529+
490530
def test_exists(self):
491531
file_name = 'gs://gcsio-test/dummy_file'
492532
file_size = 1234

0 commit comments

Comments
 (0)