@@ -6,11 +6,12 @@ defmodule Extensions.PostgresCdcRls.ReplicationPoller do
66
77 ## Lifecycle
88
9- On start the poller connects to the tenant's database, calls
10- `Replications.prepare_replication/2` to create the temporary slot, and fetches
11- the publication's tables via `Subscriptions.fetch_publication_tables/2`. If the
12- publication has tables, it kicks off the poll loop; if not, it stays idle and
13- waits for tables to appear.
9+ On start the poller connects to the tenant's database and fetches the
10+ publication's tables via `Subscriptions.fetch_publication_tables/2`. Only if
11+ the publication has tables does it call `Replications.prepare_replication/2`
12+ to create the temporary slot and kick off the poll loop; if the publication is
13+ empty it stays idle without creating a slot (an unconsumed slot would retain
14+ WAL) and waits for tables to appear.
1415
1516 ## Poll loop
1617
@@ -247,6 +248,10 @@ defmodule Extensions.PostgresCdcRls.ReplicationPoller do
247248
248249 { n , 0 } when n > 0 ->
249250 cancel_timer ( state . poll_ref )
251+ # Cancel any pending :retry too: a retry left over from a prior
252+ # list_changes/5 error would otherwise fire after the slot is dropped and
253+ # re-run prepare_replication/1, recreating work (and possibly the slot).
254+ cancel_timer ( state . retry_ref )
250255
251256 case Replications . drop_replication_slot ( conn , state . slot_name ) do
252257 { :error , reason } when reason != :slot_not_found ->
@@ -257,7 +262,16 @@ defmodule Extensions.PostgresCdcRls.ReplicationPoller do
257262
258263 _ ->
259264 cancel_timer ( state . check_oid_ref )
260- { :noreply , % { state | oids: new_oids , poll_ref: nil , check_oid_ref: schedule_check_oids ( ) } }
265+
266+ { :noreply ,
267+ % {
268+ state
269+ | oids: new_oids ,
270+ poll_ref: nil ,
271+ retry_ref: nil ,
272+ retry_count: 0 ,
273+ check_oid_ref: schedule_check_oids ( )
274+ } }
261275 end
262276
263277 _ ->
@@ -283,26 +297,37 @@ defmodule Extensions.PostgresCdcRls.ReplicationPoller do
283297 slot_name: slot_name ,
284298 tenant_id: tenant_id ,
285299 publication: publication ,
300+ oids: oids ,
286301 check_oid_ref: check_oid_ref
287302 } = state
288303 ) do
289- case Replications . prepare_replication ( conn , slot_name ) do
290- { :ok , _ } ->
291- oids = Subscriptions . fetch_publication_tables ( conn , publication )
292- if map_size ( oids ) > 0 , do: send ( self ( ) , :poll )
304+ # Reuse oids when a caller (e.g. :check_oids) already fetched them; otherwise
305+ # fetch once here so we never create a slot for an empty publication.
306+ oids = if map_size ( oids ) > 0 , do: oids , else: Subscriptions . fetch_publication_tables ( conn , publication )
293307
294- cancel_timer ( check_oid_ref )
295- { :noreply , % { state | oids: oids , check_oid_ref: schedule_check_oids ( ) } }
308+ if map_size ( oids ) > 0 do
309+ case Replications . prepare_replication ( conn , slot_name ) do
310+ { :ok , _ } ->
311+ send ( self ( ) , :poll )
296312
297- { :error , error } ->
298- log_error ( "PoolingReplicationPreparationError" , error )
313+ cancel_timer ( check_oid_ref )
314+ { :noreply , % { state | oids: oids , check_oid_ref: schedule_check_oids ( ) } }
299315
300- Realtime.Telemetry . execute ( [ :realtime , :replication , :poller , :prepare , :exception ] , % { } , % {
301- tenant: tenant_id ,
302- reason: error
303- } )
316+ { :error , error } ->
317+ log_error ( "PoolingReplicationPreparationError" , error )
304318
305- retry_or_stop ( state , error )
319+ Realtime.Telemetry . execute ( [ :realtime , :replication , :poller , :prepare , :exception ] , % { } , % {
320+ tenant: tenant_id ,
321+ reason: error
322+ } )
323+
324+ retry_or_stop ( state , error )
325+ end
326+ else
327+ # Empty publication: don't create a slot (it would retain WAL with nothing
328+ # to consume it). Wait for :check_oids to observe tables appearing.
329+ cancel_timer ( check_oid_ref )
330+ { :noreply , % { state | oids: oids , check_oid_ref: schedule_check_oids ( ) } }
306331 end
307332 end
308333
0 commit comments