-
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 18 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,9 +150,25 @@ 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); | ||
| absl::string_view key = name; | ||
|
|
||
| if (key.size() > Stats::RawStatData::maxNameLength()) { | ||
| key.remove_suffix(key.size() - Stats::RawStatData::maxNameLength()); | ||
| ENVOY_LOG_MISC( | ||
| warn, | ||
| "Statistic '{}' is too long with {} characters, it will be truncated to {} characters", key, | ||
| key.size(), Stats::RawStatData::maxNameLength()); | ||
| } | ||
|
|
||
| auto ret = stats_set_.insert(StringRawDataMap::value_type(std::string(key), nullptr)); | ||
|
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. I think stats_set_ needs a mutex. |
||
| RawStatData*& data = ret.first->second; | ||
| if (ret.second) { | ||
| data = static_cast<RawStatData*>(::calloc(RawStatData::size(), 1)); | ||
|
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. This looks correct, but it duplicates the required key storage in the map key. If you make this a set<RawStatData*, RawStatDataHash, RawStatDataCompare> rather than a map, then the hasher and comparator could reference the key stored in the RawStatData, and available as a string_view via RawStatData::key(). Before calling set insertion you'd have to prospectively calloc the ptr and initialize() it, and then free it if turned out to be a dup. That seems better than duplicating the storage. And you'd have to make the trivial functors for hashing and comparison. Then you could remove also the duplicated length check above, since it would be done in initialize().
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.
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 I +1'd too soon. It seems strange to alloc up an object just to be able to tell if the key exists, and then throwing it away if not. Duplicating the key or doing the truncation of the key before so we can check the set before allocating the object seems better IMO. It seems that now we've added a custom hash function, custom comparitor, and a somewhat complex set addition logic just to remove the duplicate storage of the key. Is there a particular reason that you think the way you suggested is more readable or performant?
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. RawStatData::initialize() literally just does a memcpy of the bytes, so it's basically the same cost as making the prospective copy of the string you need to do the map lookup. The syntax for making a custom hash/compare in STL is a little annoying, but I don't think it's that bad. I'm not following you about set-addition logic, I think it should be about equivalent, but it's (IMO) better to do the truncation in one place, and I can't judge exactly the cost of duplicating all stats at scale, but with this option it's zero :)
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. SGTM. Not a huge fan of the additional complexity around the additional allocation logic and stl munging, but your perf point is reasonable. I'm don't think getting rid of these additional allocations on successful lookups would be worth all of the wasted memory in the normal map case. And that seems to be the only choices here. Just a side note: we don't do the truncation in one place - we do it in two. |
||
| RELEASE_ASSERT(data); | ||
| data->initialize(key); | ||
| } else { | ||
| ++data->ref_count_; | ||
| } | ||
| return data; | ||
| } | ||
|
|
||
|
|
@@ -256,18 +272,23 @@ 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; | ||
| } | ||
|
|
||
| size_t key_removed = stats_set_.erase(std::string(data.key())); | ||
|
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. We expect all stats to call freed before the allocator object disappears, so I would suggest adding a destructor with an |
||
| ASSERT(key_removed >= 1); | ||
|
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. nit: Shouldn't this be == ?
Contributor
Author
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. Oops, good catch. Fixed in 87318c0. |
||
| ::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 |
|---|---|---|
|
|
@@ -412,8 +412,12 @@ class HistogramImpl : public Histogram, public MetricImpl { | |
| class HeapRawStatDataAllocator : public RawStatDataAllocator { | ||
| public: | ||
| // RawStatDataAllocator | ||
| typedef std::unordered_map<std::string, RawStatData*> StringRawDataMap; | ||
|
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. nit: since no other classes need to know about this typedef, do you think it makes sense to make it private? |
||
| RawStatData* alloc(const std::string& name) override; | ||
| void free(RawStatData& data) override; | ||
|
|
||
| private: | ||
| StringRawDataMap stats_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. nit: since |
||
| }; | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -478,5 +478,18 @@ TEST(RawStatDataTest, Truncate) { | |
| alloc.free(*stat); | ||
| } | ||
|
|
||
| TEST(RawStatDataTest, HeapAlloc) { | ||
| HeapRawStatDataAllocator alloc; | ||
| RawStatData* stat_1 = alloc.alloc("ref_name"); | ||
| RawStatData* stat_2 = alloc.alloc("ref_name"); | ||
| RawStatData* stat_3 = alloc.alloc("not_ref_name"); | ||
| 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.
nit: I don't think you need this line anymore since you can just pass name to initialize directly (string_view will implicitly be constructed from a string IIUC).