Skip to content

further threadsafety hardening #55

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 16 commits into
base: master
Choose a base branch
from
Open

further threadsafety hardening #55

wants to merge 16 commits into from

Conversation

kevinkreiser
Copy link
Collaborator

@kevinkreiser kevinkreiser commented Apr 21, 2025

over in #12 we've clarified a few things to make the library threadsafe. there are a couple more little things to finish the job though largely the library is already threadsafe. this pr makes the use of the rng threadsafe by using thread local storage. additionally we remove the ability to change the internal state of the client via setConfig and seed. you're only option for configuration is up front or to make a new client which is certainly a clearer contract

the only thing left to do is fix the tests since the api has changed some of the tests are hard to keep in their current form

fixes #12

Comment on lines +18 to +38
namespace detail {
inline std::string sanitizePrefix(std::string prefix) {
// For convenience we provide the dot when generating the stat message
if (!prefix.empty() && prefix.back() == '.') {
prefix.pop_back();
}
return prefix;
}

inline float rand(unsigned int seed) {
static thread_local std::mt19937 twister(seed);
static thread_local std::uniform_real_distribution<float> dist(0.f, 1.f);
return dist(twister);
}

// All supported metric types
constexpr char METRIC_TYPE_COUNT[] = "c";
constexpr char METRIC_TYPE_GAUGE[] = "g";
constexpr char METRIC_TYPE_TIMING[] = "ms";
constexpr char METRIC_TYPE_SET[] = "s";
} // namespace detail
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved this up above the constructor for the class so i could make use of the random function as a default argument

Comment on lines 99 to 100
FrequencyFunc frequencyFunc =
std::bind(detail::rand, std::random_device()())) noexcept;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we no longer have a seed method on the class but rather you pass in a sampling function. i personally like the term sample better here but because it has interplay with the "frequency" (nomenclature from statsd proper) i chose to make them match

Comment on lines -79 to -86
//! Sets a configuration
void setConfig(const std::string& host,
const uint16_t port,
const std::string& prefix,
const uint64_t batchsize = 0,
const uint64_t sendInterval = 1000,
const int gaugePrecision = 4) noexcept;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

allowing this makes it possible to break threadsafety gaurantees so its better to just remove it. you can simply construct a new class if you want to configure it differently (should be a rare thing anyway)

//! Flush any queued stats to the daemon
void flush() noexcept;
void flush() const noexcept;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just cleanup

Copy link
Owner

Choose a reason for hiding this comment

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

Can it be made const given that UDPSender::flush() is not const?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

its kind of a dirty trick in this case but because the udpsender is a unique_ptr and its -> dereference operator is const (since it doesnt modify the smart pointer itself) we can then freely call the non const member on the dereferenced instance of udpsender that the smart ptr returned us

@@ -152,58 +172,29 @@ class StatsdClient {

//!@}

private:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

more cleanup

//! The prefix to be used for metrics
std::string m_prefix;

//! The UDP sender to be used for actual sending
std::unique_ptr<UDPSender> m_sender;

//! The random number generator for handling sampling
mutable std::mt19937 m_randomEngine;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed as its now encapsulated inside of the random functor below

//! Fixed floating point precision of gauges
int m_gaugePrecision;
};

namespace detail {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved up as previously state

: m_prefix(detail::sanitizePrefix(prefix)),
m_sender(new UDPSender{host, port, batchsize, sendInterval}),
m_gaugePrecision(gaugePrecision) {
// Initialize the random generator to be used for sampling
seed();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

happens in the declaration as part of the default param

@@ -53,43 +53,24 @@ void testErrorConditions() {
throwOnError(client, false, "Should not be able to resolve a ridiculous ip");
}

void testReconfigure() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

functionality is removed so test is removed

Comment on lines 61 to 63
std::mt19937 twister(std::random_device{}());
std::uniform_real_distribution<float> dist(0.f, 1.f);
auto rand = [&]()->float{return dist(twister);};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we pass our own custom sampling function so we can fiddle with it to get it to give the results we want

std::cerr << "Unexpected stats received by server, got:" << std::endl;
for (const auto& message : messages) {
std::cerr << message << std::endl;
for (size_t i = 0; i < expected.size(); ++i) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

while i was debugging this trying to get a magical seed that would work for all examples i found changing the formatting of this output makes it easier to debug what is going on

@kevinkreiser kevinkreiser marked this pull request as ready for review May 15, 2025 13:46
@kevinkreiser
Copy link
Collaborator Author

@vthiery i dont know if you have any time to look at this but basically in recent times people are asking about threadsafety again and so this is the last little cleanup needed to make the library fully threadsafe again. more info in #12 and also #53

@vthiery
Copy link
Owner

vthiery commented May 15, 2025

Thank you for the work @kevinkreiser ! I have to admin that I've only been following this distantly, but I'll review asap.

docs updates
@kevinkreiser
Copy link
Collaborator Author

kevinkreiser commented May 15, 2025

well i saw your ship and so i went to edit the docs to say that the library is thread-safe but upon review i think there is still one thing to do which is to make the sender thread-safe even when the sending interval is 0.

see in most use-cases you would make use of the sending interval to spawn the background sending thread and the library is then thread-safe HOWEVER when the sending interval is 0 we expect you to call flush manually to send your messages over the socket BUT in this case we acquire a dummy lock (no thread-safety).

therefore if you configure your client with send interval 0 but you share the client in multiple threads who all race to call flush you will have thread-safety issues. i'll review the udpsender header further to make sure we remove the rest of the thread-safety pitfalls before asking for another review! apologies!

@vthiery vthiery self-requested a review May 16, 2025 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

If the client is thread safe?
2 participants