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 eef26ca8b71 Revert "[Python] Optimize BigQuery copy jobs in file loads
using multi-source copy" (#39106)
eef26ca8b71 is described below
commit eef26ca8b71c4187282b11898804d643da0b136f
Author: Shunping Huang <[email protected]>
AuthorDate: Thu Jun 25 16:49:02 2026 -0400
Revert "[Python] Optimize BigQuery copy jobs in file loads using
multi-source copy" (#39106)
* Revert "[Python] Optimize BigQuery copy jobs in file loads using
multi-source copy (#38983)"
This reverts commit 8e4ea736213d07710c4e64eeed80fe5971689f6b.
* Trigger postcommit test.
---
.github/trigger_files/beam_PostCommit_Python.json | 2 +-
.../apache_beam/io/gcp/bigquery_file_loads.py | 162 ++++++++--------
.../apache_beam/io/gcp/bigquery_file_loads_test.py | 205 +++++----------------
sdks/python/apache_beam/io/gcp/bigquery_tools.py | 18 +-
4 files changed, 136 insertions(+), 251 deletions(-)
diff --git a/.github/trigger_files/beam_PostCommit_Python.json
b/.github/trigger_files/beam_PostCommit_Python.json
index c03ecf71f04..98bf4bf9500 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": 50
+ "modification": 51
}
diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py
b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py
index 4ef6c392254..4e45d0324ee 100644
--- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py
+++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py
@@ -491,8 +491,6 @@ class TriggerCopyJobs(beam.DoFn):
"""
TRIGGER_DELETE_TEMP_TABLES = 'TriggerDeleteTempTables'
- # https://docs.cloud.google.com/bigquery/quotas#copy_jobs
- MAX_SOURCES_PER_COPY_JOB = 1200
def __init__(
self,
@@ -530,90 +528,96 @@ class TriggerCopyJobs(beam.DoFn):
self, element_list, job_name_prefix=None, unused_schema_mod_jobs=None):
if isinstance(element_list, tuple):
# Allow this for streaming update compatibility while fixing BEAM-24535.
- element_list = [element_list]
+ self.process_one(element_list, job_name_prefix)
+ else:
+ for element in element_list:
+ self.process_one(element, job_name_prefix)
- if not element_list:
- return
+ def process_one(self, element, job_name_prefix):
+ destination, job_reference = element
- first_destination = element_list[0][0]
- copy_to_reference = bigquery_tools.parse_table_reference(first_destination)
+ copy_to_reference = bigquery_tools.parse_table_reference(destination)
if copy_to_reference.projectId is None:
copy_to_reference.projectId = vp.RuntimeValueProvider.get_value(
'project', str, '') or self.project
- copy_from_references = []
- for destination, job_reference in element_list:
- copy_from_reference = bigquery_tools.parse_table_reference(destination)
- copy_from_reference.tableId = job_reference.jobId
- if copy_from_reference.projectId is None:
- copy_from_reference.projectId = vp.RuntimeValueProvider.get_value(
- 'project', str, '') or self.project
- copy_from_references.append(copy_from_reference)
+ copy_from_reference = bigquery_tools.parse_table_reference(destination)
+ copy_from_reference.tableId = job_reference.jobId
+ if copy_from_reference.projectId is None:
+ copy_from_reference.projectId = vp.RuntimeValueProvider.get_value(
+ 'project', str, '') or self.project
- full_table_ref = bigquery_tools.get_hashable_destination(copy_to_reference)
+ _LOGGER.info(
+ "Triggering copy job from %s to %s",
+ copy_from_reference,
+ copy_to_reference)
- is_first_time = full_table_ref not in self._observed_tables
- if is_first_time:
- self._observed_tables.add(full_table_ref)
- if self.bq_io_metadata:
- Lineage.sinks().add(
- 'bigquery',
- copy_to_reference.projectId,
- copy_to_reference.datasetId,
- copy_to_reference.tableId)
-
- # Split into chunks of MAX_SOURCES_PER_COPY_JOB
- chunks = [
- copy_from_references[i:i + self.MAX_SOURCES_PER_COPY_JOB]
- for i in range(
- 0, len(copy_from_references), self.MAX_SOURCES_PER_COPY_JOB)
- ]
-
- copy_job_name_base = '%s_%s' % (
- job_name_prefix,
- _bq_uuid(bigquery_tools.get_hashable_destination(copy_to_reference)))
+ wait_for_job, write_disposition = (
+ self._determine_write_disposition(copy_to_reference))
+
+ if not self.bq_io_metadata:
+ self.bq_io_metadata = create_bigquery_io_metadata(self._step_name)
project_id = (
copy_to_reference.projectId
if self.load_job_project_id is None else self.load_job_project_id)
+ copy_job_name = '%s_%s' % (
+ job_name_prefix,
+ _bq_uuid(
+ '%s:%s.%s' % (
+ copy_from_reference.projectId,
+ copy_from_reference.datasetId,
+ copy_from_reference.tableId)))
+ job_reference = self.bq_wrapper._insert_copy_job(
+ project_id,
+ copy_job_name,
+ copy_from_reference,
+ copy_to_reference,
+ create_disposition=self.create_disposition,
+ write_disposition=write_disposition,
+ job_labels=self.bq_io_metadata.add_additional_bq_job_labels())
+
+ if wait_for_job:
+ self.bq_wrapper.wait_for_bq_job(job_reference, sleep_duration_sec=10)
+ self.pending_jobs.append(
+ GlobalWindows.windowed_value((destination, job_reference)))
- for i, chunk in enumerate(chunks):
- if i == 0 and is_first_time:
- write_disposition = self.write_disposition
- # Wait inline only if we have multiple chunks and write disposition is
WRITE_TRUNCATE or WRITE_EMPTY.
- # This ensures the first chunk initializes the table, and subsequent
chunks (WRITE_APPEND) append to it.
- wait_for_job = (
- self.write_disposition in ('WRITE_TRUNCATE', 'WRITE_EMPTY') and
- len(chunks) > 1)
- else:
- write_disposition = 'WRITE_APPEND'
- wait_for_job = False
-
- chunk_job_name = copy_job_name_base
- if len(chunks) > 1:
- chunk_job_name = f"{copy_job_name_base}_{i}"
-
- _LOGGER.info(
- "Triggering copy job %s from %s to %s (write_disposition: %s)",
- chunk_job_name, [str(r) for r in chunk],
- copy_to_reference,
- write_disposition)
-
- job_reference = self.bq_wrapper._insert_copy_job(
- project_id,
- chunk_job_name,
- chunk,
- copy_to_reference,
- create_disposition=self.create_disposition,
- write_disposition=write_disposition,
- job_labels=self.bq_io_metadata.add_additional_bq_job_labels()
- if self.bq_io_metadata else None)
-
- if wait_for_job:
- self.bq_wrapper.wait_for_bq_job(job_reference, sleep_duration_sec=10)
-
- self.pending_jobs.append(
- GlobalWindows.windowed_value((first_destination, job_reference)))
+ def _determine_write_disposition(self, copy_to_reference) -> tuple[bool,
str]:
+ """
+ Determines the write disposition for a BigQuery copy job,
+ based on destination.
+
+ When the write_disposition for a job is WRITE_TRUNCATE, multiple copy jobs
+ to the same destination can interfere with each other, truncate data, and
+ write to the BigQuery table repeatedly. To prevent this, the first copy job
+ runs with the user's specified write_disposition, but subsequent jobs must
+ always use WRITE_APPEND. This ensures that subsequent copy jobs do not
+ clear out data appended by previous jobs.
+
+ Args:
+ copy_to_reference: The reference to the destination table.
+
+ Returns:
+ A tuple containing a boolean indicating whether to wait for the job to
+ complete and the write disposition to use for the job.
+ """
+ full_table_ref = '%s:%s.%s' % (
+ copy_to_reference.projectId,
+ copy_to_reference.datasetId,
+ copy_to_reference.tableId)
+ if full_table_ref not in self._observed_tables:
+ write_disposition = self.write_disposition
+ wait_for_job = True
+ self._observed_tables.add(full_table_ref)
+ Lineage.sinks().add(
+ 'bigquery',
+ copy_to_reference.projectId,
+ copy_to_reference.datasetId,
+ copy_to_reference.tableId)
+ else:
+ wait_for_job = False
+ write_disposition = 'WRITE_APPEND'
+ return wait_for_job, write_disposition
def finish_bundle(self):
for windowed_value in self.pending_jobs:
@@ -740,7 +744,7 @@ class TriggerLoadJobs(beam.DoFn):
else:
try:
schema = bigquery_tools.table_schema_to_dict(
- self.bq_wrapper.get_table(
+ bigquery_tools.BigQueryWrapper().get_table(
project_id=table_reference.projectId,
dataset_id=table_reference.datasetId,
table_id=table_reference.tableId).schema)
@@ -851,8 +855,7 @@ class PartitionFiles(beam.DoFn):
if latest_partition.can_accept(file_size):
latest_partition.add(file_path, file_size)
else:
- if latest_partition.files:
- partitions.append(latest_partition.files)
+ partitions.append(latest_partition.files)
latest_partition = PartitionFiles.Partition(
self.max_partition_size, self.max_files_per_partition)
latest_partition.add(file_path, file_size)
@@ -1178,13 +1181,12 @@ class BigQueryBatchFileLoads(beam.PTransform):
# the truncation happens only once. See
# https://github.com/apache/beam/issues/24535.
finished_temp_tables_load_job_ids_list_pc = (
- finished_temp_tables_load_job_ids_pc
- | beam.MapTuple(
+ finished_temp_tables_load_job_ids_pc | beam.MapTuple(
lambda destination, job_reference: (
- bigquery_tools.get_hashable_destination(destination),
+ bigquery_tools.parse_table_reference(destination).tableId,
(destination, job_reference)))
| beam.GroupByKey()
- | beam.MapTuple(lambda dest, batch: list(batch)))
+ | beam.MapTuple(lambda tableId, batch: list(batch)))
else:
# Loads can happen in parallel.
finished_temp_tables_load_job_ids_list_pc = (
diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py
b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py
index 47c1ce5ea1b..191719e6a20 100644
--- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py
+++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py
@@ -924,180 +924,69 @@ class TestBigQueryFileLoads(_TestCaseWithTempDirCleanUp):
write_disposition=BigQueryDisposition.WRITE_TRUNCATE))
from apache_beam.io.gcp.internal.clients.bigquery import TableReference
- mock_insert_copy_job.assert_has_calls([
- call(
- 'project1',
- mock.ANY,
- [
+ mock_insert_copy_job.assert_has_calls(
+ [
+ call(
+ 'project1',
+ mock.ANY,
TableReference(
datasetId='dataset1',
projectId='project1',
tableId='job_name1'),
+ TableReference(
+ datasetId='dataset1',
+ projectId='project1',
+ tableId='table1'),
+ create_disposition=None,
+ write_disposition='WRITE_TRUNCATE',
+ job_labels={'step_name': 'bigquerybatchfileloads'}),
+ call(
+ 'project1',
+ mock.ANY,
TableReference(
datasetId='dataset1',
projectId='project1',
tableId='job_name1'),
- ],
- TableReference(
- datasetId='dataset1', projectId='project1', tableId='table1'),
- create_disposition=None,
- write_disposition='WRITE_TRUNCATE',
- job_labels={'step_name': 'bigquerybatchfileloads'}),
- call(
- 'project1',
- mock.ANY,
- [
+ TableReference(
+ datasetId='dataset1',
+ projectId='project1',
+ tableId='table1'),
+ create_disposition=None,
+ write_disposition='WRITE_APPEND',
+ job_labels={'step_name': 'bigquerybatchfileloads'}),
+ call(
+ 'project1',
+ mock.ANY,
TableReference(
datasetId='dataset2',
projectId='project1',
tableId='job_name1'),
- ],
- TableReference(
- datasetId='dataset2', projectId='project1', tableId='table1'),
- create_disposition=None,
- write_disposition='WRITE_TRUNCATE',
- job_labels={'step_name': 'bigquerybatchfileloads'}),
- call(
- 'project1',
- mock.ANY,
- [
+ TableReference(
+ datasetId='dataset2',
+ projectId='project1',
+ tableId='table1'),
+ create_disposition=None,
+ # Previously this was `WRITE_APPEND`.
+ write_disposition='WRITE_TRUNCATE',
+ job_labels={'step_name': 'bigquerybatchfileloads'}),
+ call(
+ 'project1',
+ mock.ANY,
TableReference(
datasetId='dataset3',
projectId='project1',
tableId='job_name1'),
- ],
- TableReference(
- datasetId='dataset3', projectId='project1', tableId='table1'),
- create_disposition=None,
- write_disposition='WRITE_TRUNCATE',
- job_labels={'step_name': 'bigquerybatchfileloads'}),
- ],
- any_order=True)
- self.assertEqual(3, mock_insert_copy_job.call_count)
-
- @mock.patch(
- 'apache_beam.io.gcp.bigquery_tools.BigQueryWrapper.wait_for_bq_job')
- @mock.patch(
- 'apache_beam.io.gcp.bigquery_tools.BigQueryWrapper._insert_copy_job')
- def test_copy_jobs_splitting(
- self, mock_insert_copy_job, mock_wait_for_bq_job):
- destination = 'project1:dataset1.table1'
-
- from apache_beam.io.gcp.bigquery_file_loads import TriggerCopyJobs
- original_max_sources = TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB
- TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB = 2
-
- try:
- job_reference = bigquery_api.JobReference()
- job_reference.projectId = 'project1'
- job_reference.jobId = 'job_name1'
- result_job = mock.Mock()
- result_job.jobReference = job_reference
-
- mock_job = mock.Mock()
- mock_job.status.state = 'DONE'
- mock_job.status.errorResult = None
- mock_job.jobReference = job_reference
-
- bq_client = mock.Mock()
- bq_client.jobs.Get.return_value = mock_job
- bq_client.jobs.Insert.return_value = result_job
- bq_client.tables.Delete.return_value = None
- mock_insert_copy_job.return_value = job_reference
- temp_dir = self._new_tempdir()
-
- with TestPipeline('FnApiRunner') as p:
- _ = (
- p
- | beam.Create([
- {
- 'name': 'a'
- },
- {
- 'name': 'b'
- },
- {
- 'name': 'c'
- },
- {
- 'name': 'd'
- },
- {
- 'name': 'e'
- },
- ],
- reshuffle=False)
- | bqfl.BigQueryBatchFileLoads(
- destination,
- custom_gcs_temp_location=temp_dir,
- test_client=bq_client,
- validate=False,
- temp_file_format=bigquery_tools.FileFormat.JSON,
- max_file_size=10,
- max_partition_size=10,
- max_files_per_partition=1,
- write_disposition=BigQueryDisposition.WRITE_TRUNCATE))
-
- self.assertEqual(3, mock_insert_copy_job.call_count)
-
- from apache_beam.io.gcp.internal.clients.bigquery import TableReference
- expected_calls = [
- call(
- 'project1',
- mock.ANY,
- [
- TableReference(
- datasetId='dataset1',
- projectId='project1',
- tableId='job_name1'),
- TableReference(
- datasetId='dataset1',
- projectId='project1',
- tableId='job_name1'),
- ],
- TableReference(
- datasetId='dataset1', projectId='project1',
tableId='table1'),
- create_disposition=None,
- write_disposition='WRITE_TRUNCATE',
- job_labels=mock.ANY),
- call(
- 'project1',
- mock.ANY,
- [
- TableReference(
- datasetId='dataset1',
- projectId='project1',
- tableId='job_name1'),
- TableReference(
- datasetId='dataset1',
- projectId='project1',
- tableId='job_name1'),
- ],
- TableReference(
- datasetId='dataset1', projectId='project1',
tableId='table1'),
- create_disposition=None,
- write_disposition='WRITE_APPEND',
- job_labels=mock.ANY),
- call(
- 'project1',
- mock.ANY,
- [
- TableReference(
- datasetId='dataset1',
- projectId='project1',
- tableId='job_name1'),
- ],
- TableReference(
- datasetId='dataset1', projectId='project1',
tableId='table1'),
- create_disposition=None,
- write_disposition='WRITE_APPEND',
- job_labels=mock.ANY),
- ]
- mock_insert_copy_job.assert_has_calls(expected_calls, any_order=True)
- self.assertEqual(9, mock_wait_for_bq_job.call_count)
-
- finally:
- TriggerCopyJobs.MAX_SOURCES_PER_COPY_JOB = original_max_sources
+ TableReference(
+ datasetId='dataset3',
+ projectId='project1',
+ tableId='table1'),
+ create_disposition=None,
+ # Previously this was `WRITE_APPEND`.
+ write_disposition='WRITE_TRUNCATE',
+ job_labels={'step_name': 'bigquerybatchfileloads'}),
+ ],
+ any_order=True)
+ self.assertEqual(4, mock_insert_copy_job.call_count)
@parameterized.expand([
param(is_streaming=False, with_auto_sharding=False, compat_version=None),
diff --git a/sdks/python/apache_beam/io/gcp/bigquery_tools.py
b/sdks/python/apache_beam/io/gcp/bigquery_tools.py
index 491b7a39b0b..8dd58cd55a0 100644
--- a/sdks/python/apache_beam/io/gcp/bigquery_tools.py
+++ b/sdks/python/apache_beam/io/gcp/bigquery_tools.py
@@ -506,22 +506,16 @@ class BigQueryWrapper(object):
reference = bigquery.JobReference()
reference.jobId = job_id
reference.projectId = project_id
-
- copy_config = bigquery.JobConfigurationTableCopy(
- destinationTable=to_table_reference,
- createDisposition=create_disposition,
- writeDisposition=write_disposition,
- )
- if isinstance(from_table_reference, list):
- copy_config.sourceTables = from_table_reference
- else:
- copy_config.sourceTable = from_table_reference
-
request = bigquery.BigqueryJobsInsertRequest(
projectId=project_id,
job=bigquery.Job(
configuration=bigquery.JobConfiguration(
- copy=copy_config,
+ copy=bigquery.JobConfigurationTableCopy(
+ destinationTable=to_table_reference,
+ sourceTable=from_table_reference,
+ createDisposition=create_disposition,
+ writeDisposition=write_disposition,
+ ),
labels=_build_job_labels(job_labels),
),
jobReference=reference,