1414from prometheus_client import Counter , Gauge
1515
1616from src .config import settings
17- from src .export_cursor_fence import CursorFenceFactory , export_cursor_fence
17+ from src .export_cursor_fence import (
18+ CursorFenceFactory ,
19+ CutoffFactory ,
20+ capture_cdc_snapshot_cutoff ,
21+ export_cursor_fence ,
22+ )
1823from src .metrics import (
1924 export_errors_total ,
2025 exporter_export_lag ,
2429 local_db_pool_idle ,
2530 local_db_pool_size ,
2631 r2_pending_gauge ,
32+ reconciliation_discrepancies ,
2733 redis_queue_depth ,
2834 supa_db_pool_idle ,
2935 supa_db_pool_size ,
7379# ---------------------------------------------------------------------------
7480
7581_EPOCH = datetime .min .replace (tzinfo = UTC )
82+ _MAX_CDC_CUTOFF = datetime .max .replace (tzinfo = UTC )
7683_ZERO_UUID = uuid .UUID (int = 0 )
7784
7885# Sentinel stamped on Typesense `experience_max` for rows the extractor
@@ -995,6 +1002,8 @@ async def _export_postings_dual(
9951002 maps : TaxonomyMaps ,
9961003 supa_backoff : _DownstreamBackoff | None = None ,
9971004 ts_backoff : _DownstreamBackoff | None = None ,
1005+ * ,
1006+ cutoff : datetime = _MAX_CDC_CUTOFF ,
9981007) -> tuple [int , Cursor , Cursor ]:
9991008 """Fetch changed postings and upsert to both Supabase and Typesense concurrently.
10001009
@@ -1027,6 +1036,7 @@ async def _export_postings_dual(
10271036 fetch_ts ,
10281037 fetch_id ,
10291038 settings .export_batch_limit ,
1039+ cutoff ,
10301040 )
10311041 if not rows :
10321042 return 0 , supa_cursor , ts_cursor
@@ -1256,7 +1266,8 @@ def select_changed_sql(cls, *extras: str) -> str:
12561266 + cls .select_list (* extras )
12571267 + " FROM "
12581268 + cls .table
1259- + " WHERE (updated_at, id) > ($1, $2) ORDER BY updated_at, id LIMIT $3"
1269+ + " WHERE (updated_at, id) > ($1, $2)"
1270+ + " AND updated_at < $4 ORDER BY updated_at, id LIMIT $3"
12601271 )
12611272
12621273
@@ -1269,6 +1280,8 @@ async def _export_changed_postings(
12691280 local_pool : asyncpg .Pool ,
12701281 supa_pool : asyncpg .Pool ,
12711282 cursor : Cursor ,
1283+ * ,
1284+ cutoff : datetime = _MAX_CDC_CUTOFF ,
12721285) -> tuple [int , Cursor ]:
12731286 """Export job_posting rows changed since cursor to Supabase.
12741287
@@ -1286,6 +1299,7 @@ async def _export_changed_postings(
12861299 last_ts ,
12871300 last_id ,
12881301 settings .export_batch_limit ,
1302+ cutoff ,
12891303 )
12901304 if not rows :
12911305 return 0 , cursor
@@ -1442,6 +1456,7 @@ async def run_exporter(
14421456 shutdown_event : asyncio .Event ,
14431457 * ,
14441458 cursor_fence_factory : CursorFenceFactory = export_cursor_fence ,
1459+ cutoff_factory : CutoffFactory = capture_cdc_snapshot_cutoff ,
14451460) -> None :
14461461 """Main exporter loop.
14471462
@@ -1486,6 +1501,12 @@ async def run_exporter(
14861501 if maps .stale :
14871502 await maps .refresh (local_pool , supa_pool )
14881503
1504+ # Probe until transactions that changed exported posting
1505+ # fields have committed, capture a clock cutoff, then
1506+ # immediately release the writer barrier. Rows stamped by
1507+ # later transactions stay above this strict upper bound.
1508+ cutoff = await cutoff_factory (local_pool )
1509+
14891510 # Two-cursor dual export
14901511 exported , posting_cursor , ts_cursor = await _export_postings_dual (
14911512 local_pool ,
@@ -1495,6 +1516,7 @@ async def run_exporter(
14951516 maps ,
14961517 supa_backoff ,
14971518 ts_backoff ,
1519+ cutoff = cutoff ,
14981520 )
14991521 # Save both cursors in a single transaction so a crash
15001522 # between writes cannot leave one cursor advanced while
@@ -1513,8 +1535,12 @@ async def run_exporter(
15131535 exported = 0
15141536 else :
15151537 try :
1538+ cutoff = await cutoff_factory (local_pool )
15161539 exported , posting_cursor = await _export_changed_postings (
1517- local_pool , supa_pool , posting_cursor
1540+ local_pool ,
1541+ supa_pool ,
1542+ posting_cursor ,
1543+ cutoff = cutoff ,
15181544 )
15191545 except Exception as exc :
15201546 if not _is_downstream_unavailable (exc ):
@@ -1727,6 +1753,7 @@ async def run_reconciliation(
17271753 ts_discrepancies = await _reconcile_typesense (local_pool )
17281754 discrepancies += ts_discrepancies
17291755
1756+ reconciliation_discrepancies .inc (discrepancies )
17301757 log .info ("reconciliation.completed" , discrepancies = discrepancies )
17311758 return discrepancies
17321759
0 commit comments