Skip to content

Commit c820272

Browse files
dcarleytsenart
authored andcommitted
Wait for results before exiting from signal
Previously the attack command would not wait for in-flight requests to finish before exiting from an interrupt signal. In the case where all requests take longer than the attack duration then the output file will be empty and reporting on it will produce an obscure error: % echo "GET http://172.18.0.254/will/timeout" | time vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out & sleep 1 && pkill -2 vegeta && fg && vegeta report vegeta.out [1] 12347 12348 [1] + 12347 done echo "GET http://172.18.0.254/will/timeout" | 12348 running time vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out 0.00s user 0.01s system 0% cpu 1.075 total 2023/01/11 21:35:50 encode: can't detect encoding of "vegeta.out" By omitting the return on the first call to `Stop()` we can use the results channel to block the exit until the attack has finished: % echo "GET http://172.18.0.254/will/timeout" | time ./vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out & sleep 1 && pkill -2 vegeta && fg && ./vegeta report vegeta.out [1] 12433 12434 [1] + 12433 done echo "GET http://172.18.0.254/will/timeout" | 12434 running time ./vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out ./vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out 0.00s user 0.01s system 0% cpu 11.012 total Requests [total, rate, throughput] 1, 1.00, 0.00 Duration [total, attack, wait] 10.001s, 0s, 10.001s Latencies [min, mean, 50, 90, 95, 99, max] 10.001s, 10.001s, 10.001s, 10.001s, 10.001s, 10.001s, 10.001s Bytes In [total, mean] 0, 0.00 Bytes Out [total, mean] 0, 0.00 Success [ratio] 0.00% Status Codes [code:count] 0:1 Error Set: Get "http://172.18.0.254/will/timeout": context deadline exceeded (Client.Timeout exceeded while awaiting headers) A subsequent interrupt signal (ie. `^C^C`) is honoured if you want to force an immediate exit: % echo "GET http://172.18.0.254/will/timeout" | time vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out & sleep 1 && pkill -2 vegeta && pkill -2 vegeta && fg [1] 12073 12074 vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out 0.00s user 0.01s system 1% cpu 1.057 total [1] + 12073 done echo "GET http://172.18.0.254/will/timeout" | 12074 done time vegeta attack -rate 1 -duration 0 -timeout 10s -output vegeta.out Testing this required a refactor of `attack()` in order to pass our own signal channel in. The diff is fortunately pretty simple though. Like most simple changes and async code, the majority of the changeset is testing it. Closes #611 Signed-off-by: Tomás Senart <tsenart@gmail.com>
1 parent 3e67d01 commit c820272

File tree

3 files changed

+154
-7
lines changed

3 files changed

+154
-7
lines changed

attack.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,27 @@ func attack(opts *attackOpts) (err error) {
195195
sig := make(chan os.Signal, 1)
196196
signal.Notify(sig, os.Interrupt)
197197

198+
return processAttack(atk, res, enc, sig)
199+
}
200+
201+
func processAttack(
202+
atk *vegeta.Attacker,
203+
res <-chan *vegeta.Result,
204+
enc vegeta.Encoder,
205+
sig <-chan os.Signal,
206+
) error {
198207
for {
199208
select {
200209
case <-sig:
201-
atk.Stop()
202-
return nil
210+
if stopSent := atk.Stop(); !stopSent {
211+
// Exit immediately on second signal.
212+
return nil
213+
}
203214
case r, ok := <-res:
204215
if !ok {
205216
return nil
206217
}
207-
if err = enc.Encode(r); err != nil {
218+
if err := enc.Encode(r); err != nil {
208219
return err
209220
}
210221
}

attack_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
package main
22

33
import (
4+
"bufio"
5+
"bytes"
6+
"io"
47
"net/http"
8+
"net/http/httptest"
9+
"os"
510
"reflect"
11+
"sync"
612
"testing"
13+
"time"
14+
15+
vegeta "github.com/tsenart/vegeta/v12/lib"
716
)
817

918
func TestHeadersSet(t *testing.T) {
@@ -26,3 +35,124 @@ func TestHeadersSet(t *testing.T) {
2635
}
2736
}
2837
}
38+
39+
func decodeMetrics(buf bytes.Buffer) (vegeta.Metrics, error) {
40+
var metrics vegeta.Metrics
41+
dec := vegeta.NewDecoder(bufio.NewReader(&buf))
42+
43+
for {
44+
var r vegeta.Result
45+
if err := dec.Decode(&r); err != nil {
46+
if err == io.EOF {
47+
break
48+
}
49+
return metrics, err
50+
}
51+
metrics.Add(&r)
52+
}
53+
metrics.Close()
54+
55+
return metrics, nil
56+
}
57+
58+
func TestAttackSignalOnce(t *testing.T) {
59+
t.Parallel()
60+
61+
const (
62+
signalDelay = 300 * time.Millisecond // Delay before stopping.
63+
clientTimeout = 1 * time.Second // This, plus delay, is the max time for the attack.
64+
serverTimeout = 2 * time.Second // Must be more than clientTimeout.
65+
attackDuration = 10 * time.Second // The attack should never take this long.
66+
)
67+
68+
server := httptest.NewServer(
69+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70+
time.Sleep(serverTimeout) // Server.Close() will block for this long on shutdown.
71+
}),
72+
)
73+
defer server.Close()
74+
75+
tr := vegeta.NewStaticTargeter(vegeta.Target{Method: "GET", URL: server.URL})
76+
atk := vegeta.NewAttacker(vegeta.Timeout(clientTimeout))
77+
rate := vegeta.Rate{Freq: 10, Per: time.Second} // Every 100ms.
78+
79+
var buf bytes.Buffer
80+
writer := bufio.NewWriter(&buf)
81+
enc := vegeta.NewEncoder(writer)
82+
sig := make(chan os.Signal, 1)
83+
res := atk.Attack(tr, rate, attackDuration, "")
84+
85+
var wg sync.WaitGroup
86+
wg.Add(1)
87+
go func() {
88+
defer wg.Done()
89+
processAttack(atk, res, enc, sig)
90+
}()
91+
92+
// Allow more than one request to have started before stopping.
93+
time.Sleep(signalDelay)
94+
sig <- os.Interrupt
95+
wg.Wait()
96+
writer.Flush()
97+
98+
metrics, err := decodeMetrics(buf)
99+
if err != nil {
100+
t.Error(err)
101+
}
102+
if got, min := metrics.Requests, uint64(2); got < min {
103+
t.Errorf("not enough requests recorded. got %+v, min: %+v", got, min)
104+
}
105+
if got, want := metrics.Success, 0.0; got != want {
106+
t.Errorf("all requests should fail. got %+v, want: %+v", got, want)
107+
}
108+
if got, max := metrics.Duration, clientTimeout; got > max {
109+
t.Errorf("attack duration too long. got %+v, max: %+v", got, max)
110+
}
111+
if got, want := metrics.Wait.Round(time.Second), clientTimeout; got != want {
112+
t.Errorf("attack wait doesn't match timeout. got %+v, want: %+v", got, want)
113+
}
114+
}
115+
116+
func TestAttackSignalTwice(t *testing.T) {
117+
t.Parallel()
118+
119+
const (
120+
attackDuration = 10 * time.Second // The attack should never take this long.
121+
)
122+
123+
server := httptest.NewServer(
124+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
125+
)
126+
defer server.Close()
127+
128+
tr := vegeta.NewStaticTargeter(vegeta.Target{Method: "GET", URL: server.URL})
129+
atk := vegeta.NewAttacker()
130+
rate := vegeta.Rate{Freq: 1, Per: time.Second}
131+
132+
var buf bytes.Buffer
133+
writer := bufio.NewWriter(&buf)
134+
enc := vegeta.NewEncoder(writer)
135+
sig := make(chan os.Signal, 1)
136+
res := atk.Attack(tr, rate, attackDuration, "")
137+
138+
var wg sync.WaitGroup
139+
wg.Add(1)
140+
go func() {
141+
defer wg.Done()
142+
processAttack(atk, res, enc, sig)
143+
}()
144+
145+
// Exit as soon as possible.
146+
sig <- os.Interrupt
147+
sig <- os.Interrupt
148+
wg.Wait()
149+
writer.Flush()
150+
151+
metrics, err := decodeMetrics(buf)
152+
if err != nil {
153+
t.Error(err)
154+
}
155+
if got, max := metrics.Duration, time.Second; got > max {
156+
t.Errorf("attack duration too long. got %+v, max: %+v", got, max)
157+
}
158+
}

lib/attack.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Attacker struct {
2222
dialer *net.Dialer
2323
client http.Client
2424
stopch chan struct{}
25+
stopOnce sync.Once
2526
workers uint64
2627
maxWorkers uint64
2728
maxBody int64
@@ -68,6 +69,7 @@ var (
6869
func NewAttacker(opts ...func(*Attacker)) *Attacker {
6970
a := &Attacker{
7071
stopch: make(chan struct{}),
72+
stopOnce: sync.Once{},
7173
workers: DefaultWorkers,
7274
maxWorkers: DefaultMaxWorkers,
7375
maxBody: DefaultMaxBody,
@@ -325,13 +327,17 @@ func (a *Attacker) Attack(tr Targeter, p Pacer, du time.Duration, name string) <
325327
return results
326328
}
327329

328-
// Stop stops the current attack.
329-
func (a *Attacker) Stop() {
330+
// Stop stops the current attack. The return value indicates whether this call
331+
// has signalled the attack to stop (`true` for the first call) or whether it
332+
// was a noop because it has been previously signalled to stop (`false` for any
333+
// subsequent calls).
334+
func (a *Attacker) Stop() bool {
330335
select {
331336
case <-a.stopch:
332-
return
337+
return false
333338
default:
334-
close(a.stopch)
339+
a.stopOnce.Do(func() { close(a.stopch) })
340+
return true
335341
}
336342
}
337343

0 commit comments

Comments
 (0)