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.
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 } } }
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
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
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 __REDELAY_COUNT 0 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 return } set interval [expr { min($max, (pow(2, [incr __REDELAY_COUNT]) - 1)) * 1000 }] set delay [expr { int(rand()*($interval-1+1)+1) + 1000 }] return $delay }
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 reset puts [redelay] puts [redelay] puts [redelay] puts [redelay] puts [redelay] redelay reset puts [redelay] puts [redelay]