Skip to content

Commit 39e7c23

Browse files
authored
Merge pull request #1627 from prometheus/release-1.20
Merge Release 1.20 back to main
2 parents ac114f3 + 05fcde9 commit 39e7c23

File tree

3 files changed

+83
-31
lines changed

3 files changed

+83
-31
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
## Unreleased
22

3+
* [BUGFIX] histograms: Fix possible data race when appending exemplars vs metrics gather. #1623
4+
5+
## 1.20.3 / 2024-09-05
6+
7+
* [BUGFIX] histograms: Fix possible data race when appending exemplars. #1608
8+
39
## 1.20.2 / 2024-08-23
410

511
* [BUGFIX] promhttp: Unset Content-Encoding header when data is uncompressed. #1596

prometheus/histogram.go

+71-29
Original file line numberDiff line numberDiff line change
@@ -844,9 +844,7 @@ func (h *histogram) Write(out *dto.Metric) error {
844844
}}
845845
}
846846

847-
// If exemplars are not configured, the cap will be 0.
848-
// So append is not needed in this case.
849-
if cap(h.nativeExemplars.exemplars) > 0 {
847+
if h.nativeExemplars.isEnabled() {
850848
h.nativeExemplars.Lock()
851849
his.Exemplars = append(his.Exemplars, h.nativeExemplars.exemplars...)
852850
h.nativeExemplars.Unlock()
@@ -1658,10 +1656,17 @@ func addAndResetCounts(hot, cold *histogramCounts) {
16581656
type nativeExemplars struct {
16591657
sync.Mutex
16601658

1661-
ttl time.Duration
1659+
// Time-to-live for exemplars, it is set to -1 if exemplars are disabled, that is NativeHistogramMaxExemplars is below 0.
1660+
// The ttl is used on insertion to remove an exemplar that is older than ttl, if present.
1661+
ttl time.Duration
1662+
16621663
exemplars []*dto.Exemplar
16631664
}
16641665

1666+
func (n *nativeExemplars) isEnabled() bool {
1667+
return n.ttl != -1
1668+
}
1669+
16651670
func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
16661671
if ttl == 0 {
16671672
ttl = 5 * time.Minute
@@ -1673,6 +1678,7 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
16731678

16741679
if maxCount < 0 {
16751680
maxCount = 0
1681+
ttl = -1
16761682
}
16771683

16781684
return nativeExemplars{
@@ -1682,20 +1688,18 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
16821688
}
16831689

16841690
func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
1685-
if cap(n.exemplars) == 0 {
1691+
if !n.isEnabled() {
16861692
return
16871693
}
16881694

16891695
n.Lock()
16901696
defer n.Unlock()
16911697

1692-
// The index where to insert the new exemplar.
1693-
var nIdx int = -1
1694-
16951698
// When the number of exemplars has not yet exceeded or
16961699
// is equal to cap(n.exemplars), then
16971700
// insert the new exemplar directly.
16981701
if len(n.exemplars) < cap(n.exemplars) {
1702+
var nIdx int
16991703
for nIdx = 0; nIdx < len(n.exemplars); nIdx++ {
17001704
if *e.Value < *n.exemplars[nIdx].Value {
17011705
break
@@ -1705,17 +1709,46 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17051709
return
17061710
}
17071711

1712+
if len(n.exemplars) == 1 {
1713+
// When the number of exemplars is 1, then
1714+
// replace the existing exemplar with the new exemplar.
1715+
n.exemplars[0] = e
1716+
return
1717+
}
1718+
// From this point on, the number of exemplars is greater than 1.
1719+
17081720
// When the number of exemplars exceeds the limit, remove one exemplar.
17091721
var (
1710-
rIdx int // The index where to remove the old exemplar.
1711-
1712-
ot = time.Now() // Oldest timestamp seen.
1713-
otIdx = -1 // Index of the exemplar with the oldest timestamp.
1714-
1715-
md = -1.0 // Logarithm of the delta of the closest pair of exemplars.
1716-
mdIdx = -1 // Index of the older exemplar within the closest pair.
1717-
cLog float64 // Logarithm of the current exemplar.
1718-
pLog float64 // Logarithm of the previous exemplar.
1722+
ot = time.Time{} // Oldest timestamp seen. Initial value doesn't matter as we replace it due to otIdx == -1 in the loop.
1723+
otIdx = -1 // Index of the exemplar with the oldest timestamp.
1724+
1725+
md = -1.0 // Logarithm of the delta of the closest pair of exemplars.
1726+
1727+
// The insertion point of the new exemplar in the exemplars slice after insertion.
1728+
// This is calculated purely based on the order of the exemplars by value.
1729+
// nIdx == len(n.exemplars) means the new exemplar is to be inserted after the end.
1730+
nIdx = -1
1731+
1732+
// rIdx is ultimately the index for the exemplar that we are replacing with the new exemplar.
1733+
// The aim is to keep a good spread of exemplars by value and not let them bunch up too much.
1734+
// It is calculated in 3 steps:
1735+
// 1. First we set rIdx to the index of the older exemplar within the closest pair by value.
1736+
// That is the following will be true (on log scale):
1737+
// either the exemplar pair on index (rIdx-1, rIdx) or (rIdx, rIdx+1) will have
1738+
// the closest values to each other from all pairs.
1739+
// For example, suppose the values are distributed like this:
1740+
// |-----------x-------------x----------------x----x-----|
1741+
// ^--rIdx as this is older.
1742+
// Or like this:
1743+
// |-----------x-------------x----------------x----x-----|
1744+
// ^--rIdx as this is older.
1745+
// 2. If there is an exemplar that expired, then we simple reset rIdx to that index.
1746+
// 3. We check if by inserting the new exemplar we would create a closer pair at
1747+
// (nIdx-1, nIdx) or (nIdx, nIdx+1) and set rIdx to nIdx-1 or nIdx accordingly to
1748+
// keep the spread of exemplars by value; otherwise we keep rIdx as it is.
1749+
rIdx = -1
1750+
cLog float64 // Logarithm of the current exemplar.
1751+
pLog float64 // Logarithm of the previous exemplar.
17191752
)
17201753

17211754
for i, exemplar := range n.exemplars {
@@ -1726,7 +1759,7 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17261759
}
17271760

17281761
// Find the index at which to insert new the exemplar.
1729-
if *e.Value <= *exemplar.Value && nIdx == -1 {
1762+
if nIdx == -1 && *e.Value <= *exemplar.Value {
17301763
nIdx = i
17311764
}
17321765

@@ -1738,11 +1771,13 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17381771
}
17391772
diff := math.Abs(cLog - pLog)
17401773
if md == -1 || diff < md {
1774+
// The closest exemplar pair is at index: i-1, i.
1775+
// Choose the exemplar with the older timestamp for replacement.
17411776
md = diff
17421777
if n.exemplars[i].Timestamp.AsTime().Before(n.exemplars[i-1].Timestamp.AsTime()) {
1743-
mdIdx = i
1778+
rIdx = i
17441779
} else {
1745-
mdIdx = i - 1
1780+
rIdx = i - 1
17461781
}
17471782
}
17481783

@@ -1753,8 +1788,12 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17531788
if nIdx == -1 {
17541789
nIdx = len(n.exemplars)
17551790
}
1791+
// Here, we have the following relationships:
1792+
// n.exemplars[nIdx-1].Value < e.Value (if nIdx > 0)
1793+
// e.Value <= n.exemplars[nIdx].Value (if nIdx < len(n.exemplars))
17561794

17571795
if otIdx != -1 && e.Timestamp.AsTime().Sub(ot) > n.ttl {
1796+
// If the oldest exemplar has expired, then replace it with the new exemplar.
17581797
rIdx = otIdx
17591798
} else {
17601799
// In the previous for loop, when calculating the closest pair of exemplars,
@@ -1764,23 +1803,26 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17641803
if nIdx > 0 {
17651804
diff := math.Abs(elog - math.Log(n.exemplars[nIdx-1].GetValue()))
17661805
if diff < md {
1806+
// The value we are about to insert is closer to the previous exemplar at the insertion point than what we calculated before in rIdx.
1807+
// v--rIdx
1808+
// |-----------x-n-----------x----------------x----x-----|
1809+
// nIdx-1--^ ^--new exemplar value
1810+
// Do not make the spread worse, replace nIdx-1 and not rIdx.
17671811
md = diff
1768-
mdIdx = nIdx
1769-
if n.exemplars[nIdx-1].Timestamp.AsTime().Before(e.Timestamp.AsTime()) {
1770-
mdIdx = nIdx - 1
1771-
}
1812+
rIdx = nIdx - 1
17721813
}
17731814
}
17741815
if nIdx < len(n.exemplars) {
17751816
diff := math.Abs(math.Log(n.exemplars[nIdx].GetValue()) - elog)
17761817
if diff < md {
1777-
mdIdx = nIdx
1778-
if n.exemplars[nIdx].Timestamp.AsTime().Before(e.Timestamp.AsTime()) {
1779-
mdIdx = nIdx
1780-
}
1818+
// The value we are about to insert is closer to the next exemplar at the insertion point than what we calculated before in rIdx.
1819+
// v--rIdx
1820+
// |-----------x-----------n-x----------------x----x-----|
1821+
// new exemplar value--^ ^--nIdx
1822+
// Do not make the spread worse, replace nIdx-1 and not rIdx.
1823+
rIdx = nIdx
17811824
}
17821825
}
1783-
rIdx = mdIdx
17841826
}
17851827

17861828
// Adjust the slice according to rIdx and nIdx.

prometheus/histogram_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -1049,10 +1049,14 @@ func TestNativeHistogramConcurrency(t *testing.T) {
10491049

10501050
go func(vals []float64) {
10511051
start.Wait()
1052-
for _, v := range vals {
1052+
for i, v := range vals {
10531053
// An observation every 1 to 10 seconds.
10541054
atomic.AddInt64(&ts, rand.Int63n(10)+1)
1055-
his.Observe(v)
1055+
if i%2 == 0 {
1056+
his.Observe(v)
1057+
} else {
1058+
his.(ExemplarObserver).ObserveWithExemplar(v, Labels{"foo": "bar"})
1059+
}
10561060
}
10571061
end.Done()
10581062
}(vals)

0 commit comments

Comments
 (0)