@@ -175,6 +175,12 @@ defmodule Electric.Shapes.Consumer do
175175 end
176176
177177 @ impl GenServer
178+ # Any incoming message counts as activity: cancel the pending suspend timer (if
179+ # any) and recurse for actual handling of the call.
180+ def handle_call ( msg , from , % { suspend_timer: ref } = state ) when not is_nil ( ref ) do
181+ handle_call ( msg , from , cancel_suspend_timer ( state ) )
182+ end
183+
178184 def handle_call ( { :monitor , pid } , _from , % { monitors: monitors } = state ) do
179185 ref = make_ref ( )
180186 { :reply , ref , % { state | monitors: [ { pid , ref } | monitors ] } , state . hibernate_after }
@@ -186,8 +192,8 @@ defmodule Electric.Shapes.Consumer do
186192
187193 def handle_call ( :await_snapshot_start , from , state ) do
188194 Logger . debug ( "Starting a wait on the snapshot #{ state . shape_handle } for #{ inspect ( from ) } }" )
189-
190- { :noreply , State . add_waiter ( state , from ) , state . hibernate_after }
195+ state = State . add_waiter ( state , from )
196+ { :noreply , state , state . hibernate_after }
191197 end
192198
193199 def handle_call ( { :handle_event , event , trace_context } , _from , state ) do
@@ -205,9 +211,8 @@ defmodule Electric.Shapes.Consumer do
205211 def handle_call ( { :subscribe_materializer , pid } , _from , state ) do
206212 Logger . debug ( "Subscribing materializer for #{ state . shape_handle } " )
207213 Process . monitor ( pid , tag: :materializer_down )
208-
209- { :reply , { :ok , state . latest_offset } , % { state | materializer_subscribed?: true } ,
210- state . hibernate_after }
214+ state = % { state | materializer_subscribed?: true }
215+ { :reply , { :ok , state . latest_offset } , state , state . hibernate_after }
211216 end
212217
213218 def handle_call ( { :stop , reason } , _from , state ) do
@@ -216,6 +221,11 @@ defmodule Electric.Shapes.Consumer do
216221 end
217222
218223 @ impl GenServer
224+ # Cancel the suspend timer on activity, then recurse for the actual handling of the cast.
225+ def handle_cast ( msg , % { suspend_timer: ref } = state ) when not is_nil ( ref ) do
226+ handle_cast ( msg , cancel_suspend_timer ( state ) )
227+ end
228+
219229 def handle_cast (
220230 { :pg_snapshot_known , shape_handle , { xmin , xmax , xip_list } = snapshot } ,
221231 % { shape_handle: shape_handle } = state
@@ -253,6 +263,29 @@ defmodule Electric.Shapes.Consumer do
253263 end
254264
255265 @ impl GenServer
266+ def handle_info ( :suspend_timeout , % { suspend_timer: ref } = state ) when not is_nil ( ref ) do
267+ state = % { state | suspend_timer: nil }
268+
269+ if consumer_suspend_enabled? ( state ) and consumer_can_suspend? ( state ) do
270+ Logger . debug ( fn -> [ "Suspending consumer " , to_string ( state . shape_handle ) ] end )
271+ { :stop , ShapeCleaner . consumer_suspend_reason ( ) , state }
272+ else
273+ # Conditions changed - just restart the hibernate timeout
274+ { :noreply , state , state . hibernate_after }
275+ end
276+ end
277+
278+ # Timer already cancelled. Ignore the trigger.
279+ def handle_info ( :suspend_timeout , state ) do
280+ { :noreply , state , state . hibernate_after }
281+ end
282+
283+ # Any incoming message counts as activity: cancel the pending suspend timer (if any)
284+ # and recurse for the actual handling of the message.
285+ def handle_info ( msg , % { suspend_timer: ref } = state ) when not is_nil ( ref ) do
286+ handle_info ( msg , cancel_suspend_timer ( state ) )
287+ end
288+
256289 def handle_info ( { :initialize_shape , shape , opts } , state ) do
257290 % { stack_id: stack_id , shape_handle: shape_handle } = state
258291
@@ -383,32 +416,24 @@ defmodule Electric.Shapes.Consumer do
383416 { :stop , reason , state }
384417 end
385418
386- # Set a new value for hibernate after and set a timeout between
387- # hibernate_after and max_timeout in order to spread
388- # consumer suspend events.
389- def handle_info ( { :configure_suspend , hibernate_after , jitter_period } , state ) do
390- { :noreply , % { state | hibernate_after: hibernate_after } ,
391- Enum . random ( hibernate_after .. jitter_period ) }
419+ # Set new values for hibernate_after and suspend_after, and set a jittered
420+ # timeout between hibernate_after and jitter_period to spread hibernation
421+ # events. Each consumer will hibernate at the jittered timeout, then schedule
422+ # suspension for suspend_after ms later.
423+ def handle_info ( { :configure_suspend , hibernate_after , suspend_after , jitter_period } , state ) do
424+ state = % { state | hibernate_after: hibernate_after , suspend_after: suspend_after }
425+ { :noreply , state , Enum . random ( hibernate_after .. jitter_period ) }
392426 end
393427
394428 def handle_info ( :timeout , state ) do
395- # we can only suspend (terminate) the consumer process if
396- #
397- # 1. Consumer suspend has been enabled in the stack config
398- # 2. we're not waiting for snapshot information
399- # 3. we are not part of a subquery dependency tree, that is either
400- # a. we have no dependent shapes
401- # b. we don't have a materializer subscribed
402- # 4. we're not in the middle of processing a multi-fragment transaction
429+ state = % { state | writer: ShapeCache.Storage . hibernate ( state . writer ) }
403430
404- if consumer_suspend_enabled? ( state ) and consumer_can_suspend? ( state ) do
405- Logger . debug ( fn -> [ "Suspending consumer " , to_string ( state . shape_handle ) ] end )
406- { :stop , ShapeCleaner . consumer_suspend_reason ( ) , state }
407- else
408- state = % { state | writer: ShapeCache.Storage . hibernate ( state . writer ) }
431+ state =
432+ if consumer_suspend_enabled? ( state ) and consumer_can_suspend? ( state ) ,
433+ do: schedule_suspend_timer ( state ) ,
434+ else: state
409435
410- { :noreply , state , :hibernate }
411- end
436+ { :noreply , state , :hibernate }
412437 end
413438
414439 defp consumer_suspend_enabled? ( % { stack_id: stack_id } ) do
@@ -420,6 +445,20 @@ defmodule Electric.Shapes.Consumer do
420445 not state . materializer_subscribed? and is_nil ( state . pending_txn )
421446 end
422447
448+ defp schedule_suspend_timer ( % { suspend_after: nil } = state ) , do: state
449+
450+ defp schedule_suspend_timer ( % { suspend_after: suspend_after } = state ) do
451+ ref = :erlang . send_after ( suspend_after , self ( ) , :suspend_timeout )
452+ % { state | suspend_timer: ref }
453+ end
454+
455+ defp cancel_suspend_timer ( % { suspend_timer: nil } = state ) , do: state
456+
457+ defp cancel_suspend_timer ( % { suspend_timer: ref } = state ) do
458+ :erlang . cancel_timer ( ref )
459+ % { state | suspend_timer: nil }
460+ end
461+
423462 @ impl GenServer
424463 def terminate ( reason , state ) do
425464 Logger . debug ( fn ->
0 commit comments