VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Client.cpp
Go to the documentation of this file.
2
5
6namespace VoltMod
7{
8
9/** Jobs beyond this are refused while the database stalls. */
10static constexpr size_t MaxQueuedJobs = 4096;
11/** After a failed connect, jobs fail at once for this long. */
12static constexpr auto ReconnectBackoff = std::chrono::seconds(5);
13
18
20{
21 const Result<Driver> driver = CheckConfig(config);
22 if (!driver)
23 {
24 Log::Error("{}", driver.error().Detail);
25 return false;
26 }
27
28 {
29 std::lock_guard lock(_queueMutex);
30 if (_state != State::Stopped)
31 {
32 return true;
33 }
34 _config = config;
35 _driver = *driver;
36 _state = State::Running;
37 }
38 _worker = std::thread([this] { WorkerMain(); });
39
40 // Typed, not raw: a raw SELECT leaves an unread result set on MariaDB.
41 auto ping = Run("db_ping", [](auto& conn) {
42 for ([[maybe_unused]] const auto& row : conn(sqlpp::select(sqlpp::value(1).as(sqlpp::alias::a))))
43 {}
44 });
45 if (!ping)
46 {
47 Disconnect();
48 return false;
49 }
50
51 _onFrame = _scheduler.EveryFrame([this] { DispatchCompletions(); });
52 return true;
53}
54
55void Database::Disconnect(std::chrono::milliseconds stopDeadline)
56{
57 {
58 std::lock_guard lock(_queueMutex);
59 if (_state != State::Running)
60 {
61 return;
62 }
63 _state = State::Stopping;
64 _stopDeadline = std::chrono::steady_clock::now() + stopDeadline;
65 }
66 _queueCv.notify_all();
67 _worker.join();
68
69 {
70 std::lock_guard lock(_queueMutex);
71 _state = State::Stopped;
72 }
73 _onFrame.Reset();
74 // Dropped unrun: the state they touch is unloading.
75 _completions.Clear();
76}
77
78void Database::Enqueue(Job job)
79{
80 std::string_view refused;
81 {
82 std::lock_guard lock(_queueMutex);
83 if (_state != State::Running)
84 {
85 refused = "database not running";
86 }
87 else if (_queue.size() >= MaxQueuedJobs)
88 {
89 refused = "the job queue is full";
90 }
91 else
92 {
93 _queue.push_back(std::move(job));
94 }
95 }
96
97 if (refused.empty())
98 {
99 _queueCv.notify_one();
100 return;
101 }
102 Log::Warn("db: '{}' failed - {}.", job.Name, refused);
103 // Fail delivers on a later frame, so the caller is never re-entered here.
104 job.Fail(Error::NotReady(std::string(refused)));
105}
106
107void Database::WorkerMain()
108{
109 while (std::optional<Job> job = TakeJob())
110 {
111 RunJob(*job);
112 }
113 DropConnection();
114}
115
116std::optional<Database::Job> Database::TakeJob()
117{
118 std::unique_lock lock(_queueMutex);
119 _queueCv.wait(lock, [this] { return !_queue.empty() || _state == State::Stopping; });
120
121 // A dead database must not hang the unload.
122 const bool pastDeadline = _state == State::Stopping && std::chrono::steady_clock::now() >= _stopDeadline;
123 if (pastDeadline)
124 {
125 for (Job& dropped : _queue)
126 {
127 Log::Warn("db: dropping queued '{}' - shutdown stop deadline reached.", dropped.Name);
128 dropped.Fail(Error::NotReady("shutdown"));
129 }
130 _queue.clear();
131 }
132 if (_queue.empty())
133 {
134 return std::nullopt;
135 }
136
137 Job job = std::move(_queue.front());
138 _queue.pop_front();
139 return job;
140}
141
142void Database::RunJob(Job& job)
143{
144 if (!EnsureOpen())
145 {
146 Log::Error("db: '{}' failed - no database connection.", job.Name);
147 job.Fail(Error::NotReady("no database connection"));
148 return;
149 }
150
151 try
152 {
153 job.Run(_connection);
154 }
155 catch (const std::exception& e)
156 {
157 Log::Error("db: {} failed: {}", job.Name, e.what());
158 DropConnection(); // its state is unknown
159 job.Fail(Error::Failed(e.what()));
160 }
161}
162
163bool Database::EnsureOpen()
164{
165 if (IsOpen(_connection))
166 {
167 return true;
168 }
169 const auto now = std::chrono::steady_clock::now();
170 if (now < _retryAt)
171 {
172 return false;
173 }
174
175 try
176 {
177 OpenConnection(_connection, _driver, _config);
178 _connected.store(true, std::memory_order_relaxed);
179 return true;
180 }
181 catch (const std::exception&)
182 {
183 // The exception text can echo the connection string, password included.
184 Log::Error("Database connection failed - check host/port/credentials.");
185 }
186 DropConnection();
187 _retryAt = now + ReconnectBackoff;
188 return false;
189}
190
191void Database::DropConnection()
192{
193 _connection.emplace<std::monostate>();
194 _connected.store(false, std::memory_order_relaxed);
195}
196
197} // namespace VoltMod
Result< ResultOf< Fn > > Run(std::string name, Fn fn)
Definition Client.hpp:98
bool Connect(const DatabaseConfig &config)
Definition Client.cpp:19
void Disconnect(std::chrono::milliseconds stopDeadline=std::chrono::seconds(5))
Definition Client.cpp:55
void DispatchCompletions()
Definition Client.hpp:117
Subscription EveryFrame(std::function< void()> callback)
Definition Scheduler.cpp:35
void Error(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:97
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
bool IsOpen(const AnyConnection &connection)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static constexpr size_t MaxQueuedJobs
Definition Client.cpp:10
Result< Driver > CheckConfig(const DatabaseConfig &config)
std::expected< T, Error > Result
Definition Result.hpp:64
static constexpr auto ReconnectBackoff
Definition Client.cpp:12
void OpenConnection(AnyConnection &connection, Driver driver, const DatabaseConfig &config)
Connection parameters for every supported backend.
static Error Failed(std::string detail)
Definition Result.hpp:59
static Error NotReady(std::string detail)
Definition Result.hpp:50