Skip to content

Commit 8b3e008

Browse files
authored
Merge pull request #657 from prometheus/beorn7/registry
Make hash collisions in the registry much less likely
2 parents e0e84de + ee1078a commit 8b3e008

File tree

6 files changed

+117
-22
lines changed

6 files changed

+117
-22
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module github.com/prometheus/client_golang
22

33
require (
44
github.com/beorn7/perks v1.0.1
5+
github.com/cespare/xxhash/v2 v2.1.0
56
github.com/golang/protobuf v1.3.2
67
github.com/json-iterator/go v1.1.7
78
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
88
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
99
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
1010
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
11+
github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
12+
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
1113
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1214
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1315
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

prometheus/desc.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"sort"
2020
"strings"
2121

22+
"github.com/cespare/xxhash/v2"
2223
"github.com/golang/protobuf/proto"
2324
"github.com/prometheus/common/model"
2425

@@ -126,24 +127,24 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
126127
return d
127128
}
128129

129-
vh := hashNew()
130+
xxh := xxhash.New()
130131
for _, val := range labelValues {
131-
vh = hashAdd(vh, val)
132-
vh = hashAddByte(vh, separatorByte)
132+
xxh.WriteString(val)
133+
xxh.Write(separatorByteSlice)
133134
}
134-
d.id = vh
135+
d.id = xxh.Sum64()
135136
// Sort labelNames so that order doesn't matter for the hash.
136137
sort.Strings(labelNames)
137138
// Now hash together (in this order) the help string and the sorted
138139
// label names.
139-
lh := hashNew()
140-
lh = hashAdd(lh, help)
141-
lh = hashAddByte(lh, separatorByte)
140+
xxh.Reset()
141+
xxh.WriteString(help)
142+
xxh.Write(separatorByteSlice)
142143
for _, labelName := range labelNames {
143-
lh = hashAdd(lh, labelName)
144-
lh = hashAddByte(lh, separatorByte)
144+
xxh.WriteString(labelName)
145+
xxh.Write(separatorByteSlice)
145146
}
146-
d.dimHash = lh
147+
d.dimHash = xxh.Sum64()
147148

148149
d.constLabelPairs = make([]*dto.LabelPair, 0, len(constLabels))
149150
for n, v := range constLabels {

prometheus/metric.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424

2525
const separatorByte byte = 255
2626

27+
var separatorByteSlice = []byte{255} // For convenient use with xxhash.
28+
2729
// A Metric models a single sample value with its meta data being exported to
2830
// Prometheus. Implementations of Metric in this package are Gauge, Counter,
2931
// Histogram, Summary, and Untyped.

prometheus/registry.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"sync"
2626
"unicode/utf8"
2727

28+
"github.com/cespare/xxhash/v2"
2829
"github.com/golang/protobuf/proto"
2930
"github.com/prometheus/common/expfmt"
3031

@@ -266,7 +267,7 @@ func (r *Registry) Register(c Collector) error {
266267
descChan = make(chan *Desc, capDescChan)
267268
newDescIDs = map[uint64]struct{}{}
268269
newDimHashesByName = map[string]uint64{}
269-
collectorID uint64 // Just a sum of all desc IDs.
270+
collectorID uint64 // All desc IDs XOR'd together.
270271
duplicateDescErr error
271272
)
272273
go func() {
@@ -293,12 +294,12 @@ func (r *Registry) Register(c Collector) error {
293294
if _, exists := r.descIDs[desc.id]; exists {
294295
duplicateDescErr = fmt.Errorf("descriptor %s already exists with the same fully-qualified name and const label values", desc)
295296
}
296-
// If it is not a duplicate desc in this collector, add it to
297+
// If it is not a duplicate desc in this collector, XOR it to
297298
// the collectorID. (We allow duplicate descs within the same
298299
// collector, but their existence must be a no-op.)
299300
if _, exists := newDescIDs[desc.id]; !exists {
300301
newDescIDs[desc.id] = struct{}{}
301-
collectorID += desc.id
302+
collectorID ^= desc.id
302303
}
303304

304305
// Are all the label names and the help string consistent with
@@ -875,9 +876,9 @@ func checkMetricConsistency(
875876
}
876877

877878
// Is the metric unique (i.e. no other metric with the same name and the same labels)?
878-
h := hashNew()
879-
h = hashAdd(h, name)
880-
h = hashAddByte(h, separatorByte)
879+
h := xxhash.New()
880+
h.WriteString(name)
881+
h.Write(separatorByteSlice)
881882
// Make sure label pairs are sorted. We depend on it for the consistency
882883
// check.
883884
if !sort.IsSorted(labelPairSorter(dtoMetric.Label)) {
@@ -888,18 +889,19 @@ func checkMetricConsistency(
888889
dtoMetric.Label = copiedLabels
889890
}
890891
for _, lp := range dtoMetric.Label {
891-
h = hashAdd(h, lp.GetName())
892-
h = hashAddByte(h, separatorByte)
893-
h = hashAdd(h, lp.GetValue())
894-
h = hashAddByte(h, separatorByte)
892+
h.WriteString(lp.GetName())
893+
h.Write(separatorByteSlice)
894+
h.WriteString(lp.GetValue())
895+
h.Write(separatorByteSlice)
895896
}
896-
if _, exists := metricHashes[h]; exists {
897+
hSum := h.Sum64()
898+
if _, exists := metricHashes[hSum]; exists {
897899
return fmt.Errorf(
898900
"collected metric %q { %s} was collected before with the same name and label values",
899901
name, dtoMetric,
900902
)
901903
}
902-
metricHashes[h] = struct{}{}
904+
metricHashes[hSum] = struct{}{}
903905
return nil
904906
}
905907

prometheus/registry_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,3 +1070,90 @@ test_summary_count{name="foo"} 2
10701070
)
10711071
}
10721072
}
1073+
1074+
// collidingCollector is a collection of prometheus.Collectors,
1075+
// and is itself a prometheus.Collector.
1076+
type collidingCollector struct {
1077+
i int
1078+
name string
1079+
1080+
a, b, c, d prometheus.Collector
1081+
}
1082+
1083+
// Describe satisifies part of the prometheus.Collector interface.
1084+
func (m *collidingCollector) Describe(desc chan<- *prometheus.Desc) {
1085+
m.a.Describe(desc)
1086+
m.b.Describe(desc)
1087+
m.c.Describe(desc)
1088+
m.d.Describe(desc)
1089+
}
1090+
1091+
// Collect satisifies part of the prometheus.Collector interface.
1092+
func (m *collidingCollector) Collect(metric chan<- prometheus.Metric) {
1093+
m.a.Collect(metric)
1094+
m.b.Collect(metric)
1095+
m.c.Collect(metric)
1096+
m.d.Collect(metric)
1097+
}
1098+
1099+
// TestAlreadyRegistered will fail with the old, weaker hash function. It is
1100+
// taken from https://play.golang.org/p/HpV7YE6LI_4 , authored by @awilliams.
1101+
func TestAlreadyRegisteredCollision(t *testing.T) {
1102+
1103+
reg := prometheus.NewRegistry()
1104+
1105+
for i := 0; i < 10000; i++ {
1106+
// A collector should be considered unique if its name and const
1107+
// label values are unique.
1108+
1109+
name := fmt.Sprintf("test-collector-%010d", i)
1110+
1111+
collector := collidingCollector{
1112+
i: i,
1113+
name: name,
1114+
1115+
a: prometheus.NewCounter(prometheus.CounterOpts{
1116+
Name: "my_collector_a",
1117+
ConstLabels: prometheus.Labels{
1118+
"name": name,
1119+
"type": "test",
1120+
},
1121+
}),
1122+
b: prometheus.NewCounter(prometheus.CounterOpts{
1123+
Name: "my_collector_b",
1124+
ConstLabels: prometheus.Labels{
1125+
"name": name,
1126+
"type": "test",
1127+
},
1128+
}),
1129+
c: prometheus.NewCounter(prometheus.CounterOpts{
1130+
Name: "my_collector_c",
1131+
ConstLabels: prometheus.Labels{
1132+
"name": name,
1133+
"type": "test",
1134+
},
1135+
}),
1136+
d: prometheus.NewCounter(prometheus.CounterOpts{
1137+
Name: "my_collector_d",
1138+
ConstLabels: prometheus.Labels{
1139+
"name": name,
1140+
"type": "test",
1141+
},
1142+
}),
1143+
}
1144+
1145+
// Register should not fail, since each collector has a unique
1146+
// set of sub-collectors, determined by their names and const label values.
1147+
if err := reg.Register(&collector); err != nil {
1148+
alreadyRegErr, ok := err.(prometheus.AlreadyRegisteredError)
1149+
if !ok {
1150+
t.Fatal(err)
1151+
}
1152+
1153+
previous := alreadyRegErr.ExistingCollector.(*collidingCollector)
1154+
current := alreadyRegErr.NewCollector.(*collidingCollector)
1155+
1156+
t.Errorf("Unexpected registration error: %q\nprevious collector: %s (i=%d)\ncurrent collector %s (i=%d)", alreadyRegErr, previous.name, previous.i, current.name, current.i)
1157+
}
1158+
}
1159+
}

0 commit comments

Comments
 (0)