Skip to content

Commit 8be87e6

Browse files
committed
clean up + partial idx for delete
1 parent 7f9a797 commit 8be87e6

7 files changed

Lines changed: 82 additions & 50 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE INDEX ON idr.prior_auth (utn) WHERE utn NOT LIKE '-%';
2+
CREATE INDEX ON idr.prior_auth_item (utn) WHERE utn NOT LIKE '-%';

apps/bfd-pipeline-idr/extractor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def extract_full_idr_data(self, source: Source) -> Iterator[list[T]]:
197197
fetch_query = self.get_query(start_time, source)
198198
logger.info("extracting full {}", self.cls.table())
199199
return self.extract_many(
200-
fetch_query.replace("{LAST_TS}", "%(timestamp)s"), {"timestamp": start_time}
200+
fetch_query.replace("{MIN_TS}", "%(timestamp)s"), {"timestamp": start_time}
201201
)
202202

203203
def _transform(self, batch: list[dict[str, DbType]]) -> list[T]:

apps/bfd-pipeline-idr/loader.py

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import functools
12
import itertools
23
import operator
34
from collections.abc import Awaitable, Callable, Iterator, Sequence
45
from datetime import UTC, datetime
5-
from typing import Any, cast
6+
from typing import Any, Generic, cast, override
67

78
import anyio
89
import psycopg
@@ -78,9 +79,7 @@ async def _async_load(
7879
timeout=600,
7980
) as pool:
8081
await pool.wait()
81-
loader_cls = (
82-
FullSyncBatchLoader if model.should_fully_sync_delete_diff() else BatchLoader
83-
)
82+
loader_cls = FullSyncBatchLoader if model.should_delete_missing() else BatchLoader
8483
return await loader_cls(
8584
fetch_results,
8685
model,
@@ -94,7 +93,7 @@ async def _async_load(
9493
).load()
9594

9695

97-
class BatchLoader:
96+
class BatchLoader(Generic[T]): # noqa: UP046
9897
def __init__(
9998
self,
10099
fetch_results: Iterator[list[T]],
@@ -175,28 +174,15 @@ def __init__(
175174

176175
async def load(self) -> bool:
177176
timestamp = datetime.now(UTC)
178-
179177
self.full_load_timer.start()
180178
async with self.pool.connection() as conn, conn.cursor(binary=True) as cur:
181-
self.progress_start_timer.start()
182-
await self._insert_batch_start(cur)
183-
await conn.commit()
184-
self.progress_start_timer.stop()
179+
await self._record_batch_start(conn, cur, commit=True)
185180

186-
data_loaded = False
187-
num_rows = 0
188181
batch_num = 1
189-
while True:
190-
self.idr_query_timer.start()
191-
# We unfortunately need to use a while true loop here since we need to wrap the
192-
# iterator with the timer calls.
193-
results = next(self.fetch_results, None)
194-
self.idr_query_timer.stop()
195-
if not results:
196-
break
197182

183+
async def _process_batch(results: list[T]) -> None:
184+
nonlocal batch_num
198185
self.full_batch_timer.start()
199-
data_loaded = True
200186
logger.info(
201187
"{}-{}-{}: loading next {} results concurrently {} row(s) at a time",
202188
self.table,
@@ -205,8 +191,6 @@ async def load(self) -> bool:
205191
len(results),
206192
PER_BATCH_CONCURRENT_ROWS,
207193
)
208-
num_rows += len(results)
209-
210194
self.sort_batch_timer.start()
211195
results.sort(key=operator.attrgetter(*self.ordered_pkeys))
212196
self.sort_batch_timer.stop()
@@ -262,6 +246,9 @@ async def _wrap_batch_chunk(
262246
batch_num += 1
263247
self.full_batch_timer.stop()
264248

249+
num_rows = await self._stage_all_batches(_process_batch)
250+
data_loaded = num_rows > 0
251+
265252
# Wait until the background worker signals that all pending loading tasks are completed
266253
# for the current partition before marking it totally complete
267254
self.worker_client.wait_until_done(self.model, self.partition)
@@ -421,29 +408,54 @@ async def _copy_data(
421408
[_remove_null_bytes(getattr(row, k)) for k in self.insert_cols]
422409
)
423410

411+
async def _record_batch_start(
412+
self, conn: psycopg.AsyncConnection, cur: psycopg.AsyncCursor[Any], commit: bool
413+
) -> None:
414+
self.progress_start_timer.start()
415+
await self._insert_batch_start(cur)
416+
if commit:
417+
await conn.commit()
418+
self.progress_start_timer.stop()
419+
420+
def _next_batch(self) -> list[T] | None:
421+
self.idr_query_timer.start()
422+
results = next(self.fetch_results, None)
423+
self.idr_query_timer.stop()
424+
return results
425+
426+
async def _stage_all_batches(self, process_batch: Callable[[list[T]], Awaitable[None]]) -> int:
427+
num_rows = 0
428+
429+
while True:
430+
# We unfortunately need to use a while true loop here since we need to wrap the
431+
# iterator with the timer calls.
432+
self.idr_query_timer.start()
433+
results = next(self.fetch_results, None)
434+
self.idr_query_timer.stop()
435+
if not results:
436+
break
424437

425-
class FullSyncBatchLoader(BatchLoader):
438+
num_rows += len(results)
439+
await process_batch(results)
440+
441+
return num_rows
442+
443+
444+
class FullSyncBatchLoader(BatchLoader[T]):
445+
@override
426446
async def load(self) -> bool:
427447
timestamp = datetime.now(UTC)
428448
self.full_load_timer.start()
429-
num_rows = 0
449+
data_loaded = False
430450

431451
async with self.pool.connection() as conn, conn.cursor(binary=True) as cur:
432-
self.progress_start_timer.start()
433-
await self._insert_batch_start(cur)
434-
self.progress_start_timer.stop()
435-
452+
await self._record_batch_start(conn, cur, commit=False)
436453
full_temp_table = await self._setup_temp_table(cur, "full_temp")
437454

438-
while True:
439-
self.idr_query_timer.start()
440-
results = next(self.fetch_results, None)
441-
self.idr_query_timer.stop()
442-
if not results:
443-
break
444-
num_rows += len(results)
445-
await self._copy_data(cur, full_temp_table, results)
446-
455+
num_rows = await self._stage_all_batches(
456+
functools.partial(self._copy_data, cur, full_temp_table)
457+
)
458+
data_loaded = num_rows > 0
447459
logger.info(
448460
"{}-{}: staged {} row(s) for full sync",
449461
self.table,
@@ -472,19 +484,22 @@ async def load(self) -> bool:
472484
self.table,
473485
self.partition.name,
474486
)
475-
return True
487+
return data_loaded
476488

477489
async def _delete_missing(self, cur: psycopg.AsyncCursor[Any], temp_tablename: str) -> int:
478490
# We have to exclude our synthetic data that also exists in prod from deletion
479-
synthetic_data_filter = (
480-
"" if self.load_mode == LoadMode.SYNTHETIC else "WHERE utn NOT LIKE '-%'"
491+
synthetic_data_filter = self.model.synthetic_data_filter()
492+
synthetic_where_clause = (
493+
f"WHERE {synthetic_data_filter}"
494+
if synthetic_data_filter and self.load_mode != LoadMode.SYNTHETIC
495+
else ""
481496
)
482497
result = await cur.execute( # type: ignore
483498
f'''
484499
DELETE FROM {self.table}
485500
WHERE ({self.primary_keys_str}) IN (
486501
SELECT {self.primary_keys_str} FROM {self.table}
487-
{synthetic_data_filter}
502+
{synthetic_where_clause}
488503
EXCEPT
489504
SELECT {self.primary_keys_str} FROM "{temp_tablename}"
490505
)
@@ -506,5 +521,5 @@ def _remove_null_bytes(val: DbType) -> DbType:
506521

507522

508523
def should_track_load_progress(load_mode: LoadMode) -> bool:
509-
# Whether to read/write load progress, which is diabled for synthetic and testing loads.
524+
# Whether to read/write load progress, which is disabled for synthetic and testing loads.
510525
return load_mode == LoadMode.PROD or force_load_progress()

apps/bfd-pipeline-idr/model/base_model.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,14 +491,19 @@ def should_replace() -> bool:
491491
return False
492492

493493
@staticmethod
494-
def should_fully_sync_delete_diff() -> bool:
494+
def should_delete_missing() -> bool:
495495
"""Whether upstream data deletion requires manual cleanup on our end.
496496
497497
Upstream data can be deleted with no indicator like an obsolete timestamp, requiring
498498
us to delete it on our end.
499499
"""
500500
return False
501501

502+
@staticmethod
503+
def synthetic_data_filter() -> str:
504+
"""Expression used to exclude synthetic data from being deleted in FullSyncBatchLoader."""
505+
return ""
506+
502507
@classmethod
503508
@abstractmethod
504509
def fetch_query(

apps/bfd-pipeline-idr/model/idr_prior_auth.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,19 @@ def model_type() -> ModelType:
7676

7777
@override
7878
@staticmethod
79-
def should_fully_sync_delete_diff() -> bool:
79+
def should_delete_missing() -> bool:
8080
return True
8181

8282
@override
8383
@classmethod
8484
def is_immutable(cls) -> bool:
8585
return False
8686

87+
@override
88+
@staticmethod
89+
def synthetic_data_filter() -> str:
90+
return "utn NOT LIKE '-%'"
91+
8792
@override
8893
@classmethod
8994
def fetch_query(cls, partition: LoadPartition, start_time: datetime, source: Source) -> str:
@@ -97,7 +102,7 @@ def fetch_query(cls, partition: LoadPartition, start_time: datetime, source: Sou
97102
SELECT *, ROW_NUMBER()
98103
OVER (PARTITION BY mbi_num, utn ORDER BY current_segment) as row_order
99104
FROM {IDR_PRIOR_AUTH_TABLE}
100-
WHERE pa_req_rec_dt > {{LAST_TS}}
105+
WHERE pa_req_rec_dt > {{MIN_TS}}
101106
)
102107
SELECT {{COLUMNS}} FROM distinct_prior_auths {prior_auth}
103108
LEFT JOIN {IDR_PROVIDER_HISTORY_TABLE} {prvdr_att_phy}

apps/bfd-pipeline-idr/model/idr_prior_auth_item.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,19 @@ def model_type() -> ModelType:
5757

5858
@override
5959
@staticmethod
60-
def should_fully_sync_delete_diff() -> bool:
60+
def should_delete_missing() -> bool:
6161
return True
6262

6363
@override
6464
@classmethod
6565
def is_immutable(cls) -> bool:
6666
return False
6767

68+
@override
69+
@staticmethod
70+
def synthetic_data_filter() -> str:
71+
return "utn NOT LIKE '-%'"
72+
6873
@override
6974
@classmethod
7075
def fetch_query(cls, partition: LoadPartition, start_time: datetime, source: Source) -> str:
@@ -75,7 +80,7 @@ def fetch_query(cls, partition: LoadPartition, start_time: datetime, source: Sou
7580
SELECT *, ROW_NUMBER()
7681
OVER (PARTITION BY mbi_num, utn, current_segment ORDER BY mbi_num) as row_order
7782
FROM {IDR_PRIOR_AUTH_TABLE}
78-
WHERE pa_req_rec_dt > {{LAST_TS}}
83+
WHERE pa_req_rec_dt > {{MIN_TS}}
7984
)
8085
SELECT {{COLUMNS}} FROM distinct_prior_auths {prior_auth}
8186
WHERE row_order = 1;

apps/bfd-pipeline-idr/pipeline_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def extract_and_load(
8989

9090
data_iter = (
9191
data_extractor.extract_full_idr_data(source)
92-
if cls.should_fully_sync_delete_diff()
92+
if cls.should_delete_missing()
9393
else data_extractor.extract_idr_data(progress, job_start, source)
9494
)
9595
res = loader.load(

0 commit comments

Comments
 (0)