Skip to content

Commit c29478e

Browse files
skaslevjcchavezs
authored andcommitted
Fix HTTP reporter potential unbounded goroutine creation (#146)
* Fix HTTP reporter potential unbounded goroutine creation Currently the HTTP reporter spawns a new goroutine each time a new batch needs to be sent to the server. Execution of those goroutines gets serialized through the `sendMtx`. This behavior is problematic when the zipkin server is down and leads to creation of unbounded number of goroutines each of which will wait for its turn on the `sendMtx` and then fail. Fix this by creating one send goroutine to handle sending data to the server. This also removes the need for the `sendMtx` since `sendBatch` will only be called by the send goroutine. Signed-off-by: Slavomir Kaslev <[email protected]> * Add more HTTP reporter test cases Add HTTP reporter test cases for BatchInterval() and BatchSize() options behavior. Signed-off-by: Slavomir Kaslev <[email protected]>
1 parent e33faeb commit c29478e

File tree

2 files changed

+133
-47
lines changed

2 files changed

+133
-47
lines changed

reporter/http/http.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ type httpReporter struct {
4545
batchInterval time.Duration
4646
batchSize int
4747
maxBacklog int
48-
sendMtx *sync.Mutex
4948
batchMtx *sync.Mutex
5049
batch []*model.SpanModel
5150
spanC chan *model.SpanModel
51+
sendC chan struct{}
5252
quit chan struct{}
5353
shutdown chan error
5454
reqCallback RequestCallbackFn
@@ -80,24 +80,35 @@ func (r *httpReporter) loop() {
8080
currentBatchSize := r.append(span)
8181
if currentBatchSize >= r.batchSize {
8282
nextSend = time.Now().Add(r.batchInterval)
83-
go func() {
84-
_ = r.sendBatch()
85-
}()
83+
r.enqueueSend()
8684
}
8785
case <-tickerChan:
8886
if time.Now().After(nextSend) {
8987
nextSend = time.Now().Add(r.batchInterval)
90-
go func() {
91-
_ = r.sendBatch()
92-
}()
88+
r.enqueueSend()
9389
}
9490
case <-r.quit:
95-
r.shutdown <- r.sendBatch()
91+
close(r.sendC)
9692
return
9793
}
9894
}
9995
}
10096

97+
func (r *httpReporter) sendLoop() {
98+
for range r.sendC {
99+
_ = r.sendBatch()
100+
}
101+
r.shutdown <- r.sendBatch()
102+
}
103+
104+
func (r *httpReporter) enqueueSend() {
105+
select {
106+
case r.sendC <- struct{}{}:
107+
default:
108+
// Do nothing if there's a pending send request already
109+
}
110+
}
111+
101112
func (r *httpReporter) append(span *model.SpanModel) (newBatchSize int) {
102113
r.batchMtx.Lock()
103114

@@ -114,10 +125,6 @@ func (r *httpReporter) append(span *model.SpanModel) (newBatchSize int) {
114125
}
115126

116127
func (r *httpReporter) sendBatch() error {
117-
// in order to prevent sending the same batch twice
118-
r.sendMtx.Lock()
119-
defer r.sendMtx.Unlock()
120-
121128
// Select all current spans in the batch to be sent
122129
r.batchMtx.Lock()
123130
sendBatch := r.batch[:]
@@ -232,9 +239,9 @@ func NewReporter(url string, opts ...ReporterOption) reporter.Reporter {
232239
maxBacklog: defaultMaxBacklog,
233240
batch: []*model.SpanModel{},
234241
spanC: make(chan *model.SpanModel),
242+
sendC: make(chan struct{}, 1),
235243
quit: make(chan struct{}, 1),
236244
shutdown: make(chan error, 1),
237-
sendMtx: &sync.Mutex{},
238245
batchMtx: &sync.Mutex{},
239246
serializer: reporter.JSONSerializer{},
240247
}
@@ -244,6 +251,7 @@ func NewReporter(url string, opts ...ReporterOption) reporter.Reporter {
244251
}
245252

246253
go r.loop()
254+
go r.sendLoop()
247255

248256
return &r
249257
}

reporter/http/http_test.go

Lines changed: 112 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,28 @@
1515
package http_test
1616

1717
import (
18+
"bytes"
19+
"encoding/json"
1820
"io/ioutil"
1921
"net/http"
2022
"net/http/httptest"
23+
"sync/atomic"
2124
"testing"
22-
23-
"fmt"
2425
"time"
2526

26-
"strings"
27-
2827
"github.com/openzipkin/zipkin-go/idgenerator"
2928
"github.com/openzipkin/zipkin-go/model"
29+
"github.com/openzipkin/zipkin-go/reporter"
3030
zipkinhttp "github.com/openzipkin/zipkin-go/reporter/http"
3131
)
3232

33-
func TestSpanIsBeingReported(t *testing.T) {
33+
func generateSpans(n int) []*model.SpanModel {
34+
spans := make([]*model.SpanModel, n)
3435
idGen := idgenerator.NewRandom64()
3536
traceID := idGen.TraceID()
3637

37-
nSpans := 2
38-
var aSpans []model.SpanModel
39-
var eSpans []string
40-
41-
for i := 0; i < nSpans; i++ {
42-
span := model.SpanModel{
38+
for i := 0; i < n; i++ {
39+
spans[i] = &model.SpanModel{
4340
SpanContext: model.SpanContext{
4441
TraceID: traceID,
4542
ID: idGen.SpanID(traceID),
@@ -48,44 +45,125 @@ func TestSpanIsBeingReported(t *testing.T) {
4845
Kind: model.Client,
4946
Timestamp: time.Now(),
5047
}
51-
52-
aSpans = append(aSpans, span)
53-
eSpans = append(
54-
eSpans,
55-
fmt.Sprintf(
56-
`{"timestamp":%d,"traceId":"%s","id":"%s","name":"%s","kind":"%s"}`,
57-
span.Timestamp.Round(time.Microsecond).UnixNano()/1e3,
58-
span.SpanContext.TraceID,
59-
span.SpanContext.ID,
60-
span.Name,
61-
span.Kind,
62-
),
63-
)
6448
}
6549

66-
eSpansPayload := fmt.Sprintf("[%s]", strings.Join(eSpans, ","))
50+
return spans
51+
}
6752

68-
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53+
func newTestServer(t *testing.T, spans []*model.SpanModel, serializer reporter.SpanSerializer, onReceive func(int)) *httptest.Server {
54+
sofar := 0
55+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6956
if r.Method != "POST" {
7057
t.Errorf("expected 'POST' request, got '%s'", r.Method)
7158
}
7259

73-
aSpanPayload, err := ioutil.ReadAll(r.Body)
60+
aPayload, err := ioutil.ReadAll(r.Body)
7461
if err != nil {
75-
t.Errorf("unexpected error: %s", err.Error())
62+
t.Errorf("unexpected error: %v", err)
7663
}
7764

78-
if eSpansPayload != string(aSpanPayload) {
79-
t.Errorf("unexpected span payload \nwant %s, \nhave %s\n", eSpansPayload, string(aSpanPayload))
65+
var aSpans []*model.SpanModel
66+
err = json.Unmarshal(aPayload, &aSpans)
67+
if err != nil {
68+
t.Errorf("failed to parse json payload: %v", err)
69+
}
70+
eSpans := spans[sofar : sofar+len(aSpans)]
71+
sofar += len(aSpans)
72+
onReceive(len(aSpans))
73+
74+
ePayload, err := serializer.Serialize(eSpans)
75+
if err != nil {
76+
t.Errorf("unexpected error: %v", err)
77+
}
78+
79+
if !bytes.Equal(aPayload, ePayload) {
80+
t.Errorf("unexpected span payload\nhave %s\nwant %s", string(aPayload), string(ePayload))
8081
}
8182
}))
83+
}
84+
85+
func TestSpanIsBeingReported(t *testing.T) {
86+
serializer := reporter.JSONSerializer{}
87+
88+
var numSpans int64
89+
eNumSpans := 2
90+
spans := generateSpans(eNumSpans)
91+
ts := newTestServer(t, spans, serializer, func(num int) { atomic.AddInt64(&numSpans, int64(num)) })
92+
defer ts.Close()
93+
94+
rep := zipkinhttp.NewReporter(ts.URL, zipkinhttp.Serializer(serializer))
95+
for _, span := range spans {
96+
rep.Send(*span)
97+
}
98+
rep.Close()
99+
100+
aNumSpans := int(atomic.LoadInt64(&numSpans))
101+
if aNumSpans != eNumSpans {
102+
t.Errorf("unexpected number of spans received\nhave: %d, want: %d", aNumSpans, eNumSpans)
103+
}
104+
}
105+
106+
func TestSpanIsReportedOnTime(t *testing.T) {
107+
serializer := reporter.JSONSerializer{}
108+
batchInterval := 200 * time.Millisecond
82109

110+
var numSpans int64
111+
eNumSpans := 2
112+
spans := generateSpans(eNumSpans)
113+
ts := newTestServer(t, spans, serializer, func(num int) { atomic.AddInt64(&numSpans, int64(num)) })
83114
defer ts.Close()
84115

85-
rep := zipkinhttp.NewReporter(ts.URL)
86-
defer rep.Close()
116+
rep := zipkinhttp.NewReporter(ts.URL,
117+
zipkinhttp.Serializer(serializer),
118+
zipkinhttp.BatchInterval(batchInterval))
119+
120+
for _, span := range spans {
121+
rep.Send(*span)
122+
}
123+
124+
time.Sleep(3 * batchInterval / 2)
125+
126+
aNumSpans := int(atomic.LoadInt64(&numSpans))
127+
if aNumSpans != eNumSpans {
128+
t.Errorf("unexpected number of spans received\nhave: %d, want: %d", aNumSpans, eNumSpans)
129+
}
130+
131+
rep.Close()
132+
}
133+
134+
func TestSpanIsReportedAfterBatchSize(t *testing.T) {
135+
serializer := reporter.JSONSerializer{}
136+
batchSize := 2
137+
138+
var numSpans int64
139+
eNumSpans := 6
140+
spans := generateSpans(eNumSpans)
141+
ts := newTestServer(t, spans, serializer, func(num int) { atomic.AddInt64(&numSpans, int64(num)) })
142+
defer ts.Close()
143+
144+
rep := zipkinhttp.NewReporter(ts.URL,
145+
zipkinhttp.Serializer(serializer),
146+
zipkinhttp.BatchSize(batchSize))
147+
148+
for _, span := range spans[:batchSize] {
149+
rep.Send(*span)
150+
}
151+
152+
time.Sleep(100 * time.Millisecond)
153+
154+
aNumSpans := int(atomic.LoadInt64(&numSpans))
155+
if aNumSpans != batchSize {
156+
t.Errorf("unexpected number of spans received\nhave: %d, want: %d", aNumSpans, batchSize)
157+
}
158+
159+
for _, span := range spans[batchSize:] {
160+
rep.Send(*span)
161+
}
162+
163+
rep.Close()
87164

88-
for _, span := range aSpans {
89-
rep.Send(span)
165+
aNumSpans = int(atomic.LoadInt64(&numSpans))
166+
if aNumSpans != eNumSpans {
167+
t.Errorf("unexpected number of spans received\nhave: %d, want: %d", aNumSpans, eNumSpans)
90168
}
91169
}

0 commit comments

Comments
 (0)