Version 13 of retry

Updated 2014-05-27 01:49:14 by samoc

samoc: retry is try more than once.

usage: retry count_var count body ?handler...? ?finally script?

e.g. try a few times to get a message through if the network is busy ...

retry count 3 {

    send_message "Hello"

} trap NetworkBusy {} {}

... or, retry with exponential backoff...

retry count 10 {

    send_message "Hello"

} trap NetworkBusy {msg info} {

    puts "$msg"
    after [expr {$count * $count * 100}]
}

It might occasionally be useful to retry for all errors:

retry count 3 {

    send_message "Hello"

} on error {} {}

Implementation:

proc retry {count_var count body args} {
    # Retry "body" up to "count" times for exceptions caught by "args".
    # "args" is a list of trap handlers: trap pattern variableList script...
    # The retry count is made visible through "count_var".

    assert string is wordchar $count_var
    assert string is integer $count
    assert {[lindex $args 0] in {trap on finally}}

    upvar $count_var i

    if {[lindex $args end-1] == "finally"} {
        set traps [lrange $args 0 end-2]
        set finally [lrange $args end-1 end]
    } else {
        set traps $args
        set finally {}
    }

    for {set i 1} {$i <= $count} {incr i} {

        # Try to execute "body". On success break from loop...
        uplevel [list try $body {*}$traps on ok {} break]

        # On the last attempt, suppress the trap handlers...
        if {$i + 1 == $count} {
            set traps {}
        }
    }
}

AMG: Some more explanation would be welcome. -- samoc sorry about the original unexplained version. Revised version is above.

This code appears to retry an operation a specified number of times, swallowing configurable errors and other such abnormal returns all but the final time, though giving the user the ability to specify the handlers. Your example shows waiting one second between retries.

Is this code intended to dodge a race condition? Maybe find another way to design your system such that the race doesn't exist. -- samoc: This code is intended to deal with interfacing to real-world non-determinism (networks, people, actuators, sensors etc...)

RLE (2014-05-25): samoc: Now you've posted, effectively, non-working code because you've left out your custom 'assert' implementation. However, if you are using Tcllib's assert then you need a "package require control" before defining the proc.

AMG: This [assert] looks fairly straightforward. From how it's being used, I assume this custom [assert] could be the following:

proc assert {test args} {
    if {[llength $args]} {
        set test \[[concat [list $test] $args]\]
    }
    if {![uplevel expr [list $test]]} {
        error "assert failure: $test"
    }
}

samoc: In fact, my version of assert looks like this:

proc assert {args} {
    # usage: assert command args...
    #    or: assert {expression}

    if {[llength $args] == 1} {
        if {[uplevel expr $args]} {
            return
        }
    } else {
        if {[{*}$args]} {
            return
        }
    }

    return -code error \
           -errorcode [list assert $args] \
           "Assertion Failed:\n    $args"
}

interp alias {} require {} assert

... but there is plenty of other information about Assertions elsewhere on the wiki.

The real version of retry that I use in my own code follows...

The version above is edited to make it more like plain Tcl. I have a special prize for anyone who can guess what language my custom version of proc is inspired by :)

proc retry {count_var count body args} {

    Retry "body" up to "count" times for exceptions caught by "args".
    "args" is a list of trap handlers: trap pattern variableList script...
    The retry count is made visible through "count_var".

} require {

    is wordchar $count_var
    is integer $count
    {[lfirst $args] in {trap on finally}}

} do {

    upvar $count_var i

    if {[lindex $args end-1] == "finally"} {
        set traps [lrange $args 0 end-2]
        set finally [lrange $args end-1 end]
    } else {
        set traps $args
        set finally {}
    }

    for {set i 1} {$i <= $count} {incr i} {

        # Try to execute "body". On success break from loop...
        uplevel [list try $body {*}$traps on ok {} break {*}$finally]

        # On the last attempt, suppress the trap handlers...
        if {$i + 1 == $count} {
            set traps {}
        }
    }
}

RLE (2014-05-26): First guess, it looks quite Lisp like, with a doc-string, a let clause to define variable bindings, although in your case it is assertions, and a body to execute.

samoc: mais non, ce n'est pas lisp!