Exponential Backoff Delay

Exponential Backoff Delay

Napier / Dash Automation, LLC

So I needed an exponential backoff delay for reconnecting to our Websocket Server when our servers go down. I decided to implement it using a simple coroutine and the "redelay" command (reconnect delay). Exponential Backoff is used to schedule reconnects to a server so that when your server connection is re-established every instance of your scripts won't reconnect at the same exact time. If you want to learn more I found How to Reconnect Web Sockets in a Realtime Web App without Flooding the Server to be very helpful.

I have put together a few different methods of using the redelay algorithm below which I use for various needs and cases throughout my various scripts. If you guys see any errors or problems feel free to post here with any discussions, tips, or changes.

redelay with coroutines

Basically it will generate a delay in ms for which is a random number between 1 second and the maximum delay. Each time you call the coroutine it will increase the maximum and return a new delay to use. You can set a name for the redelay or call it normally. You can also set the maximum seconds (default is 30) by using the second argument.

Important: You will want to make sure to kill the coroutine by giving any non empty value to it ($d cleanup, $d break, $d done - doesn't matter). If no name is provided we increment a count variable so that each redelay call will generate a new redelay context. If you do provide a name, each call to redelay will reset the previous.

namespace eval redelay { variable count 0 }

proc redelay {  {name {}} {max 30} } {
  if {$name eq {}} { append name ::redelay::task [incr ::redelay::count] }
  return [coroutine $name ::redelay::delay $max]

proc ::redelay::delay { max } {
  yield [info coroutine]
  set attempts 1
  while 1 {
    set interval [expr { min($max, (pow(2, $attempts) - 1)) * 1000 }]
    set delay [expr { int(rand()*($interval-1+1)+1) }]
    incr attempts
    if {[yield $delay] ne {}} { return }

redelay coroutine example 1

Here is an example of it being used (your delays will vary from the ones shown here since it is using random)

set d [redelay]       ; # ::redelay::task1
puts [$d]             ; # 566
puts [$d]             ; # 2071
puts [$d]             ; # 1303
puts [$d]             ; # 5375
puts [$d]             ; # 24724
puts [$d]             ; # 9369
puts [$d]             ; # 11873
puts [$d]             ; # 4104
$d break

redelay coroutine example 2

Here is an example using a custom name and max time. It still returns the variable but giving it the name will create a command with that name (be careful of overlap) so you can also just use the name itself:

redelay delay 120     ; # delay
puts [delay]             ; # 105
puts [delay]             ; # 428
puts [delay]             ; # 4958
puts [delay]             ; # 11442
puts [delay]             ; # 9572
puts [delay]             ; # 25054
puts [delay]             ; # 90278
puts [delay]             ; # 13372
delay break

redelay as a TclOO Mixin

This is the way I use the redelay the most. It is used as a mixin to a TclOO Class/Object. You define it so that each object can have its own redelay context.

::oo::class create ::RedelayMixin        {}

::oo::define ::RedelayMixin constructor args {
  my variable __REDELAY_COUNT
  set response {}
  catch { set response [next {*}$args] }
  return $response

::oo::define ::RedelayMixin method redelay { {max 30} } {
  my variable __REDELAY_COUNT
  if {$max eq "reset"} { 
    set __REDELAY_COUNT 0
  set interval [expr { min($max, (pow(2, [incr __REDELAY_COUNT]) - 1)) * 1000 }]
  set delay    [expr { int(rand()*($interval-1+1)+1) + 1000 }]
  return $delay

redelay without coroutines

If you don't like the coroutine flavor which allows you to have multiple instances and imo has a nicer syntax, you can also use this version of it which utilizes the count variable to increment your interval. Giving it "reset" as an argument will reset the counter to 0.

namespace eval redelay { variable count 0 }
proc redelay { {max 30} } {
  variable ::redelay::count
  if {$max eq "reset"} { set count 0; return }
  set interval [expr { min($max, (pow(2, [incr count]) - 1)) * 1000 }]
  return [expr { int(rand()*($interval-1+1)+1) }]

redelay without coroutine example 1

redelay reset
puts [redelay]
puts [redelay]
puts [redelay]
puts [redelay]
puts [redelay]
redelay reset
puts [redelay]
puts [redelay]