Skip to content

Commit ccebff9

Browse files
fix(http): concurrent map read/write (#5753)
* fix(http): concurrent map read/write Signed-off-by: ivan katliarchuk <[email protected]> * fix(http): concurrent map read/write Signed-off-by: ivan katliarchuk <[email protected]> * fix(http): concurrent map read/write Signed-off-by: ivan katliarchuk <[email protected]> * fix(http): concurrent map read/write Signed-off-by: ivan katliarchuk <[email protected]> * fix(http): concurrent map read/write Signed-off-by: ivan katliarchuk <[email protected]> --------- Signed-off-by: ivan katliarchuk <[email protected]>
1 parent 9cbe200 commit ccebff9

File tree

8 files changed

+132
-285
lines changed

8 files changed

+132
-285
lines changed

pkg/http/http.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import (
2929
)
3030

3131
var (
32-
RequestDurationLabels = metrics.NewLabels([]string{"scheme", "host", "path", "method", "status"})
3332
RequestDurationMetric = metrics.NewSummaryVecWithOpts(
3433
prometheus.SummaryOpts{
3534
Name: "request_duration_seconds",
@@ -38,7 +37,7 @@ var (
3837
ConstLabels: prometheus.Labels{"handler": "instrumented_http"},
3938
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
4039
},
41-
*RequestDurationLabels,
40+
[]string{metrics.LabelScheme, metrics.LabelHost, metrics.LabelPath, metrics.LabelMethod, metrics.LabelStatus},
4241
)
4342
)
4443

@@ -64,15 +63,13 @@ func (r *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
6463
status = fmt.Sprintf("%d", resp.StatusCode)
6564
}
6665

67-
RequestDurationLabels.WithOptions(
68-
metrics.WithLabel("scheme", req.URL.Scheme),
69-
metrics.WithLabel("host", req.URL.Host),
70-
metrics.WithLabel("path", metrics.PathProcessor(req.URL.Path)),
71-
metrics.WithLabel("method", req.Method),
72-
metrics.WithLabel("status", status),
73-
)
74-
75-
RequestDurationMetric.SetWithLabels(time.Since(start).Seconds(), RequestDurationLabels)
66+
RequestDurationMetric.SetWithLabels(time.Since(start).Seconds(), metrics.Labels{
67+
metrics.LabelScheme: req.URL.Scheme,
68+
metrics.LabelHost: req.URL.Host,
69+
metrics.LabelPath: metrics.PathProcessor(req.URL.Path),
70+
metrics.LabelMethod: req.Method,
71+
metrics.LabelStatus: status,
72+
})
7673

7774
return resp, err
7875
}

pkg/http/http_benchmark_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package http
18+
19+
import (
20+
"bytes"
21+
"io"
22+
"net/http"
23+
"sync"
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
type roundTripFunc func(req *http.Request) *http.Response
31+
32+
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
33+
return f(req), nil
34+
}
35+
36+
// newTestClient returns *http.client with Transport replaced to avoid making real calls
37+
func newTestClient(fn roundTripFunc) *http.Client {
38+
return &http.Client{
39+
Transport: NewInstrumentedTransport(fn),
40+
}
41+
}
42+
43+
type apiUnderTest struct {
44+
client *http.Client
45+
baseURL string
46+
}
47+
48+
func (api *apiUnderTest) doStuff() ([]byte, error) {
49+
resp, err := api.client.Get(api.baseURL + "/some/path")
50+
if err != nil {
51+
return nil, err
52+
}
53+
defer resp.Body.Close()
54+
return io.ReadAll(resp.Body)
55+
}
56+
57+
func BenchmarkRoundTripper(b *testing.B) {
58+
client := newTestClient(func(req *http.Request) *http.Response {
59+
return &http.Response{
60+
StatusCode: http.StatusOK,
61+
Body: io.NopCloser(bytes.NewBufferString(`OK`)),
62+
Header: make(http.Header),
63+
}
64+
})
65+
66+
for b.Loop() {
67+
api := apiUnderTest{client, "http://example.com"}
68+
body, err := api.doStuff()
69+
require.NoError(b, err)
70+
assert.Equal(b, []byte("OK"), body)
71+
}
72+
}
73+
74+
func TestRoundTripper_Concurrent(t *testing.T) {
75+
client := newTestClient(func(req *http.Request) *http.Response {
76+
return &http.Response{
77+
StatusCode: http.StatusOK,
78+
Body: io.NopCloser(bytes.NewBufferString(`OK`)),
79+
Header: make(http.Header),
80+
}
81+
})
82+
api := &apiUnderTest{client: client, baseURL: "http://example.com"}
83+
84+
const numGoroutines = 100
85+
var wg sync.WaitGroup
86+
wg.Add(numGoroutines)
87+
88+
for i := 0; i < numGoroutines; i++ {
89+
go func() {
90+
defer wg.Done()
91+
body, err := api.doStuff()
92+
assert.NoError(t, err)
93+
assert.Equal(t, []byte("OK"), body)
94+
}()
95+
}
96+
wg.Wait()
97+
}

pkg/metrics/labels.go

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,62 +17,15 @@ limitations under the License.
1717
package metrics
1818

1919
import (
20-
"sort"
21-
"strings"
22-
23-
"github.com/sirupsen/logrus"
20+
"github.com/prometheus/client_golang/prometheus"
2421
)
2522

26-
type Labels struct {
27-
values map[string]string
28-
}
29-
30-
func (labels *Labels) GetKeysInOrder() []string {
31-
keys := make([]string, 0, len(labels.values))
32-
for key := range labels.values {
33-
keys = append(keys, key)
34-
}
35-
36-
sort.Strings(keys)
37-
38-
return keys
39-
}
40-
41-
func (labels *Labels) GetValuesOrderedByKey() []string {
42-
var orderedValues []string
43-
for _, key := range labels.GetKeysInOrder() {
44-
orderedValues = append(orderedValues, labels.values[key])
45-
}
46-
47-
return orderedValues
48-
}
49-
50-
type LabelOption func(*Labels)
51-
52-
func NewLabels(labelNames []string) *Labels {
53-
labels := &Labels{
54-
values: make(map[string]string),
55-
}
56-
57-
for _, label := range labelNames {
58-
labels.values[strings.ToLower(label)] = ""
59-
}
60-
61-
return labels
62-
}
63-
64-
func (labels *Labels) WithOptions(options ...LabelOption) {
65-
for _, option := range options {
66-
option(labels)
67-
}
68-
}
23+
const (
24+
LabelScheme = "scheme"
25+
LabelHost = "host"
26+
LabelPath = "path"
27+
LabelMethod = "method"
28+
LabelStatus = "status"
29+
)
6930

70-
func WithLabel(labelName string, labelValue string) LabelOption {
71-
return func(labels *Labels) {
72-
if _, ok := labels.values[strings.ToLower(labelName)]; !ok {
73-
logrus.Errorf("Attempting to set a value for a label that doesn't exist! '%s' does not exist!", labelName)
74-
} else {
75-
labels.values[strings.ToLower(labelName)] = labelValue
76-
}
77-
}
78-
}
31+
type Labels = prometheus.Labels

0 commit comments

Comments
 (0)