Debounce with maximum timeout

I would like to relay a bunch of changes in our database to the client. However, I don’t want to send these changes instantly. Instead I’d like to collect these events and send them in batches. If a new event arrives, I would like to debounce sending the events to the client. There’s one exception: if the debounce has lasted longer than 1 second, I’d like to flush the buffer and send things over.

Any good examples out there on how to implement using a GenServer? Thanks!

Initial reaction makes me think GenStage might be able to do this straight?

I would probably do something like:

  • include a timer_ref as part of the state of the GenServer, initialized to nil
  • have a cast-able message that can be used to send new events
  • in that handle_cast:
    • collect the messages (or counts, etc.) in the state
    • make a call to debounce(state) which would match on the timer_ref being nil and do a Process.send_after/4 of a message to self() for 1s later, or noop if there is already a timer_ref; this gives you your one second max
  • when that sent later message arrives, send the batched update to the client, and re-set the timer_ref to nil
  • should the message batch queue grow or some other abort trigger occur, cancel the pending message using the Process.cancel_timer/2

Which means a GenServer with two elements in its: one for the batches, one for the timer_ref; a handle_cast for batching, a handle_info for the send_after message to trigger the batched send, a debounce function to call send_after, … and that’s about it?

2 Likes

It sounds to me like gen_statem might be a much better solution for this use case - you basically have two states - waiting and collecting. With state timeout timers, this can lead to a very clean implementation.

You can take a look at https://github.com/michalmuskala/debounce for a similar example.

5 Likes

Here’s a very nice post on time outs with gen statems https://potatosalad.io/2017/10/13/time-out-elixir-state-machines-versus-servers.

3 Likes

Interesting timing, I wrote a blog post on a similar topic very recently https://engineering.tripping.com/how-to-implement-sliding-timeouts-in-your-elixir-genservers-cc5a2ed70db9 . The only tweak I would do to that code (https://gist.github.com/minhajuddin/292e93aa92330c9b003246d1f1e812c4#file-search_worker3-exs) would be to store the first_event_time using :erlang.monotonic_time and do a diff when it fires: https://gist.github.com/minhajuddin/292e93aa92330c9b003246d1f1e812c4#file-search_worker3-exs-L31 and if that goes over the threshold fire off the batch update.

1 Like

Without the need to guard against cancellation, those additional mechanics are not required though?

1 Like

Yeah, you are right about that! So, new refs aren’t really required because a batch update needs to go through every few seconds.

Thanks a lot everyone! Really appreciate all the suggestions. Going to give gen_statem a try and will post back with whatever solution I come up with next week.