Skip to content

Commit 575e1ba

Browse files
committed
Interval flushing to ensure timely metric emission
Adds a flushIntervalMs parameter to WriterConfig (and Config) that starts a background timer thread which drains all thread-local buffers on a fixed period. This guarantees metrics are emitted within a bounded timeframe even when write volume is low and the capacity-based flush never triggers.
1 parent 254ba61 commit 575e1ba

8 files changed

Lines changed: 174 additions & 18 deletions

File tree

libs/config/config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Config
2323
const std::string& GetWriterLocation() const noexcept { return m_writerConfig.GetLocation(); }
2424
const WriterType& GetWriterType() const noexcept { return m_writerConfig.GetType(); }
2525
const unsigned int GetWriterBufferSize() const noexcept { return m_writerConfig.GetBufferSize(); }
26+
const unsigned int GetFlushIntervalMs() const noexcept { return m_writerConfig.GetFlushIntervalMs(); }
2627

2728

2829
private:

libs/writer/writer_config/writer_config.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,11 @@ WriterConfig::WriterConfig(const std::string& type, const unsigned int bufferSiz
5656
Logger::info("WriterConfig buffering enabled with size: {}", m_bufferSize);
5757
}
5858

59+
WriterConfig::WriterConfig(const std::string& type, const unsigned int bufferSize, const unsigned int flushIntervalMs)
60+
: WriterConfig(type, bufferSize) // Constructor delegation
61+
{
62+
m_flushIntervalMs = flushIntervalMs;
63+
Logger::info("WriterConfig flush interval enabled with interval: {}ms", m_flushIntervalMs);
64+
}
65+
5966
} // namespace spectator

libs/writer/writer_config/writer_config.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ class WriterConfig
1212
public:
1313
explicit WriterConfig(const std::string& type);
1414
WriterConfig(const std::string& type, unsigned int bufferSize);
15+
WriterConfig(const std::string& type, unsigned int bufferSize, unsigned int flushIntervalMs);
1516

1617
[[nodiscard]] const WriterType& GetType() const noexcept { return m_type; }
1718
[[nodiscard]] unsigned int GetBufferSize() const noexcept { return m_bufferSize; }
1819
[[nodiscard]] const std::string& GetLocation() const noexcept { return m_location; }
20+
[[nodiscard]] unsigned int GetFlushIntervalMs() const noexcept { return m_flushIntervalMs; }
1921

2022
private:
2123
WriterType m_type;
2224
std::string m_location;
2325
unsigned int m_bufferSize = 0;
26+
unsigned int m_flushIntervalMs = 0;
2427
};
2528

2629
} // namespace spectator

libs/writer/writer_wrapper/test_writer.cpp

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,56 @@ TEST_F(WriterWrapperUDSWriterTest, MultithreadedWrite)
127127
EXPECT_EQ(actualIncrements, expectedIncrements);
128128
}
129129

130+
// Verify that metrics written infrequently are still flushed via the interval mechanism
131+
// even when the buffer is not full.
132+
TEST_F(WriterWrapperUDSWriterTest, IntervalFlushWithIdleThread)
133+
{
134+
Logger::info("Starting interval flush test...");
135+
136+
// Use a large buffer so capacity-based flush never triggers,
137+
// but set a short flush interval so metrics get sent in time.
138+
const std::string unixUrl = "/tmp/test_uds_socket";
139+
constexpr unsigned int largeBuffer = 4096;
140+
constexpr unsigned int flushIntervalMs = 200;
141+
WriterTestHelper::InitializeWriter(WriterType::Unix, unixUrl, 0, largeBuffer, flushIntervalMs);
142+
143+
MeterId meterId("interval.test.counter");
144+
Counter counter(meterId);
145+
counter.Increment();
146+
147+
// Wait for at least two flush intervals to pass
148+
std::this_thread::sleep_for(std::chrono::milliseconds(flushIntervalMs * 3));
149+
150+
auto msgs = get_uds_messages();
151+
EXPECT_FALSE(msgs.empty()) << "Expected metrics to be flushed by interval timer";
152+
153+
std::regex counter_regex(R"(c:interval\.test\.counter:1.000000)");
154+
bool found = false;
155+
for (const auto& msg : msgs)
156+
{
157+
std::stringstream ss(msg);
158+
std::string line;
159+
while (std::getline(ss, line))
160+
{
161+
if (std::regex_match(line, counter_regex))
162+
{
163+
found = true;
164+
}
165+
}
166+
}
167+
EXPECT_TRUE(found) << "Expected interval.test.counter metric to be received";
168+
}
169+
130170
// Verify that multiple worker threads do not block each other: each thread uses its own
131-
// local buffer, flushing by capacity. Remaining data is drained by ThreadLocalBuffer's
132-
// destructor when each thread exits.
171+
// local buffer, flushing by capacity. The interval timer catches any tail data that
172+
// does not fill the buffer before the threads exit.
133173
TEST_F(WriterWrapperUDSWriterTest, ThreadLocalBufferNoMutexContention)
134174
{
135175
Logger::info("Starting thread-local buffer contention test...");
136176

137177
const std::string unixUrl = "/tmp/test_uds_socket";
138-
// Small buffer so capacity-based flush fires frequently
139-
WriterTestHelper::InitializeWriter(WriterType::Unix, unixUrl, 0, 64);
178+
// Small buffer so capacity-based flush fires frequently; interval timer catches the tail
179+
WriterTestHelper::InitializeWriter(WriterType::Unix, unixUrl, 0, 64, 100);
140180

141181
constexpr auto numThreads = 8;
142182
constexpr auto incrementsPerThread = 20;
@@ -162,7 +202,7 @@ TEST_F(WriterWrapperUDSWriterTest, ThreadLocalBufferNoMutexContention)
162202
t.join();
163203
}
164204

165-
// Allow time for thread destructor flushes to reach the UDS server
205+
// Allow the interval timer to flush any remaining buffered data
166206
std::this_thread::sleep_for(std::chrono::milliseconds(300));
167207

168208
auto msgs = get_uds_messages();

libs/writer/writer_wrapper/writer.cpp

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,24 @@ Writer::ThreadLocalBuffer::~ThreadLocalBuffer()
3636
Writer::~Writer()
3737
{
3838
auto& instance = GetInstance();
39-
instance.shutdown.store(true);
39+
40+
// Signal all background threads to stop
41+
{
42+
std::lock_guard<std::mutex> lock(instance.shutdownMutex);
43+
instance.shutdown.store(true);
44+
}
45+
instance.cv_shutdown.notify_all();
46+
47+
if (instance.flushTimerThread.joinable())
48+
{
49+
instance.flushTimerThread.join();
50+
}
51+
4052
this->Close();
4153
}
4254

4355
void Writer::Initialize(WriterType type, const std::string& param, int port,
44-
unsigned int bufferSize)
56+
unsigned int bufferSize, unsigned int flushIntervalMs)
4557
{
4658
auto& instance = GetInstance();
4759

@@ -66,20 +78,37 @@ void Writer::Initialize(WriterType type, const std::string& param, int port,
6678
}
6779

6880
instance.m_currentType = type;
69-
instance.shutdown.store(false);
70-
instance.bufferingEnabled = false;
7181

72-
// Clear thread-local buffer registry from any previous init
82+
// Stop any pre-existing timer thread before re-initialising
83+
{
84+
std::lock_guard<std::mutex> lock(instance.shutdownMutex);
85+
instance.shutdown.store(true);
86+
}
87+
instance.cv_shutdown.notify_all();
88+
if (instance.flushTimerThread.joinable())
89+
{
90+
instance.flushTimerThread.join();
91+
}
92+
// Clear the thread-local buffer registry from any previous init
7393
{
7494
std::lock_guard<std::mutex> regLock(instance.registryMutex);
7595
instance.threadBufferRegistry.clear();
7696
}
97+
instance.shutdown.store(false);
98+
instance.bufferingEnabled = false;
7799

78-
if (bufferSize > 0)
100+
if (bufferSize > 0 || flushIntervalMs > 0)
79101
{
80102
instance.bufferingEnabled = true;
81103
instance.bufferSize = bufferSize;
82104
instance.writeImpl = &Writer::ThreadLocalBufferedWrite;
105+
106+
if (flushIntervalMs > 0)
107+
{
108+
instance.flushInterval = std::chrono::milliseconds(flushIntervalMs);
109+
instance.flushTimerThread = std::thread(&Writer::FlushTimerThread, &instance);
110+
Logger::info("Writer interval flush enabled: {}ms", flushIntervalMs);
111+
}
83112
}
84113
else
85114
{
@@ -99,6 +128,63 @@ void Writer::TryToSend(const std::string& message)
99128
instance.m_impl->Write(message);
100129
}
101130

131+
void Writer::FlushTimerThread()
132+
{
133+
auto& instance = GetInstance();
134+
135+
while (true)
136+
{
137+
// Sleep for the flush interval, or wake early on shutdown
138+
{
139+
std::unique_lock<std::mutex> lock(instance.shutdownMutex);
140+
instance.cv_shutdown.wait_for(lock, instance.flushInterval,
141+
[&instance] { return instance.shutdown.load(); });
142+
}
143+
144+
if (instance.shutdown.load())
145+
{
146+
break;
147+
}
148+
149+
// Collect live buffers (clean up expired weak_ptrs in the same pass)
150+
std::vector<std::shared_ptr<ThreadLocalBuffer>> buffersToFlush;
151+
{
152+
std::lock_guard<std::mutex> regLock(instance.registryMutex);
153+
for (auto it = instance.threadBufferRegistry.begin();
154+
it != instance.threadBufferRegistry.end();)
155+
{
156+
if (auto buf = it->lock())
157+
{
158+
buffersToFlush.push_back(std::move(buf));
159+
++it;
160+
}
161+
else
162+
{
163+
it = instance.threadBufferRegistry.erase(it);
164+
}
165+
}
166+
}
167+
168+
// Drain each thread-local buffer
169+
for (auto& buf : buffersToFlush)
170+
{
171+
std::string toSend;
172+
{
173+
std::lock_guard<std::mutex> lock(buf->mutex);
174+
if (!buf->data.empty())
175+
{
176+
toSend = std::move(buf->data);
177+
buf->data.clear();
178+
}
179+
}
180+
if (!toSend.empty())
181+
{
182+
instance.TryToSend(toSend);
183+
}
184+
}
185+
}
186+
}
187+
102188
void Writer::ThreadLocalBufferedWrite(const std::string& message)
103189
{
104190
auto& instance = GetInstance();
@@ -118,7 +204,7 @@ void Writer::ThreadLocalBufferedWrite(const std::string& message)
118204
tl_buffer->data.push_back(NEW_LINE);
119205

120206
// Capacity-based flush: drain when the thread-local buffer is full
121-
if (tl_buffer->data.size() >= instance.bufferSize)
207+
if (instance.bufferSize > 0 && tl_buffer->data.size() >= instance.bufferSize)
122208
{
123209
toSend = std::move(tl_buffer->data);
124210
tl_buffer->data.clear();

libs/writer/writer_wrapper/writer.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
#include <singleton.h>
44
#include <writer_types.h>
55

6+
#include <chrono>
7+
#include <condition_variable>
68
#include <memory>
79
#include <mutex>
810
#include <string>
11+
#include <thread>
912
#include <vector>
1013

1114
namespace spectator {
@@ -42,14 +45,17 @@ class Writer final : public Singleton<Writer>
4245
Writer() = default;
4346

4447
static void Initialize(WriterType type, const std::string& param = "", int port = 0,
45-
unsigned int bufferSize = 0);
48+
unsigned int bufferSize = 0, unsigned int flushIntervalMs = 0);
4649

4750
static void Write(const std::string& message);
4851

4952
void ThreadLocalBufferedWrite(const std::string& message);
5053

5154
void NonBufferedWrite(const std::string& message);
5255

56+
// Background thread: periodically drains all thread-local buffers
57+
void FlushTimerThread();
58+
5359
void TryToSend(const std::string& message);
5460

5561
void Close();
@@ -71,7 +77,14 @@ class Writer final : public Singleton<Writer>
7177
std::mutex registryMutex;
7278
std::vector<std::weak_ptr<ThreadLocalBuffer>> threadBufferRegistry;
7379

80+
// Interval flush
81+
std::chrono::milliseconds flushInterval{0};
82+
std::thread flushTimerThread;
83+
84+
// Shutdown coordination
7485
std::atomic<bool> shutdown{false};
86+
std::mutex shutdownMutex;
87+
std::condition_variable cv_shutdown;
7588
};
7689

7790
} // namespace spectator

libs/writer/writer_wrapper/writer_test_helper.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ class WriterTestHelper
1515
public:
1616
// Initialize the Writer for testing purposes
1717
static void InitializeWriter(WriterType type, const std::string& param = "", int port = 0,
18-
unsigned int bufferSize = 0)
18+
unsigned int bufferSize = 0, unsigned int flushIntervalMs = 0)
1919
{
20-
Writer::Initialize(type, param, port, bufferSize);
20+
Writer::Initialize(type, param, port, bufferSize, flushIntervalMs);
2121
}
2222

2323
// Get the Writer's implementation for testing purposes

spectator/registry.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,25 @@ Registry::Registry(const Config& config) : m_config(config)
4141
if (config.GetWriterType() == WriterType::Memory)
4242
{
4343
Logger::info("Registry initializing Memory Writer");
44-
Writer::Initialize(config.GetWriterType(), "", 0, this->m_config.GetWriterBufferSize());
44+
Writer::Initialize(config.GetWriterType(), "", 0,
45+
this->m_config.GetWriterBufferSize(),
46+
this->m_config.GetFlushIntervalMs());
4547
}
4648
else if (config.GetWriterType() == WriterType::UDP)
4749
{
4850
auto [ip, port] = ParseUdpAddress(this->m_config.GetWriterLocation());
4951
Logger::info("Registry initializing UDP Writer at {}:{}", ip, port);
50-
Writer::Initialize(config.GetWriterType(), ip, port, this->m_config.GetWriterBufferSize());
52+
Writer::Initialize(config.GetWriterType(), ip, port,
53+
this->m_config.GetWriterBufferSize(),
54+
this->m_config.GetFlushIntervalMs());
5155
}
5256
else if (config.GetWriterType() == WriterType::Unix)
5357
{
5458
auto socketPath = ParseUnixAddress(this->m_config.GetWriterLocation());
5559
Logger::info("Registry initializing UDS Writer at {}", socketPath);
56-
Writer::Initialize(config.GetWriterType(), socketPath, 0, this->m_config.GetWriterBufferSize());
60+
Writer::Initialize(config.GetWriterType(), socketPath, 0,
61+
this->m_config.GetWriterBufferSize(),
62+
this->m_config.GetFlushIntervalMs());
5763
}
5864
}
5965

0 commit comments

Comments
 (0)