Skip to content

Commit b888807

Browse files
rdemellowdaverigby
authored andcommitted
MB-41510: Fix infinite loop due to HdrHistogram being reset
Currently if a HdrHistogram has its reset() method called by one thread (T1) and is being read by another (T2) using an HdrHistoram::Iterator. There is the race that could cause the HdrHistoram::Iterator() to enter an infinite loop. This occurs as HdrHistogram's getNextValue() methods use HdrHistogam_c's hdr_iter_next() which intern uses the following function: static bool has_next(struct hdr_iter* iter) { return iter->cumulative_count < iter->total_count; } Which logically checks that the total count the Iterator was initialize with is > than the sum of all the counts its read from the buckets in the histogram. The trouble is, that the reset() method changes the bucket counts back to 0. Thus, meaning the condition cumulative_count >= total_count may never be met. To fix this wrap the std::unique_ptr<struct hdr_histogram> with a folly::Synchronized<>. This allows us to take an exclusive lock on the pointer when performing a reset() of the histogram and allows us to ensure that no iterator is reading from the histogram, as the iterator takes a non exclusive read lock for the lifetime of the HdrHistogram::Iterator struct. This also ensures that HdrHistogram::resize() is thread safe as we now take a write lock. Change-Id: I8cf5eeb0ac3107ce20e915463be3db245c68c8b9 Reviewed-on: http://review.couchbase.org/c/kv_engine/+/137228 Reviewed-by: Dave Rigby <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent 12c23e4 commit b888807

File tree

4 files changed

+136
-55
lines changed

4 files changed

+136
-55
lines changed

engines/ep/src/item_eviction.cc

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,5 @@ uint8_t ItemEviction::convertFreqCountToNRUValue(uint8_t probCounter) {
6969
}
7070

7171
void ItemEviction::copyFreqHistogram(HdrHistogram& hist) {
72-
HdrHistogram::Iterator iter{
73-
freqHistogram.makeLinearIterator(valueUnitsPerBucket)};
74-
while (auto result = freqHistogram.getNextValueAndCount(iter)) {
75-
hist.addValueAndCount(result->first, result->second);
76-
}
72+
hist = freqHistogram;
7773
}

engines/ep/tests/module_tests/hdrhistogram_test.cc

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@
1818
#include "thread_gate.h"
1919

2020
#include <folly/portability/GTest.h>
21-
#include <hdr_histogram.h>
22-
#include <optional>
23-
2421
#include <cmath>
25-
#include <iomanip>
2622
#include <memory>
23+
#include <optional>
2724
#include <random>
2825
#include <thread>
2926
#include <utility>
@@ -468,3 +465,60 @@ TEST(HdrHistogramTest, int64MaxSizeTest) {
468465
// Testing any higher than this gives us garbage results back but
469466
// unfortunately with no way of knowing that they're garbage.
470467
}
468+
469+
static std::vector<std::optional<std::pair<uint64_t, double>>> getAllValues(
470+
const HdrHistogram& histo, HdrHistogram::Iterator& iter) {
471+
std::vector<std::optional<std::pair<uint64_t, double>>> values;
472+
while (auto pair = histo.getNextValueAndPercentile(iter)) {
473+
if (!pair.has_value())
474+
break;
475+
values.push_back(pair);
476+
}
477+
return values;
478+
}
479+
480+
void resetThread(HdrHistogram& histo, ThreadGate& tg) {
481+
EXPECT_EQ(histo.getValueCount(), 10);
482+
tg.threadUp();
483+
histo.reset();
484+
EXPECT_EQ(histo.getValueCount(), 0);
485+
}
486+
487+
/**
488+
* Test to check that if you create an iterator on a HdrHistogram object, then
489+
* call reset() on it in another thread and use the iterator that the iterator
490+
* doesn't end up in an infinite loop.
491+
*/
492+
TEST(HdrHistogramTest, ResetItoratorInfLoop) {
493+
Hdr2sfMicroSecHistogram histogram;
494+
for (int i = 0; i < 10; i++) {
495+
histogram.addValue(i);
496+
}
497+
{
498+
auto iter = histogram.getHistogramsIterator();
499+
auto values = getAllValues(histogram, iter);
500+
EXPECT_EQ(values.size(), 20);
501+
}
502+
503+
std::thread thread;
504+
{ // Scope that holds read lock for iterator
505+
auto iter2 = histogram.getHistogramsIterator();
506+
/**
507+
* Create thread this should start running resetThread() at some point
508+
* int time will be blocked at HdrHistogram::reset() till this scope is
509+
* exited and iter2 is destroyed (releasing the read lock).
510+
* We will also create a ThreadGat to ensure the reset thread is runing
511+
* and is about to try and get an exclusive lock before reading values
512+
* from the histogram.
513+
*/
514+
ThreadGate tg(2);
515+
thread = std::thread(resetThread, std::ref(histogram), std::ref(tg));
516+
tg.threadUp();
517+
auto values = getAllValues(histogram, iter2);
518+
EXPECT_EQ(values.size(), 20);
519+
} // iter2 read lock released
520+
521+
// wait for resetThread() to complete
522+
thread.join();
523+
EXPECT_EQ(histogram.getValueCount(), 0);
524+
}

utilities/hdrhistogram.cc

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
*/
1717

1818
#include "hdrhistogram.h"
19+
1920
#include <folly/lang/Assume.h>
20-
#include <hdr_histogram.h>
2121
#include <nlohmann/json.hpp>
2222
#include <platform/cb_malloc.h>
2323
#include <iostream>
@@ -51,17 +51,17 @@ HdrHistogram::HdrHistogram(uint64_t lowestTrackableValue,
5151
significantFigures,
5252
&hist, // Pointer to initialise
5353
cb_calloc);
54-
55-
histogram.reset(hist);
54+
histogram.wlock()->reset(hist);
5655
defaultIterationMode = iterMode;
5756
}
5857

5958
HdrHistogram& HdrHistogram::operator+=(const HdrHistogram& other) {
60-
if (other.histogram != nullptr && other.getValueCount() > 0) {
59+
if (other.histogram.rlock()->get() != nullptr &&
60+
other.getValueCount() > 0) {
6161
// work out if we need to resize the receiving histogram
6262
uint64_t newMin = this->getMinTrackableValue();
6363
uint64_t newMax = this->getMaxTrackableValue();
64-
int32_t newSigfig = this->histogram->significant_figures;
64+
int32_t newSigfig = this->getSigFigAccuracy();
6565
bool resize = false;
6666

6767
if (other.getMinTrackableValue() < this->getMinTrackableValue()) {
@@ -73,92 +73,93 @@ HdrHistogram& HdrHistogram::operator+=(const HdrHistogram& other) {
7373
resize = true;
7474
}
7575

76-
if (other.histogram->significant_figures >
77-
this->histogram->significant_figures) {
78-
newSigfig = other.histogram->significant_figures;
76+
if (other.getSigFigAccuracy() > this->getSigFigAccuracy()) {
77+
newSigfig = other.getSigFigAccuracy();
7978
resize = true;
8079
}
8180
if (resize) {
8281
this->resize(newMin, newMax, newSigfig);
8382
}
84-
hdr_add(this->histogram.get(), other.histogram.get());
83+
hdr_add(this->histogram.rlock()->get(), other.histogram.rlock()->get());
8584
}
8685
return *this;
8786
}
8887

8988
bool HdrHistogram::addValue(uint64_t v) {
9089
// A hdr_histogram cannot store 0, therefore we add a bias of +1.
9190
int64_t vBiased = v + 1;
92-
return hdr_record_value(histogram.get(), vBiased);
91+
return hdr_record_value(histogram.rlock()->get(), vBiased);
9392
}
9493

9594
bool HdrHistogram::addValueAndCount(uint64_t v, uint64_t count) {
9695
// A hdr_histogram cannot store 0, therefore we add a bias of +1.
9796
int64_t vBiased = v + 1;
98-
return hdr_record_values(histogram.get(), vBiased, count);
97+
return hdr_record_values(histogram.rlock()->get(), vBiased, count);
9998
}
10099

101100
uint64_t HdrHistogram::getValueCount() const {
102-
return static_cast<uint64_t>(histogram->total_count);
101+
return static_cast<uint64_t>(histogram.rlock()->get()->total_count);
103102
}
104103

105104
uint64_t HdrHistogram::getMinValue() const {
105+
// check there are values in the histogram otherwise hdr_min() will return
106+
// 0 which will cause use to return a UINT64_MAX.
106107
if (getValueCount()) {
107-
return static_cast<uint64_t>(hdr_min(histogram.get()) - 1);
108+
return static_cast<uint64_t>(hdr_min(histogram.rlock()->get()) - 1);
108109
}
109110
return 0;
110111
}
111112

112113
uint64_t HdrHistogram::getMaxValue() const {
114+
// check there are values in the histogram otherwise hdr_max() will return
115+
// 0 which will cause use to return a UINT64_MAX.
113116
if (getValueCount()) {
114-
return static_cast<uint64_t>(hdr_max(histogram.get()) - 1);
117+
return static_cast<uint64_t>(hdr_max(histogram.rlock()->get()) - 1);
115118
}
116119
return 0;
117120
}
118121

119122
void HdrHistogram::reset() {
120-
hdr_reset(histogram.get());
123+
hdr_reset(histogram.wlock()->get());
121124
}
122125

123126
uint64_t HdrHistogram::getValueAtPercentile(double percentage) const {
124127
// We added the bias of +1 to the input value (see
125128
// addValueToFreqHistogram). Therefore need to minus the bias
126129
// before returning the value from the histogram.
127130
// Note: If the histogram is empty we just want to return 0;
128-
uint64_t value = hdr_value_at_percentile(histogram.get(), percentage);
131+
uint64_t value =
132+
hdr_value_at_percentile(histogram.rlock()->get(), percentage);
129133
uint64_t result = getValueCount() > 0 ? (value - 1) : 0;
130134
return result;
131135
}
132136

133137
HdrHistogram::Iterator HdrHistogram::makeLinearIterator(
134138
int64_t valueUnitsPerBucket) const {
135-
HdrHistogram::Iterator iter{};
136-
iter.type = Iterator::IterMode::Linear;
137-
hdr_iter_linear_init(&iter, histogram.get(), valueUnitsPerBucket);
138-
return iter;
139+
HdrHistogram::Iterator itr(histogram, Iterator::IterMode::Linear);
140+
hdr_iter_linear_init(&itr, itr.histoRLockPtr->get(), valueUnitsPerBucket);
141+
return itr;
139142
}
140143

141144
HdrHistogram::Iterator HdrHistogram::makeLogIterator(int64_t firstBucketWidth,
142145
double log_base) const {
143-
HdrHistogram::Iterator iter{};
144-
iter.type = Iterator::IterMode::Log;
145-
hdr_iter_log_init(&iter, histogram.get(), firstBucketWidth, log_base);
146-
return iter;
146+
HdrHistogram::Iterator itr(histogram, Iterator::IterMode::Log);
147+
hdr_iter_log_init(
148+
&itr, itr.histoRLockPtr->get(), firstBucketWidth, log_base);
149+
return itr;
147150
}
148151

149152
HdrHistogram::Iterator HdrHistogram::makePercentileIterator(
150153
uint32_t ticksPerHalfDist) const {
151-
HdrHistogram::Iterator iter{};
152-
iter.type = Iterator::IterMode::Percentiles;
153-
hdr_iter_percentile_init(&iter, histogram.get(), ticksPerHalfDist);
154-
return iter;
154+
HdrHistogram::Iterator itr(histogram, Iterator::IterMode::Percentiles);
155+
hdr_iter_percentile_init(&itr, itr.histoRLockPtr->get(), ticksPerHalfDist);
156+
return itr;
155157
}
156158

157159
HdrHistogram::Iterator HdrHistogram::makeRecordedIterator() const {
158-
HdrHistogram::Iterator iter{};
159-
iter.type = Iterator::IterMode::Recorded;
160-
hdr_iter_recorded_init(&iter, histogram.get());
161-
return iter;
160+
HdrHistogram::Iterator itr(histogram, Iterator::IterMode::Recorded);
161+
hdr_iter_recorded_init(&itr, itr.histoRLockPtr->get());
162+
return itr;
162163
}
163164

164165
HdrHistogram::Iterator HdrHistogram::makeIterator(
@@ -246,16 +247,16 @@ HdrHistogram::getNextValueAndPercentile(Iterator& iter) const {
246247
}
247248

248249
void HdrHistogram::printPercentiles() const {
249-
hdr_percentiles_print(histogram.get(), stdout, 5, 1.0, CLASSIC);
250+
hdr_percentiles_print(histogram.rlock()->get(), stdout, 5, 1.0, CLASSIC);
250251
}
251252

252253
void HdrHistogram::dumpLinearValues(int64_t bucketWidth) {
253-
HdrHistogram::Iterator itr = makeLinearIterator(bucketWidth);
254+
auto itr = makeLinearIterator(bucketWidth);
254255
std::cout << dumpValues(itr).str();
255256
}
256257

257258
void HdrHistogram::dumpLogValues(int64_t firstBucketWidth, double log_base) {
258-
HdrHistogram::Iterator itr = makeLogIterator(firstBucketWidth, log_base);
259+
auto itr = makeLogIterator(firstBucketWidth, log_base);
259260
std::cout << dumpValues(itr).str();
260261
}
261262

@@ -275,7 +276,7 @@ nlohmann::json HdrHistogram::to_json(Iterator::IterMode itrType) {
275276
nlohmann::json rootObj;
276277

277278
// Five is the number of iteration steps per half-distance to 100%.
278-
Iterator itr = makePercentileIterator(5);
279+
auto itr = makePercentileIterator(5);
279280

280281
rootObj["total"] = itr.total_count;
281282
// bucketsLow represents the starting value of the first bucket
@@ -302,11 +303,11 @@ std::string HdrHistogram::to_string() {
302303
}
303304

304305
size_t HdrHistogram::getMemFootPrint() const {
305-
return hdr_get_memory_size(histogram.get()) + sizeof(HdrHistogram);
306+
return hdr_get_memory_size(histogram.rlock()->get()) + sizeof(HdrHistogram);
306307
}
307308

308309
double HdrHistogram::getMean() const {
309-
return hdr_mean(histogram.get()) - 1;
310+
return hdr_mean(histogram.rlock()->get()) - 1;
310311
}
311312

312313
void HdrHistogram::resize(uint64_t lowestTrackableValue,
@@ -331,6 +332,6 @@ void HdrHistogram::resize(uint64_t lowestTrackableValue,
331332
&hist, // Pointer to initialise
332333
cb_calloc);
333334

334-
hdr_add(hist, histogram.get());
335-
histogram.reset(hist);
335+
hdr_add(hist, histogram.rlock()->get());
336+
histogram.wlock()->reset(hist);
336337
}

utilities/hdrhistogram.h

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#pragma once
1919

20+
#include <folly/Synchronized.h>
2021
#include <nlohmann/json_fwd.hpp>
2122
#include <chrono>
2223
#include <memory>
@@ -49,6 +50,10 @@ class HdrHistogram {
4950
void operator()(struct hdr_histogram* val);
5051
};
5152

53+
using SyncHdrHistogramPtr = folly::Synchronized<
54+
std::unique_ptr<struct hdr_histogram, HdrDeleter>>;
55+
using HistoLockedPtr = SyncHdrHistogramPtr::ConstLockedPtr;
56+
5257
public:
5358
struct Iterator : public hdr_iter {
5459
/**
@@ -77,10 +82,28 @@ class HdrHistogram {
7782
*/
7883
Percentiles
7984
};
85+
86+
Iterator(const SyncHdrHistogramPtr& SyncHistoPtr,
87+
Iterator::IterMode mode)
88+
: hdr_iter(), type(mode), histoRLockPtr(SyncHistoPtr.rlock()) {
89+
}
90+
91+
Iterator(Iterator&& itr) = default;
92+
8093
IterMode type;
8194
uint64_t lastVal = 0;
8295
uint64_t lastCumulativeCount = 0;
8396
bool isFirst = true;
97+
98+
private:
99+
// allow HdrHistogram to access histoRLockPtr
100+
friend class HdrHistogram;
101+
/**
102+
* Read lock that's held for the life time of the iterator. To prevent
103+
* it being resized or reset while we're reading from the underlying
104+
* data structure.
105+
*/
106+
HistoLockedPtr histoRLockPtr;
84107
};
85108

86109
/**
@@ -172,7 +195,10 @@ class HdrHistogram {
172195
uint64_t getMaxValue() const;
173196

174197
/**
175-
* Clears the histogram.
198+
* Clears the histogram. Please not that this takes a write lock on the
199+
* underlying data structure and will wait till all read locks have been
200+
* released before completing and thus, could result in dead lock
201+
* situations.
176202
*/
177203
void reset();
178204

@@ -311,7 +337,9 @@ class HdrHistogram {
311337
// We subtract one from the lowest value as we have added a one offset
312338
// as the underlying hdr_histogram cannot store 0 and
313339
// therefore the value must be greater than or equal to 1.
314-
return static_cast<uint64_t>(histogram->lowest_trackable_value) - 1;
340+
return static_cast<uint64_t>(
341+
histogram.rlock()->get()->lowest_trackable_value) -
342+
1;
315343
}
316344

317345
/**
@@ -322,7 +350,9 @@ class HdrHistogram {
322350
// We subtract one from the lowest value as we have added a one offset
323351
// as the underlying hdr_histogram cannot store 0 and
324352
// therefore the value must be greater than or equal to 1.
325-
return static_cast<uint64_t>(histogram->highest_trackable_value) - 1;
353+
return static_cast<uint64_t>(
354+
histogram.rlock()->get()->highest_trackable_value) -
355+
1;
326356
}
327357

328358
/**
@@ -332,7 +362,7 @@ class HdrHistogram {
332362
* figures bing used
333363
*/
334364
int getSigFigAccuracy() const {
335-
return histogram->significant_figures;
365+
return histogram.rlock()->get()->significant_figures;
336366
}
337367

338368
/**
@@ -359,9 +389,9 @@ class HdrHistogram {
359389
Iterator::IterMode defaultIterationMode;
360390

361391
/**
362-
* unique_ptr to a hdr_histogram structure
392+
* Synchronized unique pointer to a struct hdr_histogram
363393
*/
364-
std::unique_ptr<struct hdr_histogram, HdrDeleter> histogram;
394+
SyncHdrHistogramPtr histogram;
365395
};
366396

367397
/** Histogram to store counts for microsecond intervals

0 commit comments

Comments
 (0)