diff --git a/README.md b/README.md index 34a8228..1fc2f14 100644 --- a/README.md +++ b/README.md @@ -125,15 +125,12 @@ The batchsize is the only parameter that cannot be changed for the time being. ### Batching -The client supports batching of the metrics: +The client supports batching of the metrics. The batch size parameter is the number of bytes to allow in each batch (UDP datagram payload) to be sent to the statsd process. This number is not a hard limit. If appending the current stat to the current batch (separated by the `'\n'` character) pushes the current batch over the batch size, that batch is enqueued (not sent) and a new batch is started. If batch size is 0, the default, then each stat is sent individually to the statsd process and no batches are enqueued. -- the unit of the batching parameter is bytes, -- it is optional and passing a 0 will result in an immediate send of any metrics, +### Sending -If set, the UDP sender will spawn a thread sending the metrics to the server every 1 second by aggregates. The aggregation logic is as follows: +As previously mentioned, if batching is disabled (by setting the batch size to 0) then every stat is sent immediately in a blocking fashion. If batching is enabled (ie non-zero) then you may also set the send interval. The send interval controls the time, in milliseconds, to wait before flushing/sending the queued stats batches to the statsd process. When the send interval is non-zero a background thread is spawned which will do the flushing/sending at the configured send interval, in other words asynchronously. The queuing mechanism in this case is *not* lock-free. If batching is enabled but the send interval is set to zero then the queued batchs of stats will not be sent automatically by a background thread but must be sent manually via the `flush` method. The `flush` method is a blocking call. -- if the last message has already reached the batching limit, add a new element in a message queue; -- otherwise, append the message to the last one, separated by a `\n`. ### Frequency rate diff --git a/include/cpp-statsd-client/StatsdClient.hpp b/include/cpp-statsd-client/StatsdClient.hpp index 2eec825..1f5a978 100644 --- a/include/cpp-statsd-client/StatsdClient.hpp +++ b/include/cpp-statsd-client/StatsdClient.hpp @@ -24,11 +24,33 @@ namespace Statsd { * key, therefore you should neither append one to the prefix * nor prepend one to the key * - * The sampling frequency can be input, as well as the - * batching size. The latter is the optional limit in - * number of bytes a batch of stats can be before it is - * new states are added to a fresh batch. A value of 0 - * means batching is disabled. + * The sampling frequency is specified per call and uses a + * random number generator to determine whether or not the stat + * will be recorded this time or not. + * + * The top level configuration includes 2 optional parameters + * that determine how the stats are delivered to statsd. These + * parameters are the batching size and the send interval. + * + * The batching size controls the number of bytes to send + * in each UDP datagram to statsd. This is not a hard limit as + * we continue appending to a batch of stats until the limit + * has been reached or surpassed. When this occurs we add the + * batch to a queue and create a new batch to appended to. A + * value of 0 for the batching size will disable batching such + * that each stat will be sent to the daemon individually. + * + * The send interval controls the rate at which queued batches + * of stats will be sent to statsd. If batching is disabled, + * this value is ignored and each individual stat is sent to + * statsd immediately in a blocking fashion. If batching is + * enabled (ie. non-zero) then the send interval is the number + * of milliseconds to wait before flushing the queue of + * batched stats messages to the daemon. This is done in a non- + * blocking fashion via a background thread. If the send + * interval is 0 then the stats messages are appended to a + * queue until the caller manually flushes the queue via the + * flush method. * */ class StatsdClient { @@ -40,7 +62,8 @@ class StatsdClient { StatsdClient(const std::string& host, const uint16_t port, const std::string& prefix, - const uint64_t batchsize = 0) noexcept; + const uint64_t batchsize = 0, + const uint64_t sendInterval = 1000) noexcept; StatsdClient(const StatsdClient&) = delete; StatsdClient& operator=(const StatsdClient&) = delete; @@ -54,7 +77,8 @@ class StatsdClient { void setConfig(const std::string& host, const uint16_t port, const std::string& prefix, - const uint64_t batchsize = 0) noexcept; + const uint64_t batchsize = 0, + const uint64_t sendInterval = 1000) noexcept; //! Returns the error message as an std::string const std::string& errorMessage() const noexcept; @@ -96,6 +120,9 @@ class StatsdClient { //! Seed the RNG that controls sampling void seed(unsigned int seed = std::random_device()()) noexcept; + //! Flush any queued stats to the daemon + void flush() noexcept; + //!@} private: @@ -144,8 +171,9 @@ constexpr char METRIC_TYPE_SET[] = "s"; inline StatsdClient::StatsdClient(const std::string& host, const uint16_t port, const std::string& prefix, - const uint64_t batchsize) noexcept - : m_prefix(detail::sanitizePrefix(prefix)), m_sender(new UDPSender{host, port, batchsize}) { + const uint64_t batchsize, + const uint64_t sendInterval) noexcept + : m_prefix(detail::sanitizePrefix(prefix)), m_sender(new UDPSender{host, port, batchsize, sendInterval}) { // Initialize the random generator to be used for sampling seed(); // Avoid re-allocations by reserving a generous buffer @@ -155,9 +183,10 @@ inline StatsdClient::StatsdClient(const std::string& host, inline void StatsdClient::setConfig(const std::string& host, const uint16_t port, const std::string& prefix, - const uint64_t batchsize) noexcept { + const uint64_t batchsize, + const uint64_t sendInterval) noexcept { m_prefix = detail::sanitizePrefix(prefix); - m_sender.reset(new UDPSender(host, port, batchsize)); + m_sender.reset(new UDPSender(host, port, batchsize, sendInterval)); } inline const std::string& StatsdClient::errorMessage() const noexcept { @@ -261,6 +290,10 @@ inline void StatsdClient::seed(unsigned int seed) noexcept { m_randomEngine.seed(seed); } +inline void StatsdClient::flush() noexcept { + m_sender->flush(); +} + } // namespace Statsd #endif diff --git a/include/cpp-statsd-client/UDPSender.hpp b/include/cpp-statsd-client/UDPSender.hpp index d511960..ed46d00 100644 --- a/include/cpp-statsd-client/UDPSender.hpp +++ b/include/cpp-statsd-client/UDPSender.hpp @@ -28,7 +28,10 @@ class UDPSender final { //!@{ //! Constructor - UDPSender(const std::string& host, const uint16_t port, const uint64_t batchsize = 0) noexcept; + UDPSender(const std::string& host, + const uint16_t port, + const uint64_t batchsize, + const uint64_t sendInterval) noexcept; //! Destructor ~UDPSender(); @@ -38,7 +41,7 @@ class UDPSender final { //!@name Methods //!@{ - //! Send a message + //! Send or enqueue a message void send(const std::string& message) noexcept; //! Returns the error message as a string @@ -47,6 +50,9 @@ class UDPSender final { //! Returns true if the sender is initialized bool initialized() const noexcept; + //! Flushes any queued messages + void flush() noexcept; + //!@} private: @@ -93,14 +99,14 @@ class UDPSender final { // @name Batching info // @{ - //! Shall the sender use batching? - bool m_batching{false}; - //! The batching size uint64_t m_batchsize; + //! The sending frequency in milliseconds + uint64_t m_sendInterval; + //! The queue batching the messages - mutable std::deque m_batchingMessageQueue; + std::deque m_batchingMessageQueue; //! The mutex used for batching std::mutex m_batchingMutex; @@ -117,24 +123,20 @@ class UDPSender final { static constexpr int k_invalidFd{-1}; }; -inline UDPSender::UDPSender(const std::string& host, const uint16_t port, const uint64_t batchsize) noexcept - : m_host(host), m_port(port) { +inline UDPSender::UDPSender(const std::string& host, + const uint16_t port, + const uint64_t batchsize, + const uint64_t sendInterval) noexcept + : m_host(host), m_port(port), m_batchsize(batchsize), m_sendInterval(sendInterval) { // Initialize the socket if (!initialize()) { return; } // If batching is on, use a dedicated thread to send after the wait time is reached - if (batchsize != 0) { - // Thread' sleep duration between batches - // TODO: allow to input this - constexpr unsigned int batchingWait{1000U}; - - m_batching = true; - m_batchsize = batchsize; - + if (m_batchsize != 0 && m_sendInterval > 0) { // Define the batching thread - m_batchingThread = std::thread([this, batchingWait] { + m_batchingThread = std::thread([this] { // TODO: this will drop unsent stats, should we send all the unsent stats before we exit? while (!m_mustExit.load(std::memory_order_acq_rel)) { std::deque stagedMessageQueue; @@ -150,7 +152,7 @@ inline UDPSender::UDPSender(const std::string& host, const uint16_t port, const } // Wait before sending the next batch - std::this_thread::sleep_for(std::chrono::milliseconds(batchingWait)); + std::this_thread::sleep_for(std::chrono::milliseconds(m_sendInterval)); } }); } @@ -161,27 +163,33 @@ inline UDPSender::~UDPSender() { return; } - if (m_batching) { + // If we're running a background thread tell it to stop + if (m_batchingThread.joinable()) { m_mustExit.store(true, std::memory_order_acq_rel); m_batchingThread.join(); } + // Cleanup the socket close(m_socket); } inline void UDPSender::send(const std::string& message) noexcept { m_errorMessage.clear(); + // If batching is on, accumulate messages in the queue - if (m_batching) { + if (m_batchsize > 0) { queueMessage(message); return; } + // Or send it right now sendToDaemon(message); } inline void UDPSender::queueMessage(const std::string& message) noexcept { - std::unique_lock batchingLock(m_batchingMutex); + // We aquire a lock but only if we actually need to (i.e. there is a thread also accessing the queue) + auto batchingLock = + m_batchingThread.joinable() ? std::unique_lock(m_batchingMutex) : std::unique_lock(); // Either we don't have a place to batch our message or we exceeded the batch size, so make a new batch if (m_batchingMessageQueue.empty() || m_batchingMessageQueue.back().length() > m_batchsize) { m_batchingMessageQueue.emplace_back(); @@ -255,6 +263,17 @@ inline bool UDPSender::initialized() const noexcept { return m_socket != k_invalidFd; } +inline void UDPSender::flush() noexcept { + // We aquire a lock but only if we actually need to (ie there is a thread also accessing the queue) + auto batchingLock = + m_batchingThread.joinable() ? std::unique_lock(m_batchingMutex) : std::unique_lock(); + // Flush the queue + while (!m_batchingMessageQueue.empty()) { + sendToDaemon(m_batchingMessageQueue.front()); + m_batchingMessageQueue.pop_front(); + } +} + } // namespace Statsd #endif diff --git a/tests/testStatsdClient.cpp b/tests/testStatsdClient.cpp index 5842e5f..c58107d 100644 --- a/tests/testStatsdClient.cpp +++ b/tests/testStatsdClient.cpp @@ -74,13 +74,13 @@ void testReconfigure() { // batches being dropped vs sent on reconfiguring } -void testSendRecv(uint64_t batchSize) { +void testSendRecv(uint64_t batchSize, uint64_t sendInterval) { StatsdServer mock_server; std::vector messages, expected; std::thread server(mock, std::ref(mock_server), std::ref(messages)); // Set a new config that has the client send messages to a proper address that can be resolved - StatsdClient client("localhost", 8125, "sendRecv.", batchSize); + StatsdClient client("localhost", 8125, "sendRecv.", batchSize, sendInterval); throwOnError(client); // TODO: I forget if we need to wait for the server to be ready here before sending the first stats @@ -124,9 +124,9 @@ void testSendRecv(uint64_t batchSize) { expected.emplace_back("sendRecv.tutu:1227|s"); // Gauge but with tags - client.gauge("dr.rösti.grabe", 333, 1.f, {"liegt", "im", "weste"}); + client.gauge("dr.röstigrabe", 333, 1.f, {"liegt", "im", "weste"}); throwOnError(client); - expected.emplace_back("sendRecv.dr.rösti.grabe:333|g|#liegt,im,weste"); + expected.emplace_back("sendRecv.dr.röstigrabe:333|g|#liegt,im,weste"); // All the things client.count("foo", -42, .9f, {"bar", "baz"}); @@ -137,6 +137,11 @@ void testSendRecv(uint64_t batchSize) { // Signal the mock server we are done client.timing("DONE", 0); + // If manual flushing do it now + if (sendInterval == 0) { + client.flush(); + } + // Wait for the server to stop server.join(); @@ -162,9 +167,11 @@ int main() { // reconfiguring how you are sending testReconfigure(); // no batching - testSendRecv(0); + testSendRecv(0, 0); // background batching - testSendRecv(4); + testSendRecv(32, 1000); + // manual flushing of batches + testSendRecv(16, 0); return EXIT_SUCCESS; }