44
55import psycopg
66from psycopg .abc import Params , QueryNoTemplate
7+ from psycopg .errors import DeadlockDetected , LockNotAvailable , QueryCanceled
78
89from constants import DEFAULT_MIN_DATE
910from load_partition import LoadPartition , LoadType
@@ -95,10 +96,14 @@ def __init__(
9596 self .meta_keys = (
9697 ["bfd_created_ts" ] if self .immutable else ["bfd_created_ts" , "bfd_updated_ts" ]
9798 )
99+ self .progress_start_timer = Timer ("progress_start" , model , partition )
98100 self .idr_query_timer = Timer ("idr_query" , model , partition )
99101 self .temp_table_timer = Timer ("temp_table" , model , partition )
100102 self .copy_timer = Timer ("copy" , model , partition )
101- self .insert_timer = Timer ("insert" , model , partition )
103+ self .upsert_timer = Timer ("upsert" , model , partition )
104+ self .last_updated_timer = Timer ("last_updated" , model , partition )
105+ self .total_insert_timer = Timer ("total_insert" , model , partition )
106+ self .update_progress_timer = Timer ("update_progress" , model , partition )
102107 self .commit_timer = Timer ("commit" , model , partition )
103108 self .load_type = load_type
104109 self .enable_load_progress = should_track_load_progress (load_mode )
@@ -111,8 +116,10 @@ def load(
111116 # (temp tables can't be created with an explicit schema set)
112117
113118 with self .conn .cursor () as cur :
119+ self .progress_start_timer .start ()
114120 self ._insert_batch_start (cur )
115121 self .conn .commit ()
122+ self .progress_start_timer .stop ()
116123 data_loaded = False
117124 num_rows = 0
118125
@@ -140,11 +147,13 @@ def load(
140147
141148 if results :
142149 # Upsert into the main table
143- self .insert_timer .start ()
150+ self .total_insert_timer .start ()
144151 self ._merge (cur , timestamp )
145- self .insert_timer .stop ()
152+ self .total_insert_timer .stop ()
146153
154+ self .update_progress_timer .start ()
147155 self ._calculate_load_progress (cur , results )
156+ self .update_progress_timer .stop ()
148157
149158 self .commit_timer .start ()
150159 self .conn .commit ()
@@ -211,11 +220,13 @@ def _setup_temp_table(self, cur: psycopg.Cursor) -> None:
211220 # For simplicity's sake, we'll create our temp tables using the existing schema and
212221 # just drop the columns we need to ignore.
213222 cur .execute (
214- f"CREATE TEMPORARY TABLE { self .temp_table } (LIKE { self .table } ) ON COMMIT DROP" # type: ignore
223+ f"CREATE TEMPORARY TABLE IF NOT EXISTS { self .temp_table } (LIKE { self .table } ) "
224+ "ON COMMIT PRESERVE ROWS" # type: ignore
215225 )
226+ cur .execute (f"TRUNCATE TABLE { self .temp_table } " ) # type: ignore
216227 # Created/updated columns don't need to be loaded from the source.
217228 for col in self .meta_keys :
218- cur .execute (f"ALTER TABLE { self .temp_table } DROP COLUMN { col } " ) # type: ignore
229+ cur .execute (f"ALTER TABLE { self .temp_table } DROP COLUMN IF EXISTS { col } " ) # type: ignore
219230
220231 def _calculate_load_progress (self , cur : psycopg .Cursor , results : Sequence [T ]) -> None :
221232 last = results [len (results ) - 1 ].model_dump ()
@@ -260,6 +271,7 @@ def _update_load_progress(
260271 ) -> None :
261272 if self .enable_load_progress :
262273 cur .execute (query , params ) # type: ignore
274+ self .conn .commit ()
263275
264276 def _merge (self , cur : psycopg .Cursor , timestamp : datetime ) -> None :
265277 unique_key = self .model .unique_key ()
@@ -274,11 +286,15 @@ def _merge(self, cur: psycopg.Cursor, timestamp: datetime) -> None:
274286 on_conflict = (
275287 "DO NOTHING"
276288 if self .immutable or not update_set
277- else f"DO UPDATE SET { update_set } , bfd_updated_ts=%(timestamp)s"
289+ else (
290+ f"DO UPDATE SET { update_set } , bfd_updated_ts=%(timestamp)s "
291+ "WHERE (t.*) IS DISTINCT FROM (EXCLUDED.*)"
292+ )
278293 )
279- timestamp_placeholders = "," .join ("%(timestamp)s" for _ in self .meta_keys )
294+ timestamp_placeholders = ", " .join ("%(timestamp)s" for _ in self .meta_keys )
280295
281296 # Upsert into the main table
297+ self .upsert_timer .start ()
282298 if self .model .should_replace ():
283299 # Delete before inserting since we've specified that the data should be
284300 # replaced rather than merged.
@@ -287,42 +303,54 @@ def _merge(self, cur: psycopg.Cursor, timestamp: datetime) -> None:
287303 cur .execute (f"DELETE FROM { self .table } " ) # type: ignore
288304 cur .execute (
289305 f"""
290- INSERT INTO { self .table } ({ self .cols_str } , { "," .join (self .meta_keys )} )
291- SELECT { self .cols_str } ,{ timestamp_placeholders } FROM { self .temp_table }
292- ON CONFLICT ({ "," .join (unique_key )} ) { on_conflict }
306+ INSERT INTO { self .table } AS t ({ self .cols_str } , { ", " .join (self .meta_keys )} )
307+ SELECT { self .cols_str } , { timestamp_placeholders } FROM { self .temp_table }
308+ ON CONFLICT ({ ", " .join (unique_key )} ) { on_conflict }
293309 """ , # type: ignore
294310 {"timestamp" : timestamp },
311+ binary = True ,
295312 )
313+ self .conn .commit ()
314+ self .upsert_timer .stop ()
296315
297316 if self .load_type == LoadType .INCREMENTAL and self .model .last_updated_date_table ():
298317 key = self .model .last_updated_timestamp_col ()
299318 last_updated_cols = self .model .last_updated_date_column ()
300319 set_clause = ", " .join (f"{ col } = %(timestamp)s" for col in last_updated_cols )
301320
302- # We require multi-step transactions since we're dealing with temp tables, so there
303- # is a chance of a deadlock here.
304- # However, it's safe to ignore these because if the timestamp for this row is being
305- # updated concurrently then it's going to have the same end result anyway.
306- # If a deadlock occurs, the CTE returns no rows and this is a no-op.
307-
308- cur .execute (
309- f"""
310- WITH current_ts AS (
311- SELECT { key }
312- FROM { self .model .last_updated_date_table ()}
313- WHERE { key } IN (
314- SELECT { key } FROM { self .temp_table }
321+ self .last_updated_timer .start ()
322+ try :
323+ # We want to immediately terminate the transaction if there is already a lock on
324+ # the table so that we avoid extraneous waits because if there is a lock this table
325+ # is being updated concurrently and that existing update will have the same result
326+ cur .execute ("SAVEPOINT pre_last_updated" )
327+ cur .execute ("SET LOCAL lock_timeout=1" )
328+ cur .execute ("SET LOCAL statement_timeout=3000" )
329+ cur .execute (
330+ f"""
331+ WITH current_ts AS (
332+ SELECT { key }
333+ FROM { self .model .last_updated_date_table ()}
334+ WHERE { key } IN (
335+ SELECT { key } FROM { self .temp_table }
336+ )
337+ ORDER BY { key }
338+ FOR UPDATE SKIP LOCKED
315339 )
316- ORDER BY { key }
317- FOR UPDATE SKIP LOCKED
340+ UPDATE { self .model .last_updated_date_table ()} u
341+ SET { set_clause }
342+ FROM current_ts t
343+ WHERE u.{ key } = t.{ key } ;
344+ """ , # type: ignore
345+ {"timestamp" : timestamp },
318346 )
319- UPDATE { self .model . last_updated_date_table () } u
320- SET { set_clause }
321- FROM current_ts t
322- WHERE u. { key } = t. { key } ;
323- """ , # type: ignore
324- { "timestamp" : timestamp },
325- )
347+ self .conn . commit ()
348+ except ( DeadlockDetected , LockNotAvailable , QueryCanceled ) as ex :
349+ logger . warning (
350+ "deadlock/lock/statement timeout updating update timestamp, ignoring: %s" , ex
351+ )
352+ cur . execute ( "ROLLBACK TO SAVEPOINT pre_last_updated" )
353+ self . last_updated_timer . stop ( )
326354
327355 def _copy_data (self , cur : psycopg .Cursor , results : Sequence [T ]) -> None :
328356 # Use COPY to load the batch into Postgres.
0 commit comments