I was refactoring a bunch of initialisation scripts today and I found a bunch of places where I was retrying something with jittered arithmetic backoff. The details aren't all that important, except to say that I wanted to factor this out.
To do this, I needed to be able to pass a function to another
function in bash. The language provides no direct facility for
this, but it does have eval which allows us
to implement this functionality.
# Jittered backoff # # usage: # backoff_retry CONDITION [OPERATION] # # CONDITION should return success if we have succeeded in the # operation. # # OPERATION should be the operation to perform (if different from # CONDITION). # # If OPERATION is not present, the CONDITION is assumed to be the # operation and is not tested again. This is to prevent inadvertant # double retries. # backoff_retry() { count=0 sleepy_time=0 # The first parameter is our exit condition, so we extract it condition=$1 shift # Try until we succeed while ! $condition; do echo "Trying '$@'" # Here's where we execute the code we want to retry eval "$@" # We skip the check if the condition is also the operation, otherwise # we end up trying everything twice if [ "$#" != "0" ]; then $condition && break fi # Add a random number of seconds (0-9) to the sleep time and # update the counter sleepy_time=$(($sleepy_time+$RANDOM*10/32768)) count=$(($count+1)) echo "Failed [$condition] running '$@' - try $count, sleeping $sleepy_time" "warn" sleep $sleepy_time done }
The counter is purely informational, but it should be fairly trivial to add an option to limit the number of tries. Also, while you can pass raw commands rather than functions, it's probably worth writing a function for anything longer than a single command.
# Here are some examples of using the above retry function # Using just CONDITION: backoff_retry 'aptitude update' # Using CONDITION and OPERATION: backoff_retry '[ -f index.html ]' 'wget http://www.google.com/index.html' # Using a function as the operation: get_file() { domain=`cat domain.txt` wget http://$domain/index.html } backoff_retry '[ -f index.html ]' 'get_file'
There are several more interesting things you can do with eval and functions:
# You can bind functions to arbitrary names at runtime, giving you the # closest bash can probably get to lambdas and closures: bindfun() { name=$1 shift eval "$name() { $@; }" } # You can bind different functions based on input: foofun() { if $1; then bindfun $2 'echo foo' else bindfun $2 'echo bar' fi } # You can reduce functions by currying: add() { echo $(( $1 + $2 )) } bindfun inc 'add 1 "$@"' bindfun dec 'add -1 "$@"'