-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Fix issue with Envoy not reference counting across scopes under not-hot restart #3249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 29 commits
3d5849d
b6aa029
d615eef
cdd8418
101c7aa
282c56b
830c718
4de411a
11a8cdb
ecd7714
8c00102
ddcb0f9
7031caa
93eb3d7
b4b1b39
7e2b8d6
96582ad
bec4fac
ea27f46
a4eb61e
b388492
3b9d839
b10f663
759e6d9
8a8a39d
f0e19ae
76ebd08
87318c0
84913f9
5194ec2
ce99186
5429301
40ae83b
20526e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -150,10 +150,21 @@ bool TagExtractorImpl::extractTag(const std::string& stat_name, std::vector<Tag> | |||
| } | ||||
|
|
||||
| RawStatData* HeapRawStatDataAllocator::alloc(const std::string& name) { | ||||
| // This must be zero-initialized | ||||
| RawStatData* data = static_cast<RawStatData*>(::calloc(RawStatData::size(), 1)); | ||||
| data->initialize(name); | ||||
| return data; | ||||
|
|
||||
| std::unique_lock<std::mutex> lock(mutex_); | ||||
| auto ret = stats_.insert(data); | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can take the lock after the call to initialize(), to minimize the time spent holding the lock. Actually I think you can also let it go immediately after the call to insert as well as ref_count_ is atomic. |
||||
| RawStatData* existing_data = *ret.first; | ||||
| lock.unlock(); | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a subtle problem here. The iterator you were returned can be invalidated if another element is inserted into the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh very good point Matt. Just leave it locked till the end of the function then. I can't think of why the hash implementation would need to invalidate the iterator but if the standard doesn't say it is safe then there is no point risking it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the lock tightening is a good idea and will probably save us some cycles. We just need to extract the raw pointer from the iterator while locked and only use the pointer temporary after we unlock. As for how this interacts with a custom hash, my basic understanding is that as the set grows, it will at some point decide to rehash (using the same hash function) the entire set onto a larger set of buckets. This is the only process that causes iterators to be invalidated for |
||||
|
|
||||
| if (!ret.second) { | ||||
| ::free(data); | ||||
| ++existing_data->ref_count_; | ||||
| return existing_data; | ||||
| } else { | ||||
| return data; | ||||
| } | ||||
| } | ||||
|
|
||||
| TagProducerImpl::TagProducerImpl(const envoy::config::metrics::v2::StatsConfig& config) | ||||
|
|
@@ -256,18 +267,26 @@ TagProducerImpl::addDefaultExtractors(const envoy::config::metrics::v2::StatsCon | |||
| } | ||||
|
|
||||
| void HeapRawStatDataAllocator::free(RawStatData& data) { | ||||
| // This allocator does not ever have concurrent access to the raw data. | ||||
| ASSERT(data.ref_count_ == 1); | ||||
| ASSERT(data.ref_count_ > 0); | ||||
| if (--data.ref_count_ > 0) { | ||||
| return; | ||||
| } | ||||
|
|
||||
| std::unique_lock<std::mutex> lock(mutex_); | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need locking at all if the old comment about "This allocator does not ever have concurrent access to the raw data" hold true?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem isn't the stat, it's the set.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1. Also, side note: that comment is no longer valid since the same stat can be freed/allocated multiple times, meaning that there may be cases where the allocator is operating on the same raw stat from two different threads.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAICT we only ever do these allocations under existing locking, e.g.
I might be wrong in my assessment, please point out if not (and add a comment to the code!).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, good point, there probably need to be comments around this. The |
||||
| size_t key_removed = stats_.erase(&data); | ||||
| lock.unlock(); | ||||
|
|
||||
| ASSERT(key_removed == 1); | ||||
| ::free(&data); | ||||
| } | ||||
|
|
||||
| void RawStatData::initialize(absl::string_view key) { | ||||
| ASSERT(!initialized()); | ||||
| if (key.size() > maxNameLength()) { | ||||
| if (key.size() > Stats::RawStatData::maxNameLength()) { | ||||
| ENVOY_LOG_MISC( | ||||
| warn, | ||||
| "Statistic '{}' is too long with {} characters, it will be truncated to {} characters", key, | ||||
| key.size(), maxNameLength()); | ||||
| key.size(), Stats::RawStatData::maxNameLength()); | ||||
| } | ||||
| ref_count_ = 1; | ||||
|
|
||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -376,8 +376,22 @@ class HistogramImpl : public Histogram, public MetricImpl { | |
| class HeapRawStatDataAllocator : public RawStatDataAllocator { | ||
| public: | ||
| // RawStatDataAllocator | ||
| ~HeapRawStatDataAllocator() { ASSERT(stats_.empty()); } | ||
| RawStatData* alloc(const std::string& name) override; | ||
| void free(RawStatData& data) override; | ||
|
|
||
| private: | ||
| struct RawStatDataHash_ { | ||
| size_t operator()(const RawStatData* a) const { return HashUtil::xxHash64(a->key()); } | ||
| }; | ||
| struct RawStatDataCompare_ { | ||
| bool operator()(const RawStatData* a, const RawStatData* b) const { | ||
| return (a->key() == b->key()); | ||
| } | ||
| }; | ||
| typedef std::unordered_set<RawStatData*, RawStatDataHash_, RawStatDataCompare_> StringRawDataSet; | ||
| StringRawDataSet stats_; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add comments explaining what this is/does. |
||
| std::mutex mutex_; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add comments explaining what htis protects. Ideally we use |
||
| }; | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -478,5 +478,21 @@ TEST(RawStatDataTest, Truncate) { | |
| alloc.free(*stat); | ||
| } | ||
|
|
||
| TEST(RawStatDataTest, HeapAlloc) { | ||
| HeapRawStatDataAllocator alloc; | ||
| RawStatData* stat_1 = alloc.alloc("ref_name"); | ||
| ASSERT_NE(stat_1, nullptr); | ||
| RawStatData* stat_2 = alloc.alloc("ref_name"); | ||
| ASSERT_NE(stat_2, nullptr); | ||
| RawStatData* stat_3 = alloc.alloc("not_ref_name"); | ||
| ASSERT_NE(stat_3, nullptr); | ||
| EXPECT_EQ(stat_1, stat_2); | ||
| EXPECT_NE(stat_1, stat_3); | ||
| EXPECT_NE(stat_2, stat_3); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's just expect stat_3 is not nullptr too, though it looks like that would segv below anyway. |
||
| alloc.free(*stat_1); | ||
| alloc.free(*stat_2); | ||
| alloc.free(*stat_3); | ||
| } | ||
|
|
||
| } // namespace Stats | ||
| } // namespace Envoy | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we allocating and then freeing on the case where we have an existing stat?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. Fundamentally it doesn't have to be this way, but this is an artifact of the way STL sets & maps work. STL set lookups require construction of an object. If you make this an
map<string, RawStatData*>you have just pushed the problem around a little, as you'd need to copy the string to potentially truncate it, which is the same work, basically, as is being done here, and then you have to duplicate the truncation logic instead of just having it in RawStatData::initialize. Worse, you'd wind up permanently duplicating all the name storage. I argued that I don't know really how impactful that would be across different ways you might scale the system, but the current solution has zero overhead from duplication and is really no more complex from a programming perspective.One question to ask is whether RawStatData::initialize is doing anything extra that's not required for the set lookup. It is, but it's pretty minimal and IMO not worth optimizing around.
An ideal solution would allow the set lookup against a string_view, without actually constructing the templated type. BlockMemoryHashSet::insert has that signature, so in the hot-restart case you don't need to do the prospective allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, fair enough. Maybe add a comment to the code capturing this design history. Thanks!