Skip to content

Optimistically return metrics from the cache (if present) #418

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions core/include/prometheus/family.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ class PROMETHEUS_CPP_CORE_EXPORT Family : public Collectable {
/// labels already exists - the already existing dimensional data.
template <typename... Args>
T& Add(const std::map<std::string, std::string>& labels, Args&&... args) {
return Add(labels, detail::make_unique<T>(args...));
metrics_iterator iter = FindMetric(labels);
if (iter->second) return *(iter->second);
return Add(iter, detail::make_unique<T>(args...));
Comment on lines +112 to +114

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're acquiring the lock twice here. The helpers (e.g. FindMetric and Add overload) are private and should assume that the lock is held.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before: One double-locking is nothing in the long-run. However, grabbing the lock before computing the hash is always detrimental.
But that really opens the door to the fix I prefer: move the implementation of Add to the header file. Much simpler but I figure less acceptable.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think consolidating lock acquisition is incompatible with hashing outside. As long as the private methods remain in the code file the goal of preventing instantiation outside of Counter/Gauge/... should be achieved. So,

  T& Add(const std::map<std::string, std::string>& labels, Args&&... args) {
    auto hash = detail::hash_labels(labels);
    std::lock_guard<std::mutex> lock{mutex_};
    metrics_iterator iter = FindMetric(hash);
    if (iter->second) return *(iter->second);
    return Add(iter, detail::make_unique<T>(args...));

where both private methods (FindMetric and Add) use a pre-computed hash.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking the lock twice leads to a race condition where the unique ptr is null in between.

My goal when hiding the implementation was to avoid circular includes as well as limiting the exposed symbols to have at least a chance to provide patches while maintaining the SONAME.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking the lock twice leads to a race condition where the unique ptr is null in between.

If by "race condition" you mean "there is a possibility that 2 (or more) unique ptr's are created" that is correct.

My goal when hiding the implementation was to avoid circular includes as well as limiting the exposed symbols to have at least a chance to provide patches while maintaining the SONAME.

Right. This is why this patch is structured this way: to maintain the same isolation of the implementation. The runtime cost is what we've talked about here: double-hashing and double-locking until the cache is populated. After that, no more double-hashing or double-locking, and no more extraneous new/delete (the original intent of this patch.)
I think it's a worthy trade-off.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my forgeAI-nick/optimistic-family-add branch I moved the lock into to inlined template. That way we don't double lock.

}

/// \brief Remove the given dimensional data.
Expand Down Expand Up @@ -145,9 +147,11 @@ class PROMETHEUS_CPP_CORE_EXPORT Family : public Collectable {
const std::map<std::string, std::string> constant_labels_;
mutable std::mutex mutex_;

using metrics_iterator = typename std::unordered_map<std::size_t, std::unique_ptr<T>>::iterator;

ClientMetric CollectMetric(std::size_t hash, T* metric) const;
T& Add(const std::map<std::string, std::string>& labels,
std::unique_ptr<T> object);
T& Add(metrics_iterator hint, std::unique_ptr<T> object);
metrics_iterator FindMetric(const std::map<std::string, std::string>& labels);
};

} // namespace prometheus
23 changes: 17 additions & 6 deletions core/src/family.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Family<T>::Family(const std::string& name, const std::string& help,
}

template <typename T>
T& Family<T>::Add(const std::map<std::string, std::string>& labels,
std::unique_ptr<T> object) {
typename Family<T>::metrics_iterator
Family<T>::FindMetric(const std::map<std::string, std::string>& labels) {
auto hash = detail::hash_labels(labels);
std::lock_guard<std::mutex> lock{mutex_};
auto metrics_iter = metrics_.find(hash);
Expand All @@ -28,7 +28,7 @@ T& Family<T>::Add(const std::map<std::string, std::string>& labels,
const auto& old_labels = labels_iter->second;
assert(labels == old_labels);
#endif
return *metrics_iter->second;
return metrics_iter;
} else {
#ifndef NDEBUG
for (auto& label_pair : labels) {
Expand All @@ -37,14 +37,25 @@ T& Family<T>::Add(const std::map<std::string, std::string>& labels,
}
#endif

auto metric = metrics_.insert(std::make_pair(hash, std::move(object)));
auto metric = metrics_.insert(std::make_pair(hash, nullptr));
assert(metric.second);
labels_.insert({hash, labels});
labels_reverse_lookup_.insert({metric.first->second.get(), hash});
return *(metric.first->second);
return metric.first;
}
}

template <typename T>
T& Family<T>::Add(metrics_iterator hint, std::unique_ptr<T> object) {
std::lock_guard<std::mutex> lock{mutex_};
auto hash = hint->first;
assert(metrics_.find(hash) == hint);
if (! hint->second) {
labels_reverse_lookup_.insert({object.get(), hash});
hint->second.swap(object);
}
return *(hint->second);
}

template <typename T>
void Family<T>::Remove(T* metric) {
std::lock_guard<std::mutex> lock{mutex_};
Expand Down