throttle

Anyone who ends up writing a load tester will soon arrive at the need for something to execute a function a number of times per second for a number of seconds… In the wonderful load testing CLI tool hey, JBD accomplishes this elegantly with channels, optionally configuring and waiting on a ticker channel:

var throttle <-chan time.Time
if b.QPS > 0 {
	throttle = time.Tick(time.Duration(1e6/(b.QPS)) * time.Microsecond)
}

...

if b.QPS > 0 {
	<-throttle
}

Here’s some code that demonstrates the throttling acheived with this code. It will run 10 queries per second, waiting inside the for loop if a value greater than zero is provided. If no value is provided for queries per second, the application will run as fast as possible:

package main

import (
	"fmt"
	"time"
)

func main() {
	qps := 5

	var throttle <-chan time.Time
	if qps > 0 {
		throttle = time.Tick(time.Duration(1e6/(qps)) * time.Microsecond)
	}

	for i := 0; i < 10; i++ {
		if qps > 0 {
			<-throttle
		}
		fmt.Println(time.Now().Format(time.RFC3339))
	}
}

When run, this code will output something similar to the following, note that only 5 things are printed in any 1 second period:

2019-05-28T14:44:04+03:00
2019-05-28T14:44:04+03:00
2019-05-28T14:44:04+03:00
2019-05-28T14:44:04+03:00
2019-05-28T14:44:05+03:00
2019-05-28T14:44:05+03:00
2019-05-28T14:44:05+03:00
2019-05-28T14:44:05+03:00
2019-05-28T14:44:05+03:00
2019-05-28T14:44:06+03:00
throttle

To save future headaches, copy pasta, and codebase crawls, I’ve created a package that reduces the amount of manual wait code required.

// Create a throttle to run something 5 times per second:
t := throttle.New(5, time.Second)

Once a throttle has been created, choose one of the following methods depending on your use case, both will operate at a resolution of 5 operations per second, as configured in the throttle:

t.Do(context.Background(), total int, f func()) - Run a total number of queries. Useful if you know how many requests you’d like to run ahead of time.

t.DoFor(context.Background(), d time.Duration, f func()) - Run for a given duration. Useful if you know how long you’d like to run for but don’t know how many requests in total.

Under the hood, a simple function calculates what “30 requests per minute” means as a portion of the resolution you’ve provided and returns it as something to be used to feed the throttle:

func qos(rate int64, res time.Duration) time.Duration {
	micros := res.Nanoseconds()
	return time.Duration(micros/rate) * time.Nanosecond
}

For example:

qos(1, time.Hour)           // 1h0m0s
qos(1, time.Minute)         // 1m0s
qos(1, time.Second)         // 1s
qos(1, time.Millisecond)    // 1ms
qos(1, time.Microsecond)    // 1µs
qos(60, time.Hour)          // 1m0s
qos(60, time.Minute)        // 1s
qos(1000, time.Second)      // 1ms
qos(1000, time.Millisecond) // 1µs
qos(1000, time.Microsecond) // 1ns

If you need to cancel the Do function at any time, pass a cancellable context.Context as follows:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

t.Do(ctx...
Examples

The following example demonstrates a throttle configured with a resolution of 5 requests per second, running for 10 operations, as in the first example:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/codingconcepts/throttle"
)

func main() {
	t := throttle.New(5, time.Second)
	t.Do(context.Background(), 10, func() {
		fmt.Println(time.Now().Format(time.RFC3339))
	})
}

The output confirms that 10 operations were invoked, with no more than 5 things being printed at any one time:

2019-05-28T14:48:19+03:00
2019-05-28T14:48:20+03:00
2019-05-28T14:48:20+03:00
2019-05-28T14:48:20+03:00
2019-05-28T14:48:20+03:00
2019-05-28T14:48:20+03:00
2019-05-28T14:48:21+03:00
2019-05-28T14:48:21+03:00
2019-05-28T14:48:21+03:00
2019-05-28T14:48:21+03:00

The following example demonstrates a throttle configured with a resolution of 5 requests per second, running for 2 seconds:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/codingconcepts/throttle"
)

func main() {
	t := throttle.New(5, time.Second)
	t.DoFor(context.Background(), time.Second*2, func() {
		fmt.Println(time.Now().Format(time.RFC3339))
	})
}

Once again, we can see that the function has been invoked 10 times:

2019-05-28T14:56:41+03:00
2019-05-28T14:56:41+03:00
2019-05-28T14:56:41+03:00
2019-05-28T14:56:41+03:00
2019-05-28T14:56:42+03:00
2019-05-28T14:56:42+03:00
2019-05-28T14:56:42+03:00
2019-05-28T14:56:42+03:00
2019-05-28T14:56:42+03:00
2019-05-28T14:56:43+03:00