Clio  develop
The XRP Ledger API server.
Loading...
Searching...
No Matches
ExecutionStrategy.hpp
1#pragma once
2
3#include "data/BackendCounters.hpp"
4#include "data/BackendInterface.hpp"
5#include "data/cassandra/Handle.hpp"
6#include "data/cassandra/Types.hpp"
7#include "data/cassandra/impl/AsyncExecutor.hpp"
8#include "util/Assert.hpp"
9#include "util/Batching.hpp"
10#include "util/log/Logger.hpp"
11
12#include <boost/asio.hpp>
13#include <boost/asio/associated_executor.hpp>
14#include <boost/asio/executor_work_guard.hpp>
15#include <boost/asio/io_context.hpp>
16#include <boost/asio/spawn.hpp>
17#include <boost/json/object.hpp>
18#include <fmt/format.h>
19
20#include <algorithm>
21#include <atomic>
22#include <chrono>
23#include <condition_variable>
24#include <cstddef>
25#include <cstdint>
26#include <functional>
27#include <memory>
28#include <mutex>
29#include <optional>
30#include <stdexcept>
31#include <string>
32#include <thread>
33#include <type_traits>
34#include <vector>
35
36namespace data::cassandra::impl {
37
38// TODO: this could probably be also moved out of impl and into the main cassandra namespace.
39
46template <typename HandleType = Handle, SomeBackendCounters BackendCountersType = BackendCounters>
48 util::Logger log_{"Backend"};
49
50 std::uint32_t maxWriteRequestsOutstanding_;
51 std::atomic_uint32_t numWriteRequestsOutstanding_ = 0;
52
53 std::uint32_t maxReadRequestsOutstanding_;
54 std::atomic_uint32_t numReadRequestsOutstanding_ = 0;
55
56 std::size_t writeBatchSize_;
57
58 std::mutex throttleMutex_;
59 std::condition_variable throttleCv_;
60
61 std::mutex syncMutex_;
62 std::condition_variable syncCv_;
63
64 boost::asio::io_context ioc_;
65 std::optional<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> work_;
66
67 std::reference_wrapper<HandleType const> handle_;
68 std::thread thread_;
69
70 BackendCountersType::PtrType counters_;
71
72public:
73 using ResultOrErrorType = HandleType::ResultOrErrorType;
74 using StatementType = HandleType::StatementType;
75 using PreparedStatementType = HandleType::PreparedStatementType;
76 using FutureType = HandleType::FutureType;
77 using FutureWithCallbackType = HandleType::FutureWithCallbackType;
78 using ResultType = HandleType::ResultType;
79 using CompletionTokenType = boost::asio::yield_context;
80
86 Settings const& settings,
87 HandleType const& handle,
88 BackendCountersType::PtrType counters = BackendCountersType::make()
89 )
90 : maxWriteRequestsOutstanding_{settings.maxWriteRequestsOutstanding}
91 , maxReadRequestsOutstanding_{settings.maxReadRequestsOutstanding}
92 , writeBatchSize_{settings.writeBatchSize}
93 , work_{boost::asio::make_work_guard(ioc_)}
94 , handle_{std::cref(handle)}
95 , thread_{[this]() { ioc_.run(); }}
96 , counters_{std::move(counters)}
97 {
98 LOG(log_.info()) << "Max write requests outstanding is " << maxWriteRequestsOutstanding_
99 << "; Max read requests outstanding is " << maxReadRequestsOutstanding_;
100 }
101
102 ~DefaultExecutionStrategy()
103 {
104 work_.reset();
105 ioc_.stop();
106 thread_.join();
107 }
108
112 void
114 {
115 LOG(log_.debug()) << "Waiting to sync all writes...";
116 std::unique_lock<std::mutex> lck(syncMutex_);
117 syncCv_.wait(lck, [this]() { return finishedAllWriteRequests(); });
118 LOG(log_.debug()) << "Sync done.";
119 }
120
124 [[nodiscard]] bool
125 isTooBusy() const
126 {
127 bool const result = numReadRequestsOutstanding_ >= maxReadRequestsOutstanding_;
128 if (result)
129 counters_->registerTooBusy();
130 return result;
131 }
132
138 ResultOrErrorType
139 writeSync(StatementType const& statement)
140 {
141 auto const startTime = std::chrono::steady_clock::now();
142 while (true) {
143 auto res = handle_.get().execute(statement);
144 if (res) {
145 counters_->registerWriteSync(startTime);
146 return res;
147 }
148
149 counters_->registerWriteSyncRetry();
150 LOG(log_.warn()) << "Cassandra sync write error, retrying: " << res.error();
151 std::this_thread::sleep_for(std::chrono::milliseconds(5));
152 }
153 }
154
160 template <typename... Args>
161 ResultOrErrorType
162 writeSync(PreparedStatementType const& preparedStatement, Args&&... args)
163 {
164 return writeSync(preparedStatement.bind(std::forward<Args>(args)...));
165 }
166
176 template <typename... Args>
177 void
178 write(PreparedStatementType const& preparedStatement, Args&&... args)
179 {
180 auto statement = preparedStatement.bind(std::forward<Args>(args)...);
181 write(std::move(statement));
182 }
183
192 void
193 write(StatementType&& statement)
194 {
195 auto const startTime = std::chrono::steady_clock::now();
196
197 incrementOutstandingRequestCount();
198
199 counters_->registerWriteStarted();
200 // Note: lifetime is controlled by std::shared_from_this internally
201 AsyncExecutor<std::decay_t<decltype(statement)>, HandleType>::run(
202 ioc_,
203 handle_,
204 std::move(statement),
205 [this, startTime](auto const&) {
206 decrementOutstandingRequestCount();
207
208 counters_->registerWriteFinished(startTime);
209 },
210 [this]() { counters_->registerWriteRetry(); }
211 );
212 }
213
222 void
223 write(std::vector<StatementType>&& statements)
224 {
225 if (statements.empty())
226 return;
227
228 util::forEachBatch(std::move(statements), writeBatchSize_, [this](auto begin, auto end) {
229 auto const startTime = std::chrono::steady_clock::now();
230 auto chunk = std::vector<StatementType>{};
231
232 chunk.reserve(std::distance(begin, end));
233 std::move(begin, end, std::back_inserter(chunk));
234
235 incrementOutstandingRequestCount();
236 counters_->registerWriteStarted();
237
238 // Note: lifetime is controlled by std::shared_from_this internally
239 AsyncExecutor<std::decay_t<decltype(chunk)>, HandleType>::run(
240 ioc_,
241 handle_,
242 std::move(chunk),
243 [this, startTime](auto const&) {
244 decrementOutstandingRequestCount();
245 counters_->registerWriteFinished(startTime);
246 },
247 [this]() { counters_->registerWriteRetry(); }
248 );
249 });
250 }
251
261 void
262 writeEach(std::vector<StatementType>&& statements)
263 {
264 std::ranges::for_each(std::move(statements), [this](auto& statement) {
265 this->write(std::move(statement));
266 });
267 }
268
280 template <typename... Args>
281 [[maybe_unused]] ResultOrErrorType
282 read(CompletionTokenType token, PreparedStatementType const& preparedStatement, Args&&... args)
283 {
284 return read(token, preparedStatement.bind(std::forward<Args>(args)...));
285 }
286
297 [[maybe_unused]] ResultOrErrorType
298 read(CompletionTokenType token, std::vector<StatementType> const& statements)
299 {
300 auto const startTime = std::chrono::steady_clock::now();
301
302 auto const numStatements = statements.size();
303 std::optional<FutureWithCallbackType> future;
304 counters_->registerReadStarted(numStatements);
305
306 // todo: perhaps use policy instead
307 while (true) {
308 numReadRequestsOutstanding_ += numStatements;
309
310 auto init = [this, &statements, &future]<typename Self>(Self& self) {
311 auto sself = std::make_shared<Self>(std::move(self));
312
313 future.emplace(handle_.get().asyncExecute(statements, [sself](auto&& res) mutable {
314 boost::asio::post(
315 boost::asio::get_associated_executor(*sself),
316 [sself, res = std::forward<decltype(res)>(res)]() mutable {
317 sself->complete(std::move(res));
318 }
319 );
320 }));
321 };
322
323 auto res = boost::asio::async_compose<CompletionTokenType, void(ResultOrErrorType)>(
324 std::move(init), token, boost::asio::get_associated_executor(token)
325 );
326 numReadRequestsOutstanding_ -= numStatements;
327
328 if (res) {
329 counters_->registerReadFinished(startTime, numStatements);
330 return res;
331 }
332
333 LOG(log_.error()) << "Failed batch read in coroutine: " << res.error();
334 try {
335 throwErrorIfNeeded(res.error());
336 } catch (...) {
337 counters_->registerReadError(numStatements);
338 throw;
339 }
340 counters_->registerReadRetry(numStatements);
341 }
342 }
343
354 [[maybe_unused]] ResultOrErrorType
355 read(CompletionTokenType token, StatementType const& statement)
356 {
357 auto const startTime = std::chrono::steady_clock::now();
358
359 std::optional<FutureWithCallbackType> future;
360 counters_->registerReadStarted();
361
362 // todo: perhaps use policy instead
363 while (true) {
364 ++numReadRequestsOutstanding_;
365 auto init = [this, &statement, &future]<typename Self>(Self& self) {
366 auto sself = std::make_shared<Self>(std::move(self));
367
368 future.emplace(handle_.get().asyncExecute(statement, [sself](auto&& res) mutable {
369 boost::asio::post(
370 boost::asio::get_associated_executor(*sself),
371 [sself, res = std::forward<decltype(res)>(res)]() mutable {
372 sself->complete(std::move(res));
373 }
374 );
375 }));
376 };
377
378 auto res = boost::asio::async_compose<CompletionTokenType, void(ResultOrErrorType)>(
379 std::move(init), token, boost::asio::get_associated_executor(token)
380 );
381 --numReadRequestsOutstanding_;
382
383 if (res) {
384 counters_->registerReadFinished(startTime);
385 return res;
386 }
387
388 LOG(log_.error()) << "Failed read in coroutine: " << res.error();
389 try {
390 throwErrorIfNeeded(res.error());
391 } catch (...) {
392 counters_->registerReadError();
393 throw;
394 }
395 counters_->registerReadRetry();
396 }
397 }
398
410 std::vector<ResultType>
411 readEach(CompletionTokenType token, std::vector<StatementType> const& statements)
412 {
413 auto const startTime = std::chrono::steady_clock::now();
414
415 std::atomic_uint64_t errorsCount = 0u;
416 std::atomic_int numOutstanding = statements.size();
417 numReadRequestsOutstanding_ += statements.size();
418
419 auto futures = std::vector<FutureWithCallbackType>{};
420 futures.reserve(numOutstanding);
421 counters_->registerReadStarted(statements.size());
422
423 auto init = [this, &statements, &futures, &errorsCount, &numOutstanding]<typename Self>(
424 Self& self
425 ) {
426 auto sself = std::make_shared<Self>(std::move(self));
427 auto executionHandler =
428 [&errorsCount, &numOutstanding, sself](auto const& res) mutable {
429 if (not res)
430 ++errorsCount;
431
432 // when all async operations complete unblock the result
433 if (--numOutstanding == 0) {
434 boost::asio::post(
435 boost::asio::get_associated_executor(*sself),
436 [sself]() mutable { sself->complete(); }
437 );
438 }
439 };
440
441 std::transform(
442 std::cbegin(statements),
443 std::cend(statements),
444 std::back_inserter(futures),
445 [this, &executionHandler](auto const& statement) {
446 return handle_.get().asyncExecute(statement, executionHandler);
447 }
448 );
449 };
450
451 boost::asio::async_compose<CompletionTokenType, void()>(
452 std::move(init), token, boost::asio::get_associated_executor(token)
453 );
454 numReadRequestsOutstanding_ -= statements.size();
455
456 if (errorsCount > 0) {
457 ASSERT(
458 errorsCount <= statements.size(), "Errors number cannot exceed statements number"
459 );
460 counters_->registerReadError(errorsCount);
461 counters_->registerReadFinished(startTime, statements.size() - errorsCount);
462 throw DatabaseError{};
463 }
464 counters_->registerReadFinished(startTime, statements.size());
465
466 std::vector<ResultType> results;
467 results.reserve(futures.size());
468
469 // it's safe to call blocking get on futures here as we already waited for the coroutine to
470 // resume above.
471 std::transform(
472 std::make_move_iterator(std::begin(futures)),
473 std::make_move_iterator(std::end(futures)),
474 std::back_inserter(results),
475 [](auto&& future) {
476 auto entry = future.get();
477 auto&& res = entry.value();
478 return std::move(res);
479 }
480 );
481
482 ASSERT(
483 futures.size() == statements.size(),
484 "Futures size must be equal to statements size. Got {} and {}",
485 futures.size(),
486 statements.size()
487 );
488 ASSERT(
489 results.size() == statements.size(),
490 "Results size must be equal to statements size. Got {} and {}",
491 results.size(),
492 statements.size()
493 );
494 return results;
495 }
496
500 [[nodiscard]] boost::json::object
501 stats() const
502 {
503 return counters_->report();
504 }
505
506private:
507 void
508 incrementOutstandingRequestCount()
509 {
510 {
511 std::unique_lock<std::mutex> lck(throttleMutex_);
512 if (!canAddWriteRequest()) {
513 LOG(log_.trace()) << "Max outstanding requests reached. "
514 << "Waiting for other requests to finish";
515 throttleCv_.wait(lck, [this]() { return canAddWriteRequest(); });
516 }
517 }
518 ++numWriteRequestsOutstanding_;
519 }
520
521 void
522 decrementOutstandingRequestCount()
523 {
524 // sanity check
525 ASSERT(numWriteRequestsOutstanding_ > 0, "Decrementing num outstanding below 0");
526 size_t const cur = (--numWriteRequestsOutstanding_);
527 {
528 // mutex lock required to prevent race condition around spurious
529 // wakeup
530 std::scoped_lock const lck(throttleMutex_);
531 throttleCv_.notify_one();
532 }
533 if (cur == 0) {
534 // mutex lock required to prevent race condition around spurious
535 // wakeup
536 std::scoped_lock const lck(syncMutex_);
537 syncCv_.notify_one();
538 }
539 }
540
541 [[nodiscard]] bool
542 canAddWriteRequest() const
543 {
544 return numWriteRequestsOutstanding_ < maxWriteRequestsOutstanding_;
545 }
546
547 [[nodiscard]] bool
548 finishedAllWriteRequests() const
549 {
550 return numWriteRequestsOutstanding_ == 0;
551 }
552
553 void
554 throwErrorIfNeeded(CassandraError err) const
555 {
556 // NOTE: etl::impl::Loader and etl::impl::Extractor treat std::runtime_error as
557 // "amendment blocked", so only genuinely permanent failures may be thrown as one.
558 if (err.isInvalidQuery())
559 throw std::runtime_error("Invalid query");
560
561 // anything else, including unclassified codes, is transient and gets retried
562 throw DatabaseError{fmt::format("Database error [{}]: {}", err.code(), err.message())};
563 }
564};
565
566} // namespace data::cassandra::impl
Represents a transient database error that the caller should retry.
Definition BackendInterface.hpp:41
A query executor with a changeable retry policy.
Definition AsyncExecutor.hpp:37
ResultOrErrorType read(CompletionTokenType token, StatementType const &statement)
Coroutine-based query execution used for reading data.
Definition ExecutionStrategy.hpp:355
void write(StatementType &&statement)
Non-blocking query execution used for writing data.
Definition ExecutionStrategy.hpp:193
ResultOrErrorType read(CompletionTokenType token, PreparedStatementType const &preparedStatement, Args &&... args)
Coroutine-based query execution used for reading data.
Definition ExecutionStrategy.hpp:282
ResultOrErrorType read(CompletionTokenType token, std::vector< StatementType > const &statements)
Coroutine-based query execution used for reading data.
Definition ExecutionStrategy.hpp:298
void write(PreparedStatementType const &preparedStatement, Args &&... args)
Non-blocking query execution used for writing data.
Definition ExecutionStrategy.hpp:178
void write(std::vector< StatementType > &&statements)
Non-blocking batched query execution used for writing data.
Definition ExecutionStrategy.hpp:223
void sync()
Wait for all async writes to finish before unblocking.
Definition ExecutionStrategy.hpp:113
ResultOrErrorType writeSync(PreparedStatementType const &preparedStatement, Args &&... args)
Blocking query execution used for writing data.
Definition ExecutionStrategy.hpp:162
DefaultExecutionStrategy(Settings const &settings, HandleType const &handle, BackendCountersType::PtrType counters=BackendCountersType::make())
Definition ExecutionStrategy.hpp:85
bool isTooBusy() const
Definition ExecutionStrategy.hpp:125
ResultOrErrorType writeSync(StatementType const &statement)
Blocking query execution used for writing data.
Definition ExecutionStrategy.hpp:139
std::vector< ResultType > readEach(CompletionTokenType token, std::vector< StatementType > const &statements)
Coroutine-based query execution used for reading data.
Definition ExecutionStrategy.hpp:411
boost::json::object stats() const
Get statistics about the backend.
Definition ExecutionStrategy.hpp:501
void writeEach(std::vector< StatementType > &&statements)
Non-blocking query execution used for writing data. Contrast with write, this method does not execute...
Definition ExecutionStrategy.hpp:262
A simple thread-safe logger for the channel specified in the constructor.
Definition Logger.hpp:78
Pump info(std::source_location const &loc=std::source_location::current()) const
Interface for logging at Severity::NFO severity.
Definition Logger.cpp:502
Pump trace(std::source_location const &loc=std::source_location::current()) const
Interface for logging at Severity::TRC severity.
Definition Logger.cpp:492
void forEachBatch(std::ranges::forward_range auto &&container, std::size_t batchSize, auto &&fn)
Iterate over a container in batches.
Definition Batching.hpp:19
Bundles all cassandra settings in one place.
Definition Cluster.hpp:37