NeoGraph 0.16.0
A C++20 Graph Agent Engine Library — LangGraph for C++
Loading...
Searching...
No Matches
request_queue.h
Go to the documentation of this file.
1
9#pragma once
10
11#include <concurrentqueue.h>
12#include <chrono>
13#include <thread>
14#include <vector>
15#include <atomic>
16#include <functional>
17#include <future>
18#include <memory>
19#include <mutex>
20#include <condition_variable>
21#include <exception>
22#include <stdexcept>
23#include <utility>
24
25namespace neograph::util {
26
27namespace detail {
28
29// This is separate from RequestQueue only so tests can force the rare
30// enqueue-failure branch without relying on allocator failure. It does not
31// change RequestQueue's public object layout.
32template <typename Queue, typename F>
33std::pair<bool, std::future<void>> enqueue_reserved_task(
34 Queue& queue,
35 std::atomic<size_t>& pending,
36 std::atomic<size_t>& rejected,
37 F&& task) {
38 bool reservation_active = true;
39 auto rollback_reservation = [&] {
40 if (!reservation_active) return;
41 pending.fetch_sub(1, std::memory_order_acq_rel);
42 rejected.fetch_add(1, std::memory_order_relaxed);
43 reservation_active = false;
44 };
45
46 std::shared_ptr<std::promise<void>> promise;
47 std::future<void> future;
48 try {
49 promise = std::make_shared<std::promise<void>>();
50 future = promise->get_future();
51 std::function<void()> queued_task(
52 [t = std::forward<F>(task), p = promise]() mutable {
53 try {
54 t();
55 p->set_value();
56 } catch (...) {
57 p->set_exception(std::current_exception());
58 }
59 });
60
61 // Vendored ConcurrentQueue documents enqueue() as thread-safe but
62 // fallible; see deps/concurrentqueue.h:997-1017, checked for #207.
63 if (!queue.enqueue(std::move(queued_task))) {
64 rollback_reservation();
65 promise->set_exception(std::make_exception_ptr(
66 std::runtime_error("RequestQueue enqueue failed")));
67 return {false, std::move(future)};
68 }
69 reservation_active = false;
70 return {true, std::move(future)};
71 } catch (...) {
72 const auto error = std::current_exception();
73 rollback_reservation();
74 if (promise && future.valid()) {
75 promise->set_exception(error);
76 return {false, std::move(future)};
77 }
78 throw;
79 }
80}
81
82} // namespace detail
83
97public:
99 struct Stats {
100 size_t pending;
101 size_t active;
102 size_t completed;
103 size_t rejected;
104 size_t num_workers;
106 };
107
108private:
109 struct State {
110 explicit State(size_t max_queue_size)
111 : max_queue_size_(max_queue_size) {}
112
113 template<typename F>
114 std::pair<bool, std::future<void>> submit(F&& task) {
115 std::lock_guard<std::mutex> lock(lifecycle_mutex_);
116 if (!running_.load(std::memory_order_acquire)) {
117 rejected_.fetch_add(1, std::memory_order_relaxed);
118 return {false, std::future<void>()};
119 }
120 if (!try_reserve_pending_slot()) {
121 rejected_.fetch_add(1, std::memory_order_relaxed);
122 return {false, std::future<void>()};
123 }
124
125 auto result = detail::enqueue_reserved_task(
126 queue_, pending_, rejected_,
127 [this, t = std::forward<F>(task)]() mutable {
128 if (!claim_task_start()) {
129 throw std::runtime_error("RequestQueue is closed");
130 }
131 t();
132 });
133 if (result.first) cv_.notify_one();
134 return result;
135 }
136
137 void close() {
138 const bool caller_is_worker = current_worker_ == this;
139 const auto caller_id = std::this_thread::get_id();
140 {
141 std::unique_lock<std::mutex> lock(lifecycle_mutex_);
142 if (close_started_) {
143 if (caller_is_worker) return;
144 cv_.wait(lock, [this] { return close_complete_; });
145 return;
146 }
147
148 close_started_ = true;
149 worker_initiated_close_ = caller_is_worker;
150 running_.store(false, std::memory_order_release);
151 }
152 cv_.notify_all();
153
154 if (caller_is_worker) {
155 // A worker cannot join itself. Detach only that thread, then
156 // wait for every other claimed callback before the facade can
157 // be destroyed. State remains alive through the detached
158 // worker until it exits and marks shutdown complete.
159 for (auto& worker : workers_) {
160 if (!worker.joinable()) continue;
161 if (worker.get_id() == caller_id) {
162 worker.detach();
163 } else {
164 worker.join();
165 }
166 }
167 cancel_pending_tasks();
168 {
169 std::lock_guard<std::mutex> lock(lifecycle_mutex_);
170 cancellation_complete_ = true;
171 }
172 return;
173 }
174
175 for (auto& worker : workers_) {
176 if (worker.joinable()) worker.join();
177 }
178 cancel_pending_tasks();
179 {
180 std::lock_guard<std::mutex> lock(lifecycle_mutex_);
181 cancellation_complete_ = true;
182 close_complete_ = true;
183 }
184 cv_.notify_all();
185 }
186
187 bool is_closed() const noexcept {
188 return !running_.load(std::memory_order_acquire);
189 }
190
191 Stats stats() const {
192 return {
193 pending_.load(std::memory_order_relaxed),
194 active_.load(std::memory_order_relaxed),
195 completed_.load(std::memory_order_relaxed),
196 rejected_.load(std::memory_order_relaxed),
197 workers_.size(),
198 max_queue_size_
199 };
200 }
201
202 void run_worker() {
203 current_worker_ = this;
204 worker_loop();
205 worker_exited();
206 current_worker_ = nullptr;
207 }
208
209 std::mutex lifecycle_mutex_;
210 std::condition_variable cv_;
211 std::vector<std::thread> workers_;
212 std::atomic<bool> running_{true};
213 std::atomic<size_t> pending_{0};
214 std::atomic<size_t> active_{0};
215 std::atomic<size_t> completed_{0};
216 std::atomic<size_t> rejected_{0};
217 size_t max_queue_size_;
218 size_t workers_alive_{0};
219 bool close_started_{false};
220 bool close_complete_{false};
221 bool cancellation_complete_{false};
222 bool worker_initiated_close_{false};
223 inline static thread_local State* current_worker_ = nullptr;
224
225 private:
226 bool try_reserve_pending_slot() {
227 size_t pending = pending_.load(std::memory_order_relaxed);
228 while (pending < max_queue_size_) {
229 if (pending_.compare_exchange_weak(
230 pending, pending + 1,
231 std::memory_order_acq_rel,
232 std::memory_order_relaxed)) {
233 return true;
234 }
235 }
236 return false;
237 }
238
239 bool claim_task_start() {
240 std::lock_guard<std::mutex> lock(lifecycle_mutex_);
241 return running_.load(std::memory_order_acquire);
242 }
243
244 void worker_loop() {
245 std::function<void()> task;
246 while (running_.load(std::memory_order_acquire)) {
247 if (queue_.try_dequeue(task)) {
248 pending_.fetch_sub(1, std::memory_order_acq_rel);
249 execute_task(task);
250 } else {
251 // No busy-spin — sleep until notified by submit().
252 std::unique_lock<std::mutex> lock(lifecycle_mutex_);
253 cv_.wait_for(lock, std::chrono::milliseconds(50), [this] {
254 return !running_.load(std::memory_order_acquire)
255 || queue_.size_approx() > 0;
256 });
257 }
258 }
259 }
260
261 void worker_exited() {
262 bool finalize_worker_close = false;
263 {
264 std::lock_guard<std::mutex> lock(lifecycle_mutex_);
265 --workers_alive_;
266 finalize_worker_close = close_started_
267 && worker_initiated_close_
268 && !close_complete_
269 && workers_alive_ == 0;
270 }
271 if (!finalize_worker_close) return;
272
273 bool cancel_pending = false;
274 {
275 std::lock_guard<std::mutex> lock(lifecycle_mutex_);
276 cancel_pending = !cancellation_complete_;
277 }
278 if (cancel_pending) cancel_pending_tasks();
279 {
280 std::lock_guard<std::mutex> lock(lifecycle_mutex_);
281 cancellation_complete_ = true;
282 close_complete_ = true;
283 }
284 cv_.notify_all();
285 }
286
287 void cancel_pending_tasks() {
288 std::function<void()> task;
289 while (queue_.try_dequeue(task)) {
290 pending_.fetch_sub(1, std::memory_order_acq_rel);
291 try {
292 task();
293 } catch (...) {
294 // The task wrapper owns its promise and reports errors.
295 }
296 completed_.fetch_add(1, std::memory_order_relaxed);
297 }
298 }
299
300 void execute_task(std::function<void()>& task) {
301 active_.fetch_add(1, std::memory_order_relaxed);
302 try {
303 task();
304 } catch (...) {
305 // A task wrapper should already complete its promise, but a
306 // malformed task must not strand accounting or kill a worker.
307 }
308 active_.fetch_sub(1, std::memory_order_relaxed);
309 completed_.fetch_add(1, std::memory_order_relaxed);
310 }
311
312 moodycamel::ConcurrentQueue<std::function<void()>> queue_;
313 };
314
315public:
322 RequestQueue(size_t num_workers = 128, size_t max_queue_size = 10000)
323 : state_(std::make_shared<State>(max_queue_size))
324 {
325 if (num_workers == 0) {
326 throw std::invalid_argument("RequestQueue requires at least one worker");
327 }
328 try {
329 for (size_t i = 0; i < num_workers; ++i) {
330 start_worker();
331 }
332 } catch (...) {
333 state_->close();
334 throw;
335 }
336 // No banner on stdout — library code shouldn't pollute the
337 // host process's stdout (breaks scripts that parse it). If
338 // observability is desired, route through an injected logger
339 // instead of std::cout.
340 }
341
344 close();
345 }
346
347 RequestQueue(const RequestQueue&) = delete;
348 RequestQueue& operator=(const RequestQueue&) = delete;
349
367 template<typename F>
368 std::pair<bool, std::future<void>> submit(F&& task) {
369 return state_->submit(std::forward<F>(task));
370 }
371
381 void close() {
382 auto state = state_;
383 state->close();
384 }
385
387 bool is_closed() const noexcept {
388 return state_->is_closed();
389 }
390
395 Stats stats() const {
396 return state_->stats();
397 }
398
399private:
400 void start_worker() {
401 {
402 std::lock_guard<std::mutex> lock(state_->lifecycle_mutex_);
403 ++state_->workers_alive_;
404 }
405 try {
406 auto state = state_;
407 state_->workers_.emplace_back([state] { state->run_worker(); });
408 } catch (...) {
409 std::lock_guard<std::mutex> lock(state_->lifecycle_mutex_);
410 --state_->workers_alive_;
411 throw;
412 }
413 }
414
415 std::shared_ptr<State> state_;
416};
417
418} // namespace neograph::util
Lock-free task queue with a fixed worker thread pool and backpressure.
bool is_closed() const noexcept
True once close() has started rejecting new work.
RequestQueue(size_t num_workers=128, size_t max_queue_size=10000)
Construct a request queue with a worker thread pool.
void close()
Stop accepting work, cancel queued tasks, and wait for workers.
Stats stats() const
Get current queue statistics.
std::pair< bool, std::future< void > > submit(F &&task)
Submit a task to the queue.
~RequestQueue()
Destructor: applies the same cancel-on-close policy as close().
Runtime statistics for monitoring queue health.
size_t rejected
Tasks rejected during admission.
size_t completed
Total tasks settled, including close cancellation.
size_t pending
Tasks waiting in the queue.
size_t active
Tasks currently being executed.
size_t max_queue_size
Maximum queue capacity.
size_t num_workers
Number of worker threads.