@@ -491,6 +491,7 @@ class TriggerCopyJobs(beam.DoFn):
491491 """
492492
493493 TRIGGER_DELETE_TEMP_TABLES = 'TriggerDeleteTempTables'
494+ MAX_SOURCES_PER_COPY_JOB = 1200
494495
495496 def __init__ (
496497 self ,
@@ -528,97 +529,97 @@ def process(
528529 self , element_list , job_name_prefix = None , unused_schema_mod_jobs = None ):
529530 if isinstance (element_list , tuple ):
530531 # Allow this for streaming update compatibility while fixing BEAM-24535.
531- self .process_one (element_list , job_name_prefix )
532- else :
533- for element in element_list :
534- self .process_one (element , job_name_prefix )
532+ element_list = [element_list ]
535533
536- def process_one ( self , element , job_name_prefix ) :
537- destination , job_reference = element
534+ if not element_list :
535+ return
538536
539- copy_to_reference = bigquery_tools .parse_table_reference (destination )
537+ first_destination = element_list [0 ][0 ]
538+ copy_to_reference = bigquery_tools .parse_table_reference (first_destination )
540539 if copy_to_reference .projectId is None :
541540 copy_to_reference .projectId = vp .RuntimeValueProvider .get_value (
542541 'project' , str , '' ) or self .project
543542
544- copy_from_reference = bigquery_tools .parse_table_reference (destination )
545- copy_from_reference .tableId = job_reference .jobId
546- if copy_from_reference .projectId is None :
547- copy_from_reference .projectId = vp .RuntimeValueProvider .get_value (
548- 'project' , str , '' ) or self .project
549-
550- _LOGGER .info (
551- "Triggering copy job from %s to %s" ,
552- copy_from_reference ,
553- copy_to_reference )
543+ copy_from_references = []
544+ for destination , job_reference in element_list :
545+ copy_from_reference = bigquery_tools .parse_table_reference (destination )
546+ copy_from_reference .tableId = job_reference .jobId
547+ if copy_from_reference .projectId is None :
548+ copy_from_reference .projectId = vp .RuntimeValueProvider .get_value (
549+ 'project' , str , '' ) or self .project
550+ copy_from_references .append (copy_from_reference )
554551
555- wait_for_job , write_disposition = (
556- self ._determine_write_disposition (copy_to_reference ))
557-
558- if not self .bq_io_metadata :
559- self .bq_io_metadata = create_bigquery_io_metadata (self ._step_name )
552+ full_table_ref = '%s:%s.%s' % (
553+ copy_to_reference .projectId ,
554+ copy_to_reference .datasetId ,
555+ copy_to_reference .tableId )
560556
561- project_id = (
562- copy_to_reference .projectId
563- if self .load_job_project_id is None else self .load_job_project_id )
564- copy_job_name = '%s_%s' % (
557+ is_first_time = full_table_ref not in self ._observed_tables
558+ if is_first_time :
559+ self ._observed_tables .add (full_table_ref )
560+ if self .bq_io_metadata :
561+ Lineage .sinks ().add (
562+ 'bigquery' ,
563+ copy_to_reference .projectId ,
564+ copy_to_reference .datasetId ,
565+ copy_to_reference .tableId )
566+
567+ # Split into chunks of MAX_SOURCES_PER_COPY_JOB
568+ chunks = [
569+ copy_from_references [i :i + self .MAX_SOURCES_PER_COPY_JOB ]
570+ for i in range (
571+ 0 , len (copy_from_references ), self .MAX_SOURCES_PER_COPY_JOB )
572+ ]
573+
574+ copy_job_name_base = '%s_%s' % (
565575 job_name_prefix ,
566576 _bq_uuid (
567577 '%s:%s.%s' % (
568- copy_from_reference .projectId ,
569- copy_from_reference .datasetId ,
570- copy_from_reference .tableId )))
571- job_reference = self .bq_wrapper ._insert_copy_job (
572- project_id ,
573- copy_job_name ,
574- copy_from_reference ,
575- copy_to_reference ,
576- create_disposition = self .create_disposition ,
577- write_disposition = write_disposition ,
578- job_labels = self .bq_io_metadata .add_additional_bq_job_labels ())
579-
580- if wait_for_job :
581- self .bq_wrapper .wait_for_bq_job (job_reference , sleep_duration_sec = 10 )
582- self .pending_jobs .append (
583- GlobalWindows .windowed_value ((destination , job_reference )))
578+ copy_to_reference .projectId ,
579+ copy_to_reference .datasetId ,
580+ copy_to_reference .tableId )))
584581
585- def _determine_write_disposition (self , copy_to_reference ) -> tuple [bool , str ]:
586- """
587- Determines the write disposition for a BigQuery copy job,
588- based on destination.
589-
590- When the write_disposition for a job is WRITE_TRUNCATE, multiple copy jobs
591- to the same destination can interfere with each other, truncate data, and
592- write to the BigQuery table repeatedly. To prevent this, the first copy job
593- runs with the user's specified write_disposition, but subsequent jobs must
594- always use WRITE_APPEND. This ensures that subsequent copy jobs do not
595- clear out data appended by previous jobs.
596-
597- Args:
598- copy_to_reference: The reference to the destination table.
582+ project_id = (
583+ copy_to_reference .projectId
584+ if self .load_job_project_id is None else self .load_job_project_id )
599585
600- Returns:
601- A tuple containing a boolean indicating whether to wait for the job to
602- complete and the write disposition to use for the job.
603- """
604- full_table_ref = '%s:%s.%s' % (
605- copy_to_reference .projectId ,
606- copy_to_reference .datasetId ,
607- copy_to_reference .tableId )
608- if full_table_ref not in self ._observed_tables :
609- write_disposition = self .write_disposition
610- wait_for_job = (
611- self .write_disposition != BigQueryDisposition .WRITE_APPEND )
612- self ._observed_tables .add (full_table_ref )
613- Lineage .sinks ().add (
614- 'bigquery' ,
615- copy_to_reference .projectId ,
616- copy_to_reference .datasetId ,
617- copy_to_reference .tableId )
618- else :
619- wait_for_job = False
620- write_disposition = 'WRITE_APPEND'
621- return wait_for_job , write_disposition
586+ for i , chunk in enumerate (chunks ):
587+ if i == 0 and is_first_time :
588+ write_disposition = self .write_disposition
589+ # Wait inline only if we have multiple chunks and write disposition is WRITE_TRUNCATE or WRITE_EMPTY.
590+ # This ensures the first chunk initializes the table, and subsequent chunks (WRITE_APPEND) append to it.
591+ wait_for_job = (
592+ self .write_disposition in ('WRITE_TRUNCATE' , 'WRITE_EMPTY' ) and
593+ len (chunks ) > 1 )
594+ else :
595+ write_disposition = 'WRITE_APPEND'
596+ wait_for_job = False
597+
598+ chunk_job_name = copy_job_name_base
599+ if len (chunks ) > 1 :
600+ chunk_job_name = f"{ copy_job_name_base } _{ i } "
601+
602+ _LOGGER .info (
603+ "Triggering copy job %s from %s to %s (write_disposition: %s)" ,
604+ chunk_job_name , [str (r ) for r in chunk ],
605+ copy_to_reference ,
606+ write_disposition )
607+
608+ job_reference = self .bq_wrapper ._insert_copy_job (
609+ project_id ,
610+ chunk_job_name ,
611+ chunk ,
612+ copy_to_reference ,
613+ create_disposition = self .create_disposition ,
614+ write_disposition = write_disposition ,
615+ job_labels = self .bq_io_metadata .add_additional_bq_job_labels ()
616+ if self .bq_io_metadata else None )
617+
618+ if wait_for_job :
619+ self .bq_wrapper .wait_for_bq_job (job_reference , sleep_duration_sec = 10 )
620+
621+ self .pending_jobs .append (
622+ GlobalWindows .windowed_value ((first_destination , job_reference )))
622623
623624 def finish_bundle (self ):
624625 for windowed_value in self .pending_jobs :
@@ -745,7 +746,7 @@ def process(
745746 else :
746747 try :
747748 schema = bigquery_tools .table_schema_to_dict (
748- bigquery_tools . BigQueryWrapper () .get_table (
749+ self . bq_wrapper .get_table (
749750 project_id = table_reference .projectId ,
750751 dataset_id = table_reference .datasetId ,
751752 table_id = table_reference .tableId ).schema )
@@ -856,7 +857,8 @@ def process(self, element):
856857 if latest_partition .can_accept (file_size ):
857858 latest_partition .add (file_path , file_size )
858859 else :
859- partitions .append (latest_partition .files )
860+ if latest_partition .files :
861+ partitions .append (latest_partition .files )
860862 latest_partition = PartitionFiles .Partition (
861863 self .max_partition_size , self .max_files_per_partition )
862864 latest_partition .add (file_path , file_size )
@@ -1182,12 +1184,13 @@ def _load_data(
11821184 # the truncation happens only once. See
11831185 # https://github.com/apache/beam/issues/24535.
11841186 finished_temp_tables_load_job_ids_list_pc = (
1185- finished_temp_tables_load_job_ids_pc | beam .MapTuple (
1187+ finished_temp_tables_load_job_ids_pc
1188+ | beam .MapTuple (
11861189 lambda destination , job_reference : (
1187- bigquery_tools .parse_table_reference (destination ). tableId ,
1190+ bigquery_tools .get_hashable_destination (destination ),
11881191 (destination , job_reference )))
11891192 | beam .GroupByKey ()
1190- | beam .MapTuple (lambda tableId , batch : list (batch )))
1193+ | beam .MapTuple (lambda dest , batch : list (batch )))
11911194 else :
11921195 # Loads can happen in parallel.
11931196 finished_temp_tables_load_job_ids_list_pc = (
0 commit comments