@@ -61,7 +61,8 @@ defmodule Commanded.Event.Handler do
6161 handler_state: map(),
6262 first_event_id: binary(),
6363 last_event_id: binary(),
64- event_count: integer()}
64+ event_count: integer(),
65+ flush_reason: :size | :timeout | :immediate}
6566 """
6667 } )
6768
@@ -79,6 +80,7 @@ defmodule Commanded.Event.Handler do
7980 last_event_id: binary(),
8081 event_count: integer(),
8182 recorded_event: RecordedEvent.t() | nil,
83+ flush_reason: :size | :timeout | :immediate,
8284 optional(:error) => any()}
8385 """
8486 } )
@@ -97,6 +99,7 @@ defmodule Commanded.Event.Handler do
9799 first_event_id: binary(),
98100 last_event_id: binary(),
99101 event_count: integer(),
102+ flush_reason: :size | :timeout | :immediate,
100103 kind: :throw | :error | :exit,
101104 reason: any(),
102105 optional(:stacktrace) => list()}
@@ -646,8 +649,14 @@ defmodule Commanded.Event.Handler do
646649 - :subscribe_to - which stream to subscribe to can be either `:all` to
647650 subscribe to all events or a named stream (default: `:all`).
648651
649- - :batch_size - the size of batches to deliver to `handle_batch` in batched
650- mode.
652+ - :batch_size - controls the EventStore subscription's in-flight buffer
653+ size. When `batch_timeout` is also set, this is the maximum number of
654+ events the handler buffers before flushing. Enables `handle_batch/1`.
655+
656+ - :batch_timeout - maximum milliseconds to wait for events to accumulate
657+ in the handler buffer before flushing. Defaults to `:infinity` (no
658+ buffering; events processed immediately as delivered). Requires
659+ `:batch_size`.
651660
652661 The default options supported by `GenServer.start_link/3` are supported,
653662 including the `:hibernate_after` option which allows the process to go
@@ -734,7 +743,8 @@ defmodule Commanded.Event.Handler do
734743 :subscribe_to ,
735744 :subscription_opts ,
736745 :state ,
737- :batch_size
746+ :batch_size ,
747+ :batch_timeout
738748 ]
739749
740750 @ doc false
@@ -753,6 +763,12 @@ defmodule Commanded.Event.Handler do
753763 "both `:concurrency` and `:batch_size` are specified, this is not yet supported. Please choose one or the other."
754764 end
755765
766+ if Keyword . has_key? ( config , :batch_timeout ) and not Keyword . has_key? ( config , :batch_size ) do
767+ raise ArgumentError ,
768+ inspect ( module ) <>
769+ " :batch_timeout requires :batch_size. Remove the timeout or configure batching."
770+ end
771+
756772 { application , config } = Keyword . pop ( config , :application )
757773
758774 unless application do
@@ -766,20 +782,37 @@ defmodule Commanded.Event.Handler do
766782 end
767783
768784 { batch_size , config } = Keyword . pop ( config , :batch_size )
785+ { batch_timeout , config } = Keyword . pop ( config , :batch_timeout , :infinity )
786+
787+ unless is_nil ( batch_size ) or ( is_integer ( batch_size ) and batch_size > 0 ) do
788+ raise ArgumentError ,
789+ inspect ( module ) <>
790+ " :batch_size must be nil or positive integer, got: " <> inspect ( batch_size )
791+ end
792+
793+ unless batch_timeout == :infinity or ( is_integer ( batch_timeout ) and batch_timeout > 0 ) do
794+ raise ArgumentError ,
795+ inspect ( module ) <>
796+ " :batch_timeout must be :infinity or positive integer, got: " <>
797+ inspect ( batch_timeout )
798+ end
769799
770800 config =
771801 case batch_size do
772802 nil ->
773- # Delegate to `handle_event/2` when `batch_size` is not specified
774- Keyword . put ( config , :handler_callback , :event )
803+ config
804+ |> Keyword . put ( :handler_callback , :event )
805+ |> Keyword . put ( :batch_size , nil )
806+ |> Keyword . put ( :batch_timeout , :infinity )
775807
776- size when is_integer ( size ) ->
808+ size when is_integer ( size ) and size > 0 ->
777809 config
778810 |> Keyword . update ( :subscription_opts , [ buffer_size: size ] , fn opts ->
779811 Keyword . put ( opts , :buffer_size , size )
780812 end )
781- # Delegate to `handle_batch/2` when `batch_size` is specified
782813 |> Keyword . put ( :handler_callback , :batch )
814+ |> Keyword . put ( :batch_size , size )
815+ |> Keyword . put ( :batch_timeout , batch_timeout )
783816 end
784817
785818 { application , name , config }
@@ -823,7 +856,11 @@ defmodule Commanded.Event.Handler do
823856 :handler_state ,
824857 :last_seen_event ,
825858 :subscription ,
826- :subscribe_timer
859+ :subscribe_timer ,
860+ :batch_size ,
861+ :batch_timer_ref ,
862+ batch_timeout: :infinity ,
863+ batch_buffer: [ ]
827864 ]
828865
829866 @ doc false
@@ -842,7 +879,11 @@ defmodule Commanded.Event.Handler do
842879 handler_callback: Keyword . fetch! ( handler_opts , :handler_callback ) ,
843880 handler_state: Keyword . get ( handler_opts , :state ) ,
844881 consistency: consistency ,
845- subscription: subscription
882+ subscription: subscription ,
883+ batch_size: Keyword . get ( handler_opts , :batch_size ) ,
884+ batch_timeout: Keyword . get ( handler_opts , :batch_timeout , :infinity ) ,
885+ batch_timer_ref: nil ,
886+ batch_buffer: [ ]
846887 }
847888
848889 with { :ok , pid } <- Registration . start_link ( application , name , __MODULE__ , handler , start_opts ) do
@@ -940,18 +981,22 @@ defmodule Commanded.Event.Handler do
940981 @ doc false
941982 @ impl GenServer
942983 def handle_info ( { :events , events } , state ) do
943- % Handler { handler_callback: callback } = state
944-
945- processor =
946- case callback do
947- :event -> fn events , state -> Enum . reduce ( events , state , & handle_event / 2 ) end
948- :batch -> & handle_batch / 2
949- end
984+ % Handler { handler_callback: callback , batch_timeout: batch_timeout } = state
950985
951986 Logger . debug ( describe ( state ) <> " received events: #{ inspect ( events ) } " )
952987
953988 try do
954- state = processor . ( events , state )
989+ state =
990+ case { callback , batch_timeout } do
991+ { :event , _ } ->
992+ Enum . reduce ( events , state , & handle_event / 2 )
993+
994+ { :batch , :infinity } ->
995+ handle_batch ( events , state )
996+
997+ { :batch , _timeout } ->
998+ buffer_and_maybe_flush ( events , state )
999+ end
9551000
9561001 { :noreply , state }
9571002 catch
@@ -961,6 +1006,24 @@ defmodule Commanded.Event.Handler do
9611006 end
9621007 end
9631008
1009+ @ doc false
1010+ @ impl GenServer
1011+ def handle_info ( :flush_batch_timeout , state ) do
1012+ % Handler { batch_buffer: buffer } = state
1013+
1014+ Logger . debug (
1015+ describe ( state ) <> " flushing batch due to timeout: #{ length ( buffer || [ ] ) } event(s)"
1016+ )
1017+
1018+ try do
1019+ state = flush_batch_buffer ( state , :timeout )
1020+ { :noreply , state }
1021+ catch
1022+ { :error , reason } ->
1023+ { :stop , reason , state }
1024+ end
1025+ end
1026+
9641027 @ doc false
9651028 @ impl GenServer
9661029 def handle_info (
@@ -1131,6 +1194,70 @@ defmodule Commanded.Event.Handler do
11311194 end
11321195 end
11331196
1197+ defp buffer_and_maybe_flush ( events , % Handler { } = state ) do
1198+ % Handler { batch_buffer: buffer , batch_size: batch_size } = state
1199+
1200+ new_buffer = ( buffer || [ ] ) ++ events
1201+ state = % Handler { state | batch_buffer: new_buffer } |> maybe_start_batch_timer ( )
1202+
1203+ if length ( new_buffer ) >= batch_size do
1204+ state
1205+ |> cancel_batch_timer ( )
1206+ |> flush_batch_buffer ( :size )
1207+ else
1208+ state
1209+ end
1210+ end
1211+
1212+ defp flush_batch_buffer ( % Handler { batch_buffer: buffer } = state , _flush_reason )
1213+ when buffer in [ nil , [ ] ] ,
1214+ do: state
1215+
1216+ defp flush_batch_buffer ( % Handler { batch_buffer: buffer } = state , flush_reason ) do
1217+ Logger . debug (
1218+ describe ( state ) <> " flushing batch of #{ length ( buffer ) } event(s) due to #{ flush_reason } "
1219+ )
1220+
1221+ state = % Handler { state | batch_buffer: [ ] , batch_timer_ref: nil }
1222+ handle_batch ( buffer , % { flush_reason: flush_reason } , state )
1223+ end
1224+
1225+ defp maybe_start_batch_timer ( % Handler { batch_buffer: buffer } = state ) when buffer in [ nil , [ ] ] ,
1226+ do: state
1227+
1228+ defp maybe_start_batch_timer ( % Handler { batch_timeout: :infinity } = state ) , do: state
1229+
1230+ defp maybe_start_batch_timer (
1231+ % Handler { batch_timer_ref: nil , batch_timeout: batch_timeout } = state
1232+ )
1233+ when is_integer ( batch_timeout ) do
1234+ timer_ref = Process . send_after ( self ( ) , :flush_batch_timeout , batch_timeout )
1235+ % Handler { state | batch_timer_ref: timer_ref }
1236+ end
1237+
1238+ defp maybe_start_batch_timer ( state ) , do: state
1239+
1240+ defp cancel_batch_timer ( % Handler { batch_timer_ref: nil } = state ) , do: state
1241+
1242+ defp cancel_batch_timer ( % Handler { batch_timer_ref: ref } = state ) do
1243+ case Process . cancel_timer ( ref ) do
1244+ false ->
1245+ drain_flush_batch_timeout_message ( )
1246+ % Handler { state | batch_timer_ref: nil }
1247+
1248+ _remaining ->
1249+ % Handler { state | batch_timer_ref: nil }
1250+ end
1251+ end
1252+
1253+ defp drain_flush_batch_timeout_message do
1254+ receive do
1255+ :flush_batch_timeout -> :ok
1256+ after
1257+ 0 -> :ok
1258+ end
1259+ end
1260+
11341261 defp handle_batch ( events , context \\ % { } , handler )
11351262
11361263 defp handle_batch ( events , context , % Handler { last_seen_event: last_seen_event } = state )
@@ -1538,7 +1665,8 @@ defmodule Commanded.Event.Handler do
15381665 recorded_event: nil ,
15391666 first_event_id: first_event . event_id ,
15401667 last_event_id: last_event . event_id ,
1541- event_count: length ( recorded_events )
1668+ event_count: length ( recorded_events ) ,
1669+ flush_reason: Map . get ( context , :flush_reason , :immediate )
15421670 }
15431671 end
15441672
0 commit comments