Skip to content

Commit 6964e9e

Browse files
committed
support very low rates
1 parent fab23c3 commit 6964e9e

5 files changed

Lines changed: 46 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Only depends on the standard library.
1414
`go get github.com/linkdata/bwlimit`
1515

1616
`Ticker` must be created with `bwlimit.NewTicker()`. The zero-value `Ticker` is not supported.
17-
Limits are enforced in 100ms slices. Very low limits are therefore approximate rather than exact.
17+
Limits are enforced in 100ms slices with fractional carry-over between slices, so very low limits are accurate over time but can still be bursty at slice boundaries.
1818

1919
## Example
2020

limiter.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ type Limiter struct {
1515
// NewLimiter returns a new limiter from DefaultTicker.
1616
// If you provide limits, the first will set
1717
// both read and write limits, the second will set the write limit.
18-
// Limits are applied in 100ms slices, so very low rates are approximate.
18+
// Limits are applied in 100ms slices with fractional carry-over between
19+
// slices, so very low rates are accurate over time but can be bursty
20+
// at slice boundaries.
1921
//
2022
// To stop the Limiter and free it's resources, call Stop.
2123
func NewLimiter(limits ...int64) *Limiter {

operation.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func (op *Operation) run(ch chan<- int64) {
6868
stopCh := op.stopCh
6969
seccount := 0
7070
counts := make([]int64, secparts)
71+
carry := int64(0)
7172
op.mu.Unlock()
7273

7374
if stopCh != nil {
@@ -76,9 +77,16 @@ func (op *Operation) run(ch chan<- int64) {
7677
var todo int64
7778
var batch int64
7879
if limit := op.Limit.Load(); limit > 0 {
79-
limitCh = ch
80-
todo = max(1, limit/secparts)
81-
batch = min(batchsize, todo)
80+
carry += limit
81+
todo = carry / secparts
82+
carry = carry % secparts
83+
todo += op.avail.Swap(0)
84+
if todo > 0 {
85+
limitCh = ch
86+
batch = min(batchsize, todo)
87+
}
88+
} else {
89+
carry = 0
8290
}
8391
waitCh := op.WaitCh()
8492

operation_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,34 @@ func TestOperation_read_rate_low(t *testing.T) {
170170
}
171171
}
172172

173+
func TestOperation_read_rate_very_low(t *testing.T) {
174+
l := NewLimiter(5)
175+
defer l.Stop()
176+
177+
r := bytes.NewReader(make([]byte, 5))
178+
buf := make([]byte, 5)
179+
180+
now := time.Now()
181+
n, err := l.Reads.io(r.Read, buf)
182+
elapsed := time.Since(now)
183+
184+
if n != 5 {
185+
t.Fatal(n)
186+
}
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
191+
// 5 bytes at 5 bytes/sec should take about a second. The old fixed
192+
// 100ms grant floor completed in about 500ms.
193+
if elapsed < 700*time.Millisecond {
194+
t.Fatalf("too fast for very low limit: %v", elapsed)
195+
}
196+
if elapsed > 5*time.Second {
197+
t.Fatalf("too slow for very low limit: %v", elapsed)
198+
}
199+
}
200+
173201
func TestOperation_read_rate_high(t *testing.T) {
174202
const numbytes = (2 * 1024 * 1024 * 1024) - 1
175203
l := NewLimiter(numbytes)

ticker.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ func NewTicker() (ot *Ticker) {
2424
// NewLimiter returns a new Limiter using this Ticker.
2525
// If you provide limits, the first will set
2626
// both read and write limits, the second will set the write limit.
27-
// Limits are applied in 100ms slices, so very low rates are approximate.
27+
// Limits are applied in 100ms slices with fractional carry-over between
28+
// slices, so very low rates are accurate over time but can be bursty
29+
// at slice boundaries.
2830
//
2931
// To stop the limiter and free it's resources, call Stop.
3032
func (ot *Ticker) NewLimiter(limits ...int64) (l *Limiter) {

0 commit comments

Comments
 (0)