3

I'm interested in wrapping a command such that it only runs at most once every X duration; essentially, the same functionality as the lodash throttle function. I'd basically like to be able to run this:

throttle 60 -- check-something
another-command
throttle 60 -- check-something
another-command
throttle 60 -- check-something

For each of those throttle commands, if it's been less than 60 seconds since check-something was run (successfully), the command is skipped. Does anything like this already exist? Is it easy to do with a shell script?

  • 1
    If any of the answers solved your problem, please accept it by clicking the checkmark next to it. Thank you! – Jeff Schaller Feb 24 at 14:22
3

I'm not aware of anything off-the-shelf, but a wrapper function could do the job. I've implemented one in bash using an associative array:

declare -A _throttled=()

throttle() {
  if [ "$#" -lt 2 ]
  then
    printf '%s\n' "Usage: throttle timeout command [arg ... ]" >&2
    return 1
  fi

  local t=$1
  shift

  if [ -n "${_throttled["$1"]}" ]
  then
        if [ "$(date +%s)" -ge "${_throttled["$1"]}" ]
        then
                "$@" && _throttled["$1"]=$((t + $(date +%s)))
        else
                : printf '%s\n' "Timeout for: $1 has not yet been reached" >&2
        fi
  else
        "$@" && _throttled["$1"]=$((t + $(date +%s)))
  fi
}

The basic logic is: if the command has an entry in the _throttle array, check the current time against the array value; if the timeout has expired, run the command and -- if the command was successful -- set a new timeout value. If the timeout has not yet expired, (don't) print an informative message. If, on the other hand, the command does not (yet) have an entry in the array, run the command and -- if the command was successful -- set a new timeout value.

The wrapper function doesn't distinguish commands based on any arguments, so throttle 30 ls is the same to it as throttle 30 ls /tmp. This is easily changed by replacing the array references and assignments of "$1" to "$@".

Also note that I dropped the -- from your example syntax.

Also note that this is limited to seconds-level resolution.

If you have bash version 4.2 or later, you may save the call to the external date command by using a feature of the printf built-in instead:

...
_throttled["$1"]=$((t + $(printf '%(%s)T\n' -1)))
...

... where we're asking for the time formatted in seconds (%s) explicitly of the current time (-1).

Or in bash 5.0 or later:

_throttled["$1"]=$((t + EPOCHSECONDS))
  • Note that this would require you to be running bash as your shell, running version 4+ (for the associative array support). – Jeff Schaller Feb 21 at 15:06
  • ksh, bash and zsh have a $SECONDS special variable which you may be able to use here instead of date +%s. – Stéphane Chazelas Feb 21 at 15:07
  • I considered using SECONDS, but didn't want to rely on the user not resetting it / using it for their own purposes. – Jeff Schaller Feb 21 at 15:10
  • bash5+ have $EPOCHREALTIME (from zsh) which would remove the 1 second imprecision in your approach, but bash doesn't do floats so it would be more difficult there. I'd use zsh or ksh93 here instead of bash. – Stéphane Chazelas Feb 21 at 15:18
  • See also printf -v to avoid the fork. – Stéphane Chazelas Feb 21 at 15:18
2

With zsh:

typeset -A last_run
zmodload zsh/datetime

throttle() {
  local delay=$1; shift

  # $cmd is the (minimally) quoted arguments of the command joined
  # with spaces and used as the key for the `$last_run` associative array
  local cmd="${(j: :)${(q+)@}}"
  local now=$EPOCHREALTIME
  local lr=$last_run[$cmd]
  local t=$((now - lr))

  if ((t < delay)); then
    printf >&2 '%s was already run successfully %.3g seconds ago\n' "$cmd" "$t"
    return 1
  else
    "$@" && last_run[$cmd]=$now
    # $now being the time when the command started, replace with
    # $EPOCHREALTIME if you want the time it finished.
  fi
}

throttle 3.5 echo "test 1 2 3"
sleep 2
throttle 3.5 echo "test 1 2 3"
sleep 4
throttle 3.5 echo "test 1 2 3"

That assumes all the instances of throttle for a given command are run in the same shell process (not in subshells).

  • thank you! I've updated mine correspondingly. – Jeff Schaller Feb 21 at 15:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.