Skip to content

Commit

Permalink
add request delay
Browse files Browse the repository at this point in the history
  • Loading branch information
Tantalor93 committed Sep 1, 2024
1 parent 0677cda commit c5b09e1
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 9 deletions.
5 changes: 5 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ func init() {
"the workers will use separate connections. Disabled by default.").
Default("false").BoolVar(&benchmark.SeparateWorkerConnections)

pApp.Flag("request-delay", "Configures delay to be added before each request done by worker. Delay can be either constant or randomized. "+
"Constant delay is configured as single duration <GO duration> (e.g. 500ms, 2s, etc.). Randomized delay is configured as interval of "+
"two durations <GO duration>-<GO duration> (e.g. 1s-2s, 500ms-2s, etc.), where the actual delay is random value from the interval that "+
"is randomized after each request.").Default("0s").StringVar(&benchmark.RequestDelay)

pApp.Arg("queries", "Queries to issue. It can be a local file referenced using @<file-path>, for example @data/2-domains. "+
"It can also be resource accessible using HTTP, like https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/1000-domains, in that "+
"case, the file will be downloaded and saved in-memory. "+
Expand Down
8 changes: 8 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ Flags:
--log-requests-path="requests.log"
Specifies path to the file, where the request logs will be logged. If the file exists, the logs will be
appended to the file. If the file does not exist, the file will be created.
--[no-]separate-worker-connections
Controls whether the concurrent workers will try to share connections to the server or not. When enabled
the workers will use separate connections. Disabled by default.
--request-delay="0s" Configures delay to be added before each request done by worker. Delay can be either constant or
randomized. Constant delay is configured as single duration <GO duration> (e.g. 500ms, 2s, etc.).
Randomized delay is configured as interval of two durations <GO duration>-<GO duration> (e.g. 1s-2s,
500ms-2s, etc.), where the actual delay is random value from the interval that is randomized after each
request.
--[no-]version Show application version.
Args:
Expand Down
29 changes: 29 additions & 0 deletions docs/requestdelay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: Delaying requests
layout: default
parent: Examples
---

# Delaying requests
v3.4.0
{: .label .label-yellow }
*dnspyre* by default generates queries one after another as soon as the previous query is finished. In some cases you might want to delay
the queries. This is possible using `--request-delay` flag. This option allows user to specify either constant or randomized delay to be added
before sending query.

## Constant delay
To specify constant delay, you can specify arbitrary GO duration as parameter to the `--request-delay` flag. Each parallel worker will
always wait for the specified duration before sending another query

```
dnspyre --duration 10s --server '1.1.1.1' google.com --request-delay 2s
```

## Randomized delay
To specify randomized delay, you can specify interval of GO durations `<GO duration>-<GO duration>` as parameter to the `--request-delay` flag.
Each parallel worker will always wait for the random duration specified by the interval. If you specify interval `1s-2s`, workers will wait
between 1 second and 2 seconds before sending another query

```
dnspyre --duration 10s --server '1.1.1.1' google.com --request-delay 1s-2s
```
77 changes: 73 additions & 4 deletions pkg/dnsbench/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -197,9 +198,14 @@ type Benchmark struct {
// Writer used for writing benchmark execution logs and results. Default is os.Stdout.
Writer io.Writer

// RequestDelay configures delay between each DNS request. Either constant delay can be configured (e.g. 2s) or randomized delay can be configured (e.g. 1s-2s).
RequestDelay string

// internal variable so we do not have to parse the address with each request.
useDoH bool
useQuic bool
useDoH bool
useQuic bool
requestDelayStart time.Duration
requestDelayEnd time.Duration
}

type queryFunc func(context.Context, string, *dns.Msg) (*dns.Msg, error)
Expand Down Expand Up @@ -267,6 +273,40 @@ func (b *Benchmark) init() error {
b.RequestLogPath = DefaultRequestLogPath
}

if err := b.parseRequestDelay(); err != nil {
return err
}

return nil
}

func (b *Benchmark) parseRequestDelay() error {
if len(b.RequestDelay) == 0 {
return nil
}
requestDelayRegex := regexp.MustCompile(`^(\d+(?:ms|ns|[smhdw]))(?:-(\d+(?:ms|ns|[smhdw])))?$`)

durations := requestDelayRegex.FindStringSubmatch(b.RequestDelay)
if len(durations) != 3 {
return fmt.Errorf("'%s' has unexpected format, either <GO duration> or <GO duration>-<Go duration> is expected", b.RequestDelay)
}
if len(durations[1]) != 0 {
durationStart, err := time.ParseDuration(durations[1])
if err != nil {
return err
}
b.requestDelayStart = durationStart
}
if len(durations[2]) != 0 {
durationEnd, err := time.ParseDuration(durations[2])
if err != nil {
return err
}
b.requestDelayEnd = durationEnd
}
if b.requestDelayEnd > 0 && b.requestDelayStart > 0 && b.requestDelayEnd-b.requestDelayStart <= 0 {
return fmt.Errorf("'%s' is invalid interval, start should be strictly less than end", b.RequestDelay)
}
return nil
}

Expand Down Expand Up @@ -331,7 +371,7 @@ func (b *Benchmark) Run(ctx context.Context) ([]*ResultStats, error) {
var bar *progressbar.ProgressBar
var incrementBar bool
if repetitions := b.Count * int64(b.Concurrency) * int64(len(b.Types)) * int64(len(questions)); !b.Silent && b.ProgressBar && repetitions >= 100 {
fmt.Fprintln(b.Writer)
fmt.Fprintln(os.Stderr)
if b.Probability < 1.0 {
// show spinner when Benchmark.Probability is less than 1.0, because the actual number of repetitions is not known
repetitions = -1
Expand All @@ -340,9 +380,10 @@ func (b *Benchmark) Run(ctx context.Context) ([]*ResultStats, error) {
incrementBar = true
}
if !b.Silent && b.ProgressBar && b.Duration >= 10*time.Second {
fmt.Fprintln(b.Writer)
fmt.Fprintln(os.Stderr)
bar = progressbar.Default(int64(b.Duration.Seconds()), "Progress:")
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
go func() {
for {
select {
Expand Down Expand Up @@ -446,17 +487,45 @@ func (b *Benchmark) Run(ctx context.Context) ([]*ResultStats, error) {
if incrementBar {
bar.Add(1)
}

b.delay(ctx, rando)
}
}
}
}(w, st)
}

wg.Wait()
if bar != nil {
_ = bar.Exit()
}

return stats, nil
}

func (b *Benchmark) delay(ctx context.Context, rando *rand.Rand) {
switch {
case b.requestDelayStart > 0 && b.requestDelayEnd > 0:
delay := time.Duration(rando.Int63n(int64(b.requestDelayEnd-b.requestDelayStart))) + b.requestDelayStart
waitFor(ctx, delay)
case b.requestDelayStart > 0:
waitFor(ctx, b.requestDelayStart)
default:
}
}

func waitFor(ctx context.Context, dur time.Duration) {
timer := time.NewTimer(dur)
defer timer.Stop()

select {
case <-timer.C:
// slept for requested duration
case <-ctx.Done():
// sleep interrupted
}
}

func (b *Benchmark) network() string {
if b.useDoH {
_, network := isHTTPUrl(b.Server)
Expand Down
80 changes: 80 additions & 0 deletions pkg/dnsbench/benchmark_plaindns_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -682,3 +682,83 @@ func (suite *PlainDNSTestSuite) TestBenchmark_Requestlog() {

assertRequestLogStructure(suite.T(), requestLogFile)
}

func (suite *PlainDNSTestSuite) TestBenchmark_ConstantRequestDelay() {
s := NewServer(dnsbench.UDPTransport, nil, func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, A("example.org. IN A 127.0.0.1"))

w.WriteMsg(ret)
})
defer s.Close()

bench := dnsbench.Benchmark{
Queries: []string{"example.org"},
Types: []string{"A", "AAAA"},
Server: s.Addr,
TCP: false,
Concurrency: 2,
Count: 1,
Probability: 1,
WriteTimeout: 1 * time.Second,
ReadTimeout: 3 * time.Second,
ConnectTimeout: 1 * time.Second,
RequestTimeout: 5 * time.Second,
Rcodes: true,
Recurse: true,
DNSSEC: true,
RequestDelay: "1s",
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

start := time.Now()
rs, err := bench.Run(ctx)
benchDuration := time.Since(start)

suite.Require().NoError(err, "expected no error from benchmark run")
assertResult(suite.T(), rs)
suite.InDelta(2*time.Second, benchDuration, float64(100*time.Millisecond))
}

func (suite *PlainDNSTestSuite) TestBenchmark_RandomRequestDelay() {
s := NewServer(dnsbench.UDPTransport, nil, func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, A("example.org. IN A 127.0.0.1"))

w.WriteMsg(ret)
})
defer s.Close()

bench := dnsbench.Benchmark{
Queries: []string{"example.org"},
Types: []string{"A", "AAAA"},
Server: s.Addr,
TCP: false,
Concurrency: 2,
Count: 1,
Probability: 1,
WriteTimeout: 1 * time.Second,
ReadTimeout: 3 * time.Second,
ConnectTimeout: 1 * time.Second,
RequestTimeout: 5 * time.Second,
Rcodes: true,
Recurse: true,
DNSSEC: true,
RequestDelay: "1s-2s",
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

start := time.Now()
rs, err := bench.Run(ctx)
benchDuration := time.Since(start)

suite.Require().NoError(err, "expected no error from benchmark run")
assertResult(suite.T(), rs)
suite.InDelta(4*time.Second, benchDuration, float64(time.Second))
}
32 changes: 27 additions & 5 deletions pkg/dnsbench/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (

func TestBenchmark_init(t *testing.T) {
tests := []struct {
name string
benchmark Benchmark
wantServer string
wantRequestLogPath string
wantErr bool
name string
benchmark Benchmark
wantServer string
wantRequestLogPath string
wantErr bool
wantRequestDelayStart time.Duration
wantRequestDelayEnd time.Duration
}{
{
name: "server - IPv4",
Expand Down Expand Up @@ -107,6 +109,24 @@ func TestBenchmark_init(t *testing.T) {
wantServer: "8.8.8.8:53",
wantRequestLogPath: DefaultRequestLogPath,
},
{
name: "constant delay",
benchmark: Benchmark{Server: "8.8.8.8", RequestDelay: "2s"},
wantServer: "8.8.8.8:53",
wantRequestDelayStart: 2 * time.Second,
},
{
name: "random delay",
benchmark: Benchmark{Server: "8.8.8.8", RequestDelay: "2s-3s"},
wantServer: "8.8.8.8:53",
wantRequestDelayStart: 2 * time.Second,
wantRequestDelayEnd: 3 * time.Second,
},
{
name: "invalid delay",
benchmark: Benchmark{Server: "8.8.8.8", RequestDelay: "invalid"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -116,6 +136,8 @@ func TestBenchmark_init(t *testing.T) {
if !tt.wantErr {
assert.Equal(t, tt.wantServer, tt.benchmark.Server)
assert.Equal(t, tt.wantRequestLogPath, tt.benchmark.RequestLogPath)
assert.Equal(t, tt.wantRequestDelayStart, tt.benchmark.requestDelayStart)
assert.Equal(t, tt.wantRequestDelayEnd, tt.benchmark.requestDelayEnd)
}
})
}
Expand Down

0 comments on commit c5b09e1

Please sign in to comment.