Skip to content

Instantly share code, notes, and snippets.

@luciferous
Created January 10, 2012 01:10
Show Gist options
  • Save luciferous/1586130 to your computer and use it in GitHub Desktop.
Save luciferous/1586130 to your computer and use it in GitHub Desktop.
Retryable continuations

This is an experiment in retrying code that can sometimes throw errors. Consider a function login which returns true if the password argument is correct.

    login = (password) -> password == "456"

Then another function tryPasswords which attempts to login with a list of passwords.

    tryPasswords = (passwords) ->
      for password in passwords
        return password if login password
      throw "Failed to login with any given passwords"

We could use it like so:

    try
      password = tryPasswords ["foo", "bar", "baz"]
      console.log "Logged in with #{password}!"
    catch error
      console.log error

Since this example is already hopelessly contrived, let's imagine the correct password in login was somehow continuously changing. And so we'd like to retry the password list at intervals.

    run = ->
      password = tryPasswords ["foo", "bar", "baz"]
      console.log "Logged in with #{password}!"
    
    try
      run()
    catch error
      console.log error
      setTimeout run, 1000 # Retry after 1000

But this doesn't work, setTimeout will only run once since the thrown exception is outside of the call stack of our error handler.

    run = (attempt, maxAttempts) ->
      try
        password = tryPasswords ["foo", "bar", "baz"]
        console.log "Logged in with #{password}!"
      catch error
        console.log error
        setTimeout (-> run ++attempt, maxAttempts), 1000
    run 0, 5

OK. Now let's say we don't want to retry indefinitely, but only allow a maximum number of attempts. Additionally, we want to increase the delay length at each iteration.

    run = (attempt, maxAttempts, delay) ->
      try
        password = tryPasswords ["foo", "bar", "baz"]
        console.log "Logged in with #{password}!"
      catch error
        console.log error
        if attempt < maxAttempts
          delay *= 2
          setTimeout (-> run ++attempt, maxAttempts), delay
        else
          console.log "Failed to find a valid password"
    run 0, 5, 1000

Why does this code make me want to kill myself? Why can't we do this:

    try
      password = tryPasswords ["foo", "bar", "baz"]
      console.log password
    catch error
      this.retry()

If we don't mind changing the syntax a little, we can get all the functionality we want like so:

    tryPasswords ["foo", "bar", "baz"],
      return: (password) -> console.log password
      error: -> this.retry()

The above will try ["foo, "bar", "baz"] indefinitely with no delay between attempts. The following adds parameters to the retry behavior as well as increasing delay length at each iteration.

    tryPasswords ["foo", "bar", "baz"],
      return: console.log
      error: (e) ->
        if this.attempts < 5
          delay this.retry, 1000
            attempt: this.attempts
            advance: (x) -> 2 * x
        else
          console.log e

Here's how it's done. We first have to modify tryPasswords by adding retryable just before the function declaration.

    tryPasswords = retryable (passwords) ->

Then we change return and throw to this.return and this.error respectively.

      for password in passwords
        this.return password if login password
      this.error "Failed to login with any given passwords"

And, that's it!

There are two utilities at work in the above examples:

  • retryable wraps a function to give it retry powers
  • delay wraps a function to give it timing powers
retryable = (fn) -> (args..., callbacks) ->
cont =
attempts: 0
return: -> callbacks.return.apply cont, arguments
error: -> callbacks.error.apply cont, arguments
retry: ->
cont.attempts++
fn.apply cont, args
fn.apply cont, args
delay = (fn, initial, options={}) ->
options.attempt or= 0
options.advance or= (x) -> x
delayL = initial
for i in [0..options.attempt]
delayL = options.advance delayL
setTimeout fn, delayL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment