Lightweight 0.20260921.0
Loading...
Searching...
No Matches
Pool.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include "../Async/Executor.hpp"
5#include "../Async/Task.hpp"
6#include "../SqlConnectInfo.hpp"
7#include "../SqlLogger.hpp"
8#include "../SqlStatistics.hpp"
9#include "DataMapper.hpp"
10
11#include <cassert>
12#include <chrono>
13#include <condition_variable>
14#include <coroutine>
15#include <cstdint>
16#include <deque>
17#include <expected>
18#include <functional>
19#include <memory>
20#include <mutex>
21#include <ranges>
22#include <stdexcept>
23#include <vector>
24
25/// @defgroup ConnectionPool Connection Pooling
26/// @brief A thread-safe pool of @c DataMapper instances, configured at compile time.
27///
28/// The growth strategy, initial size and maximum size are supplied as a @c PoolConfig
29/// template parameter, so the policy is fixed at the type level rather than at runtime.
30
31namespace Lightweight
32{
33
34/// @ingroup ConnectionPool
35/// Enum to define growth strategies of the pool
36///
37enum class GrowthStrategy : uint8_t
38{
39 /// Pre-create initialSize objects. Allow the total count
40 /// to grow up to maxSize. Once maxSize objects exist, callers BLOCK
41 /// until one is returned.
43
44 /// Pre-create initialSize objects. The pool stores up to maxSize
45 /// objects. If none are idle, a fresh object is ALWAYS created (no
46 /// waiting). On return: kept if the idle set is below maxSize, otherwise
47 /// destroyed.
49
50 /// Pre-create initialSize objects. Grow without limit. Every returned
51 /// object is always kept in the pool.
53};
54
55/// @ingroup ConnectionPool
56/// Whether the pool checks that a connection is still live before handing it to a caller.
57enum class ValidateOnBorrow : uint8_t
58{
59 /// Hand the connection out without checking it.
60 No,
61
62 /// Check SqlConnection::IsAlive() first and discard a connection reported dead, transparently
63 /// serving the caller from the next idle connection or a freshly created one.
64 ///
65 /// The check reads the driver-local @c SQL_ATTR_CONNECTION_DEAD attribute, so it costs no round
66 /// trip to the server. By the same token it is only as good as the driver's own bookkeeping:
67 /// several drivers mark a connection dead only after an operation has already failed, so a
68 /// connection whose peer vanished silently (a firewall or NAT dropping the flow without sending
69 /// FIN or RST) can still pass this check. Pair it with @ref PoolConfig::maxIdleTimeMs to retire
70 /// such connections before they are ever handed out.
71 Yes,
72};
73
74/// @ingroup ConnectionPool
75/// Reason an @ref Pool::Acquire call with a timeout failed to produce a data mapper.
76enum class PoolError : uint8_t
77{
78 /// The timeout elapsed before a data mapper became available.
79 Timeout,
80};
81
82/// @ingroup ConnectionPool
83/// Structure to hold the configuration of the pool, including the initial size, maximum size and growth strategy.
84/// Structure is used as a template parameter for the Pool class to configure its behavior at compile time.
85///
86/// @note The lifetime bounds are expressed as plain millisecond counts rather than
87/// @c std::chrono::milliseconds because this structure is used as a non-type template
88/// parameter: @c std::chrono::duration keeps its representation private and is therefore not a
89/// structural type. Use @ref PoolConfig::MaxIdleTime and @ref PoolConfig::MaxLifetime to read
90/// them back as durations.
92{
93 /// Initial number of data mappers to pre-create and store in the pool, must be less than or equal to maxSize
94 size_t initialSize {};
95 /// Maximum number of data mappers that can exist in the pool, must be greater than or equal to initialSize
96 /// this is used for the Bounded* strategies to determine when to block or when to stop accepting returned data mappers,
97 /// for the UnboundedGrow strategy this is ignored
98 size_t maxSize {};
99 /// Strategy to determine how the pool should grow when there are no idle data mappers available, default is BoundedWait
100 /// which blocks until a data mapper is returned to the pool
102
103 /// Whether a connection is checked for liveness before it is handed to a caller, enabled by default.
104 ///
105 /// @see ValidateOnBorrow
107
108 /// Maximum time in milliseconds a connection may sit idle in the pool before it is retired
109 /// instead of handed out again; 0 (the default) disables the bound.
110 ///
111 /// Set this below any idle timeout imposed by the network path (a firewall or NAT dropping idle
112 /// flows) or by the server, so a connection is never idle long enough to be reaped behind the
113 /// pool's back. This removes the failure mode that @ref ValidateOnBorrow can only detect, and
114 /// then only when the driver has noticed.
115 ///
116 /// @note Retirement is lazy: it happens when the pool is next used, so a pool that goes
117 /// completely idle keeps its connections until the next @ref Pool::Acquire. Correctness is
118 /// unaffected — an expired connection is discarded rather than handed out — but the pool
119 /// does not shrink on its own.
120 std::chrono::milliseconds::rep maxIdleTimeMs {};
121
122 /// Maximum total age in milliseconds of a connection, counted from when it was created, after
123 /// which it is retired rather than reused; 0 (the default) disables the bound.
124 ///
125 /// Unlike @ref validateOnBorrow, this retires connections that are perfectly alive but no longer
126 /// appropriate: after a failover or a rolling restart a pooled connection stays bound to the old
127 /// node, and nothing else in the pool will ever move it. Setting this shorter than any
128 /// connection-age ceiling imposed by the database or the infrastructure also means connections
129 /// are retired while idle, which is free, rather than being cut mid-query by something else.
130 ///
131 /// @note Retirement is lazy, as described for @ref maxIdleTimeMs.
132 std::chrono::milliseconds::rep maxLifetimeMs {};
133
134 /// Prepared-statement cache capacity given to the connection of every data mapper this pool creates,
135 /// i.e. how many already-prepared ODBC statement handles that connection keeps for reuse. Zero (the
136 /// default) leaves the cache disabled, exactly as an unpooled connection.
137 ///
138 /// The bound is per connection, not per pool: a pool may hold up to `maxSize` connections, each with
139 /// its own cache of this size, so the live prepared handles a fully warmed pool holds on the server
140 /// are `maxSize * preparedStatementCacheCapacity`. Size it against the backend's per-session limit
141 /// on prepared statements, not against the number of distinct queries alone.
142 ///
143 /// @see SqlConnection::SetPreparedStatementCacheCapacity for what enabling the cache implies.
144 size_t preparedStatementCacheCapacity { PreparedStatementCacheCapacityDefault };
145
146 /// @return @ref maxIdleTimeMs as a duration.
147 [[nodiscard]] constexpr std::chrono::milliseconds MaxIdleTime() const noexcept
148 {
149 return std::chrono::milliseconds { maxIdleTimeMs };
150 }
151
152 /// @return @ref maxLifetimeMs as a duration.
153 [[nodiscard]] constexpr std::chrono::milliseconds MaxLifetime() const noexcept
154 {
155 return std::chrono::milliseconds { maxLifetimeMs };
156 }
157};
158
159/// @ingroup ConnectionPool
160/// A thread-safe pool of DataMapper instances with the policy configured by the PoolConfig template parameter.
161/// The pool allows acquiring and returning DataMapper instances, and manages the lifecycle of these instances according to
162/// the specified growth strategy.
163template <PoolConfig Config>
164class Pool
165{
166 private:
167 /// Clock the idle-time and lifetime bounds are measured against. Monotonic, so the bounds are
168 /// immune to wall-clock adjustments.
169 using Clock = std::chrono::steady_clock;
170
171 /// True when this configuration enables at least one time-based bound, and the pool therefore
172 /// has to timestamp its connections. When false, no clock is ever read.
173 static constexpr bool TracksTime = Config.maxIdleTimeMs > 0 || Config.maxLifetimeMs > 0;
174
175 /// A pooled DataMapper together with the timestamps the health bounds are evaluated against.
176 ///
177 /// @c createdAt travels with the mapper across checkout and return, so @ref PoolConfig::maxLifetimeMs
178 /// measures the connection's total age rather than the time since it was last idled.
179 /// @c idleSince is refreshed each time the mapper is stored in the idle set.
180 ///
181 /// An entry whose @c mapper is null is the empty result of @ref TakeIdleLocked, not a pooled entry.
182 struct Entry
183 {
184 std::unique_ptr<DataMapper> mapper;
185 Clock::time_point createdAt {};
186 Clock::time_point idleSince {};
187 };
188
189 public:
190 /// @ingroup ConnectionPool
191 /// A wrapper around a DataMapper that returns it to the pool when destroyed
192 /// can be created only from the Pool and is move-only to ensure it is always
193 /// returned to the pool when it goes out of scope
195 {
196 private:
197 friend class Pool;
198
199 explicit PooledDataMapper(Pool& pool, Entry entry) noexcept:
200 _entry { std::move(entry) },
201 _pool { pool }
202 {
203 }
204
205 public:
206 PooledDataMapper() = delete;
207 PooledDataMapper(PooledDataMapper const&) = delete;
208
209 /// Move constructor for the pooled data mapper, the only public
210 /// constructor, allows moving the pooled data mapper but not copying it
212 _entry { std::move(other._entry) },
213 _pool { other._pool }
214 {
215 }
216 PooledDataMapper& operator=(PooledDataMapper const&) = delete;
217 PooledDataMapper& operator=(PooledDataMapper&&) = delete;
218 ~PooledDataMapper() noexcept
219 {
220 if (_entry.mapper)
221 ReturnToPool();
222 }
223
224 /// Access the underlying data mapper via pointer semantics
225 DataMapper* operator->() const noexcept
226 {
227 return _entry.mapper.get();
228 }
229
230 /// Access the underlying data mapper via reference semantics
231 /// This is useful for passing the pooled data mapper to functions
232 /// that expect a DataMapper reference
233 [[nodiscard]] DataMapper& Get() const noexcept
234 {
235 return *_entry.mapper;
236 }
237
238 private:
239 void ReturnToPool() noexcept
240 {
241 _pool.Return(std::move(_entry));
242 _entry.mapper = nullptr;
243 }
244
245 Entry _entry;
246 Pool& _pool;
247 };
248
249 private:
250 struct WaiterNode; // defined below; referenced by ReturnLocked's signature.
251
252 /// Detaches the async backend from a returned mapper's connection before it is idled or handed
253 /// off, so a recycled connection never carries references to executors that may since have been
254 /// destroyed (the next @c AcquireAsync re-enables it fresh). Shared by every @c Return overload.
255 ///
256 /// @warning The caller must not return a mapper that still has an async operation in flight on it:
257 /// dropping the backend destroys the strand/executors an outstanding offloaded step references and
258 /// races the worker still touching the ODBC handle. Await every async op before returning.
259 /// @param dm The mapper whose connection's async backend is dropped.
260 static void DropAsyncBackend(DataMapper& dm) noexcept
261 {
262 dm.Connection().DisableAsync();
263 }
264
265 /// @return The current time, or a default-constructed time point when this configuration enables
266 /// no time-based bound and therefore never inspects timestamps.
267 [[nodiscard]] Clock::time_point NowIfTracking() const noexcept
268 {
269 if constexpr (!TracksTime)
270 return {};
271 else
272 {
273 if (_clock)
274 return _clock();
275 return Clock::now();
276 }
277 }
278
279 /// Creates a fresh entry, stamped with the current time.
280 /// @return The new entry; its mapper is never null.
281 [[nodiscard]] Entry MakeEntry() const
282 {
283 auto const now = NowIfTracking();
284 auto mapper = std::make_unique<DataMapper>();
285 // The one place a pooled connection comes into existence, so a pool-wide connection setting is
286 // applied here rather than at each call site. Compile-time gated, so a pool left at the default
287 // capacity emits exactly the code it did before the setting existed.
288 if constexpr (Config.preparedStatementCacheCapacity != 0)
289 mapper->Connection().SetPreparedStatementCacheCapacity(Config.preparedStatementCacheCapacity);
290 return Entry { std::move(mapper), now, now };
291 }
292
293 /// Decides whether an idle connection may still be handed to a caller.
294 ///
295 /// Applied only to connections coming out of the idle set. A connection handed straight from
296 /// @ref Return to a parked waiter deliberately bypasses this: see @ref ReturnLocked.
297 ///
298 /// @param entry The idle entry under consideration.
299 /// @param now The current time, as returned by @ref NowIfTracking.
300 /// @return true when the entry is within both configured bounds and, if validation is enabled,
301 /// its connection is still reported alive.
302 [[nodiscard]] bool IsUsable(Entry const& entry, Clock::time_point now) const noexcept
303 {
304 if constexpr (Config.maxLifetimeMs > 0)
305 {
306 if (now - entry.createdAt >= Config.MaxLifetime())
307 return false;
308 }
309 if constexpr (Config.maxIdleTimeMs > 0)
310 {
311 if (now - entry.idleSince >= Config.MaxIdleTime())
312 return false;
313 }
314 if constexpr (Config.validateOnBorrow == ValidateOnBorrow::Yes)
315 {
316 if (!entry.mapper->Connection().IsAlive())
317 return false;
318 }
319 return true;
320 }
321
322 /// Pops the most recently idled usable connection, retiring any expired or dead entries it passes.
323 ///
324 /// Retired entries are moved into @p retired rather than destroyed here, so the ODBC disconnect
325 /// they trigger happens after the caller has released @c _mutex instead of blocking every other
326 /// thread for the duration of a network teardown.
327 ///
328 /// @pre @c _mutex is held by the caller.
329 /// @param retired Collects the retired entries; must outlive the caller's lock.
330 /// @return A usable entry, or an entry with a null mapper when the idle set holds none.
331 [[nodiscard]] Entry TakeIdleLocked(std::vector<Entry>& retired)
332 {
333 auto const now = NowIfTracking();
334 while (!_idleDataMappers.empty())
335 {
336 auto entry = std::move(_idleDataMappers.back());
337 _idleDataMappers.pop_back();
338 if (IsUsable(entry, now))
339 return entry;
340 retired.push_back(std::move(entry));
341 }
342 return {};
343 }
344
345 /// @param entry The entry about to be stored in the idle set.
346 /// @param now The current time, as returned by @ref NowIfTracking.
347 /// @return true when the connection has outlived @ref PoolConfig::maxLifetimeMs and must be
348 /// retired instead of idled. Checking this on return as well as on borrow releases the
349 /// connection as soon as it is no longer wanted, rather than holding it until the next
350 /// acquire. The idle bound is not checked here — the entry is idle for zero time.
351 [[nodiscard]] static bool IsPastLifetime([[maybe_unused]] Entry const& entry,
352 [[maybe_unused]] Clock::time_point now) noexcept
353 {
354 if constexpr (Config.maxLifetimeMs > 0)
355 return now - entry.createdAt >= Config.MaxLifetime();
356 else
357 return false;
358 }
359
360 /// always return the data mapper to the pool for this strategy
361 void Return(Entry entry) noexcept
362 requires(Config.growthStrategy == GrowthStrategy::UnboundedGrow)
363 {
364 DropAsyncBackend(*entry.mapper);
365 auto const now = NowIfTracking();
366 if (IsPastLifetime(entry, now))
367 {
368 // Retired rather than idled: destroyed here, so it counts as a discarded release. The
369 // idle set is untouched, so the occupancy gauge needs no update (and _mutex is not held).
370 LIGHTWEIGHT_STATS_POOL_RELEASE(true);
371 return; // retired here, outside the lock
372 }
373 entry.idleSince = now;
374 SqlLogger::GetLogger().OnConnectionIdle(entry.mapper->Connection());
375 std::scoped_lock lock(_mutex);
376 _idleDataMappers.push_back(std::move(entry));
377 LIGHTWEIGHT_STATS_POOL_RELEASE(false);
378 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
379 }
380
381 /// for bounded wait strategy, return the data mapper to the pool: hand it to the next FIFO waiter
382 /// (sync or async) or idle it.
383 void Return(Entry entry) noexcept
384 requires(Config.growthStrategy == GrowthStrategy::BoundedWait)
385 {
386 DropAsyncBackend(*entry.mapper);
387 Entry retired; // declared before the lock so its disconnect runs after the lock is released
388 std::shared_ptr<WaiterNode> toResume;
389 {
390 std::scoped_lock const lock(_mutex);
391 toResume = ReturnLocked(std::move(entry), retired);
392 }
393 // Resume outside the lock to avoid re-entrancy (the resumed coroutine may call back into the pool).
394 if (toResume)
395 toResume->resume->Resume(toResume->handle);
396 }
397
398 /// Produces a data mapper for a caller without waiting: reuses a usable idle one, otherwise
399 /// creates a fresh one while the pool is below capacity.
400 ///
401 /// @pre @c _mutex is held by the caller.
402 /// @param retired Collects entries retired while scanning the idle set; must outlive the lock.
403 /// @return A ready entry, or an entry with a null mapper when the pool is at capacity and the
404 /// caller must park.
405 [[nodiscard]] Entry AcquireReadyLocked(std::vector<Entry>& retired)
406 requires(Config.growthStrategy == GrowthStrategy::BoundedWait)
407 {
408 if (auto entry = TakeIdleLocked(retired); entry.mapper)
409 {
410 ++_checkedOut;
411 SqlLogger::GetLogger().OnConnectionReuse(entry.mapper->Connection());
412 // Only ever reached before a caller parks, so the wait is zero by construction; the
413 // parking paths record their own sample once they are handed a connection.
414 LIGHTWEIGHT_STATS_POOL_ACQUIRE(std::chrono::microseconds { 0 }, true, false);
415 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
416 return entry;
417 }
418 if (_checkedOut < Config.maxSize)
419 {
420 // below capacity: create a fresh data mapper. Claim the slot only once the connection
421 // actually stands up, so a failing connect does not leak capacity.
422 auto fresh = MakeEntry();
423 ++_checkedOut;
424 LIGHTWEIGHT_STATS_POOL_ACQUIRE(std::chrono::microseconds { 0 }, false, false);
425 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
426 return fresh;
427 }
428 // At capacity: the caller parks and records its own acquire sample.
429 return {};
430 }
431
432 /// Hands @p entry to the next FIFO waiter (transferring the checked-out count) or idles it. Serving
433 /// @c _waiters in arrival order keeps sync @ref Acquire and async @ref AcquireAsync waiters fair.
434 ///
435 /// @pre @c _mutex is held by the caller.
436 /// @param entry The entry to return; its mapper's async backend must already be disabled.
437 /// @param retired Receives the entry when it is retired for having outlived
438 /// @ref PoolConfig::maxLifetimeMs; must outlive the caller's lock.
439 /// @return The async waiter node handed the mapper, to be resumed by the caller after releasing
440 /// @c _mutex; @c nullptr if a sync waiter was woken in place, or the entry was idled or
441 /// retired.
442 std::shared_ptr<WaiterNode> ReturnLocked(Entry entry, Entry& retired) noexcept
443 requires(Config.growthStrategy == GrowthStrategy::BoundedWait)
444 {
445 while (!_waiters.empty())
446 {
447 auto node = _waiters.front();
448 _waiters.pop_front();
449 // _waiters only ever holds parked nodes (an async awaitable de-registers itself on
450 // abandonment, and so does a timed-out Acquire(timeout)), but guard defensively.
451 if (node->state != WaiterNode::State::Parked)
452 continue;
453 // A direct hand-off deliberately skips the health bounds and the liveness check. A waiter
454 // is blocked on a predicate only a hand-off satisfies, so retiring the connection here
455 // would strand it, and manufacturing a replacement means a DataMapper construction that
456 // may throw inside this noexcept path. The connection was in active use moments ago, and
457 // it is still checked the next time it comes out of the idle set.
458 //
459 // Handed directly to a waiter, never idled: a reuse, not an idle transition.
460 SqlLogger::GetLogger().OnConnectionReuse(entry.mapper->Connection());
461 // The hand-off is both a release by the returner and a (reusing) acquire by the waiter.
462 LIGHTWEIGHT_STATS_POOL_RELEASE(false);
463 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
464 node->state = WaiterNode::State::Fulfilled;
465 node->entry = std::move(entry); // hand off ownership; _checkedOut stays (transferred)
466 if (node->kind == WaiterNode::Kind::Async)
467 return node; // resumed by the caller outside the lock
468 node->cv.notify_one(); // wake the blocked Acquire(); it consumes node->entry
469 return nullptr;
470 }
471 // No waiter: the connection goes idle, so the lifetime bound applies. Releasing the slot
472 // matters either way — a retired connection frees capacity just as an idled one does.
473 --_checkedOut;
474 auto const now = NowIfTracking();
475 if (IsPastLifetime(entry, now))
476 {
477 retired = std::move(entry); // destroyed by the caller, after _mutex is released
478 // Retired rather than idled: the connection is destroyed, so it counts as a discard.
479 // Without this an idle count reconstructed from these events alone would drift.
480 LIGHTWEIGHT_STATS_POOL_RELEASE(true);
481 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
482 return nullptr;
483 }
484 entry.idleSince = now;
485 SqlLogger::GetLogger().OnConnectionIdle(entry.mapper->Connection());
486 _idleDataMappers.push_back(std::move(entry));
487 LIGHTWEIGHT_STATS_POOL_RELEASE(false);
488 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
489 return nullptr;
490 }
491
492 /// for bounded overflow strategy, only return to pool if we have capacity, otherwise just destroy the data mapper
493 void Return(Entry entry) noexcept
494 requires(Config.growthStrategy == GrowthStrategy::BoundedOverflow)
495 {
496 DropAsyncBackend(*entry.mapper);
497 auto const now = NowIfTracking();
498 if (IsPastLifetime(entry, now))
499 {
500 // Retired rather than idled: destroyed here, so it counts as a discarded release. The
501 // idle set is untouched, so the occupancy gauge needs no update (and _mutex is not held).
502 LIGHTWEIGHT_STATS_POOL_RELEASE(true);
503 return; // retired here, outside the lock
504 }
505 entry.idleSince = now;
506 std::scoped_lock lock(_mutex);
507 if (_idleDataMappers.size() < Config.maxSize)
508 {
509 SqlLogger::GetLogger().OnConnectionIdle(entry.mapper->Connection());
510 _idleDataMappers.push_back(std::move(entry));
511 LIGHTWEIGHT_STATS_POOL_RELEASE(false);
512 }
513 else
514 {
515 // Over capacity: the mapper is destroyed here rather than idled. Without this counter an
516 // idle-count reconstructed from idle/reuse events alone would silently drift.
517 LIGHTWEIGHT_STATS_POOL_RELEASE(true);
518 }
519 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
520 }
521
522 public:
523 /// Default constructor that pre-creates the initial number of data mappers and stores them in the pool
524 /// No other constructors are provided, as the pool is configured at compile time via the template parameter
525 explicit Pool()
526 {
527 _idleDataMappers.reserve(Config.initialSize);
528 for ([[maybe_unused]] auto const _: std::views::iota(0U, Config.initialSize))
529 _idleDataMappers.push_back(MakeEntry());
530 }
531
532 /// Destructor. The pool manages the lifecycle of the idle data mappers; be aware that any
533 /// acquired data mappers not returned to the pool are destroyed when the pool is destroyed,
534 /// which may leak resources if not handled properly.
535 ~Pool() noexcept
536 {
537 // A parked AcquireAsync coroutine (or a thread blocked in Acquire) holds a reference back to this
538 // pool, so destroying the pool out from under it is undefined: drive every AcquireAsync task to
539 // completion and let every blocked Acquire() return first. The assert catches this in debug; the
540 // warning surfaces it in release (where the later access would be a use-after-free).
541 if (!_waiters.empty())
543 "Pool destroyed while acquirers are still waiting on it (coroutines parked in AcquireAsync "
544 "and/or threads blocked in Acquire); the pool must outlive every acquirer (drive each "
545 "AcquireAsync task to completion or destroy it first, and never destroy the pool while a "
546 "thread is blocked in Acquire). This is undefined behavior.");
547 assert(_waiters.empty() && "Pool destroyed while acquirers are still waiting on it");
548 }
549
550 Pool(Pool const&) = delete;
551 Pool& operator=(Pool const&) = delete;
552 Pool(Pool&&) = delete;
553 Pool& operator=(Pool&&) = delete;
554
555 /// Function to acquire a data mapper from the pool, the behavior of this function depends on the growth strategy
556 /// this is a specific implementation for the BoundedWait strategy, which blocks until a data mapper is available if the
557 /// pool is at maximum capacity
558 ///
559 /// Prefer the @ref Acquire(std::chrono::milliseconds) overload in production code: this one waits
560 /// indefinitely, so an exhausted pool parks the calling thread with no diagnostic.
562 requires(Config.growthStrategy == GrowthStrategy::BoundedWait)
563 {
564 std::vector<Entry> retired; // declared before the lock: disconnects run after it is released
565 [[maybe_unused]] auto const acquireStartedAt = std::chrono::steady_clock::now();
566 std::unique_lock lock(_mutex);
567 if (auto entry = AcquireReadyLocked(retired); entry.mapper)
568 return PooledDataMapper(*this, std::move(entry));
569
570 // Pool exhausted: park as a FIFO waiter (fair with AcquireAsync waiters) and block until a
571 // mapper is handed to this node. The hand-off transfers a checked-out slot, so no ++_checkedOut.
572 auto node = std::make_shared<WaiterNode>(WaiterNode::Kind::Sync);
573 _waiters.push_back(node);
574 node->cv.wait(lock, [&node] { return node->state == WaiterNode::State::Fulfilled; });
575 // Only this branch actually blocked, so only this branch contributes a wait-latency sample.
576 LIGHTWEIGHT_STATS_POOL_ACQUIRE(
577 std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - acquireStartedAt),
578 true,
579 true);
580 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
581 return PooledDataMapper(*this, std::move(node->entry));
582 }
583
584 /// Acquires a data mapper, giving up if none becomes available within @p timeout.
585 ///
586 /// Bounds how long an exhausted BoundedWait pool may park the calling thread, so a stuck or
587 /// overloaded pool surfaces as an error the caller can act on rather than as an indefinite hang.
588 ///
589 /// @param timeout How long to wait for a data mapper to be returned. A non-positive value makes
590 /// this a pure try-acquire.
591 /// @return The acquired data mapper, or @ref PoolError::Timeout if @p timeout elapsed first.
592 [[nodiscard]] std::expected<PooledDataMapper, PoolError> Acquire(std::chrono::milliseconds timeout)
593 requires(Config.growthStrategy == GrowthStrategy::BoundedWait)
594 {
595 std::vector<Entry> retired; // declared before the lock: disconnects run after it is released
596 [[maybe_unused]] auto const acquireStartedAt = std::chrono::steady_clock::now();
597 std::unique_lock lock(_mutex);
598 if (auto entry = AcquireReadyLocked(retired); entry.mapper)
599 return PooledDataMapper(*this, std::move(entry));
600
601 auto node = std::make_shared<WaiterNode>(WaiterNode::Kind::Sync);
602 _waiters.push_back(node);
603 if (!node->cv.wait_for(lock, timeout, [&node] { return node->state == WaiterNode::State::Fulfilled; }))
604 {
605 // wait_for evaluates the predicate under the lock and reports its final value, so a false
606 // result proves this node is still Parked and no hand-off can be in flight. De-registering
607 // it here is therefore race-free: a later Return will never see it.
608 node->state = WaiterNode::State::Abandoned;
609 std::erase(_waiters, node);
610 // A timed-out acquire yields no connection, so it contributes no acquire sample.
611 return std::unexpected { PoolError::Timeout };
612 }
613 // Reached only after actually blocking, so this contributes a wait-latency sample.
614 LIGHTWEIGHT_STATS_POOL_ACQUIRE(
615 std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - acquireStartedAt),
616 true,
617 true);
618 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
619 return PooledDataMapper(*this, std::move(node->entry));
620 }
621
622 /// Function to acquire a data mapper from the pool, the behavior of this function depends on the growth strategy
623 /// this is a specific implementation for the strategies that do not block, which always creates a new data mapper if
624 /// the pool is empty, regardless of the maximum capacity
626 requires(Config.growthStrategy != GrowthStrategy::BoundedWait)
627 {
628 std::vector<Entry> retired; // declared before the lock: disconnects run after it is released
629 std::scoped_lock lock(_mutex);
630 auto entry = TakeIdleLocked(retired);
631 if (!entry.mapper)
632 {
633 // no usable idle data mapper: create a new one and return it
634 LIGHTWEIGHT_STATS_POOL_ACQUIRE(std::chrono::microseconds { 0 }, false, false);
635 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
636 return PooledDataMapper(*this, MakeEntry());
637 }
638 SqlLogger::GetLogger().OnConnectionReuse(entry.mapper->Connection());
639 LIGHTWEIGHT_STATS_POOL_ACQUIRE(std::chrono::microseconds { 0 }, true, false);
640 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(_idleDataMappers.size(), _checkedOut);
641 return PooledDataMapper(*this, std::move(entry));
642 }
643
644 /// Acquires a data mapper, for the strategies that never wait.
645 ///
646 /// Provided so call sites can be written without knowing the strategy. These strategies create a
647 /// fresh data mapper whenever no idle one is available, so the timeout can never elapse and the
648 /// result always holds a value.
649 ///
650 /// @param timeout Ignored; see above.
651 /// @return The acquired data mapper.
652 [[nodiscard]] std::expected<PooledDataMapper, PoolError> Acquire([[maybe_unused]] std::chrono::milliseconds timeout)
653 requires(Config.growthStrategy != GrowthStrategy::BoundedWait)
654 {
655 return Acquire();
656 }
657
658 /// Asynchronously acquires a DataMapper from the pool without blocking the calling thread.
659 ///
660 /// If the pool is exhausted (BoundedWait at capacity), the awaiting coroutine is suspended and
661 /// resumed — via @p resume — when a mapper is returned, rather than parking a thread. The acquired
662 /// mapper's connection is wired for async via SqlConnection::EnableAsync(@p dbWorkers, @p resume),
663 /// so the caller can immediately co_await its async methods.
664 ///
665 /// @param dbWorkers The worker pool used to run the acquired mapper's blocking ODBC calls.
666 /// @param resume The scheduler used to resume coroutines (typically the app run loop).
667 /// @return A Task yielding a pooled DataMapper.
669 {
670 // Forward to a coroutine taking pointers (coroutines must not take reference parameters).
671 return AcquireAsyncImpl(&dbWorkers, &resume);
672 }
673
674 /// Configures the executors that the no-argument @ref AcquireAsync() overload wires acquired
675 /// mappers for, so async consumers of this pool no longer repeat the executors at every call.
676 ///
677 /// This is opt-in and scoped to the pool: only pools configured this way hand out async-enabled
678 /// mappers via the no-arg overload; synchronous @ref Acquire and connections outside the pool are
679 /// unaffected. Unlike a process-global default, the executors' lifetime is tied to this pool,
680 /// which already must outlive every acquirer.
681 ///
682 /// @warning @p dbWorkers and @p resume must outlive this pool's async use (the same contract the
683 /// explicit-argument @ref AcquireAsync overload already implies). Only references are
684 /// retained. Intended to be called once during setup, before any concurrent
685 /// @ref AcquireAsync(); it is not synchronized against in-flight acquirers.
686 /// @param dbWorkers The worker pool used to run acquired mappers' blocking ODBC calls.
687 /// @param resume The scheduler used to resume coroutines (typically the app run loop).
689 {
690 _asyncDbWorkers = &dbWorkers;
691 _asyncResume = &resume;
692 }
693
694 /// Asynchronously acquires a DataMapper using the executors previously set via
695 /// @ref SetAsyncExecutors, without blocking the calling thread.
696 ///
697 /// Equivalent to the explicit-argument @ref AcquireAsync overload with the pool's stored
698 /// executors; pass the executors to that overload to override them for a single call.
699 ///
700 /// @return A Task yielding a pooled DataMapper.
701 /// @throws std::logic_error if @ref SetAsyncExecutors has not been called on this pool.
703 {
704 if (!_asyncDbWorkers || !_asyncResume)
705 throw std::logic_error {
706 "Pool::AcquireAsync(): no async executors configured; call Pool::SetAsyncExecutors(...) first "
707 "or use the explicit AcquireAsync(dbWorkers, resume) overload."
708 };
709 return AcquireAsyncImpl(_asyncDbWorkers, _asyncResume);
710 }
711
712 /// Overrides the clock the idle-time and lifetime bounds are measured against, so eviction can be
713 /// driven deterministically (from a test, or from a simulated clock) instead of by sleeping.
714 ///
715 /// @warning Like @ref SetAsyncExecutors, this is setup, not a runtime knob: it is not
716 /// synchronized against in-flight acquirers, so call it before any concurrent use of the
717 /// pool. Advancing whatever time @p clock reports is the caller's business afterwards.
718 /// @param clock Source of the current time; pass @c {} to restore the real clock.
719 void SetClock(std::function<Clock::time_point()> clock) noexcept
720 {
721 _clock = std::move(clock);
722 }
723
724#if defined(BUILD_TESTS)
725 [[nodiscard]] size_t IdleCount() noexcept
726 {
727 std::scoped_lock lock(_mutex);
728 return _idleDataMappers.size();
729 }
730
731 /// @return the number of parked acquirers (blocked Acquire() threads + suspended AcquireAsync
732 /// coroutines); lets tests observe parking/fairness deterministically.
733 [[nodiscard]] size_t WaiterCount() noexcept
734 {
735 std::scoped_lock lock(_mutex);
736 return _waiters.size();
737 }
738#endif
739
740 private:
741 /// A parked acquirer awaiting a DataMapper — a suspended @ref AcquireAsync coroutine (@c Kind::Async)
742 /// or a blocked synchronous @ref Acquire thread (@c Kind::Sync). Both share one FIFO queue
743 /// (@c _waiters), served in arrival order so neither kind starves the other.
744 ///
745 /// Heap-allocated and shared with the pool. Holding the handed-off mapper and @c state in this node
746 /// (not via a pointer into a coroutine frame) lets @ref Return and the awaitable's destructor
747 /// coordinate purely through node state under @c _mutex, never touching a possibly-destroyed frame.
748 struct WaiterNode
749 {
750 /// Whether this waiter is a suspended coroutine or a blocked synchronous Acquire() thread.
751 enum class Kind : std::uint8_t
752 {
753 Sync, ///< A blocked @ref Acquire thread; woken via @c cv.
754 Async, ///< A suspended @ref AcquireAsync coroutine; resumed via @c resume / @c handle.
755 };
756
757 /// Liveness of the waiter, transitioned only under @c Pool::_mutex.
758 enum class State : std::uint8_t
759 {
760 Parked, ///< Registered in @c _waiters, awaiting a mapper.
761 Fulfilled, ///< Return handed it a mapper (in @c mapper) and woke/scheduled it.
762 Abandoned, ///< The awaiting async task was destroyed (or its entry consumed); inert.
763 };
764
765 Kind kind;
766 State state = State::Parked;
767 Entry entry {}; ///< Filled by Return on hand-off; lives outside any frame.
768
769 // Async waiter only:
770 std::coroutine_handle<> handle {};
771 Async::IResumeScheduler* resume = nullptr;
772
773 // Sync waiter only: the blocked Acquire() waits on this CV (under Pool::_mutex). One waiter per
774 // CV, so Return's notify_one wakes exactly the served thread.
775 std::condition_variable cv {};
776
777 explicit WaiterNode(Kind nodeKind) noexcept:
778 kind { nodeKind }
779 {
780 }
781 };
782
783 /// Awaitable that acquires a DataMapper, suspending only when the pool is at capacity.
784 ///
785 /// Non-copyable/non-movable: constructed in place in the co_await expression. On suspension it
786 /// registers a shared @ref WaiterNode (@c Kind::Async) in pool._waiters; the node carries the
787 /// handed-off mapper and liveness state so Return() and this destructor coordinate safely.
788 struct AsyncAcquireAwaitable
789 {
790 Pool& pool;
791 Async::IResumeScheduler& resume;
792 Entry acquired {}; ///< Entry obtained without suspending (idle/fresh).
793 std::shared_ptr<WaiterNode> node {}; ///< Set only while parked; shared with the pool.
794 std::vector<Entry> retired {}; ///< Entries retired while scanning the idle set.
795 /// When this acquisition parked, used to attribute its wait latency. Only read when @c node
796 /// is set, so it needs no value on the non-parking paths.
797 std::chrono::steady_clock::time_point parkedAt {};
798
799 AsyncAcquireAwaitable(Pool& poolRef, Async::IResumeScheduler& resumeRef) noexcept:
800 pool { poolRef },
801 resume { resumeRef }
802 {
803 }
804
805 AsyncAcquireAwaitable(AsyncAcquireAwaitable const&) = delete;
806 AsyncAcquireAwaitable& operator=(AsyncAcquireAwaitable const&) = delete;
807 AsyncAcquireAwaitable(AsyncAcquireAwaitable&&) = delete;
808 AsyncAcquireAwaitable& operator=(AsyncAcquireAwaitable&&) = delete;
809
810 /// Cleans up if the awaiting coroutine is destroyed before it consumes its mapper.
811 ///
812 /// Under pool._mutex: if still parked, de-registers the node so a later Return() never hands
813 /// off to a dead frame. If Return() already handed off a mapper (Fulfilled) that await_resume
814 /// never consumed, reclaims it into the pool so the BoundedWait checked-out count is not
815 /// leaked (possibly handing it straight to the next waiter, resumed after the lock is released).
816 ///
817 /// @warning A task that has already been handed a mapper must still be driven to completion:
818 /// the resumption Return() scheduled cannot be cancelled, so a coroutine frame with a pending
819 /// resumption must not be freed (do not destroy such a task concurrently with, or right after,
820 /// the hand-off). Likewise the pool must outlive every task acquired from it.
821 ~AsyncAcquireAwaitable()
822 {
823 if (!node)
824 return;
825 Entry reclaimed; // declared before the lock so its disconnect runs after the lock is released
826 std::shared_ptr<WaiterNode> toResume;
827 {
828 std::scoped_lock const lock(pool._mutex);
829 switch (node->state)
830 {
831 case WaiterNode::State::Parked:
832 // Never fulfilled: remove ourselves so Return() won't hand off to a dead frame.
833 // Parking never incremented _checkedOut, so there is nothing to release.
834 node->state = WaiterNode::State::Abandoned;
835 std::erase(pool._waiters, node);
836 break;
837 case WaiterNode::State::Fulfilled:
838 // Handed a mapper but the task is dropped before consuming it: reclaim it,
839 // releasing this acquisition's checked-out count so the pool does not leak.
840 node->state = WaiterNode::State::Abandoned;
841 if constexpr (Config.growthStrategy == GrowthStrategy::BoundedWait)
842 {
843 if (node->entry.mapper)
844 toResume = pool.ReturnLocked(std::move(node->entry), reclaimed);
845 }
846 break;
847 case WaiterNode::State::Abandoned:
848 break;
849 }
850 }
851 if (toResume)
852 toResume->resume->Resume(toResume->handle);
853 }
854
855 [[nodiscard]] bool await_ready() const noexcept
856 {
857 return false;
858 }
859
860 bool await_suspend(std::coroutine_handle<> handle)
861 {
862 std::scoped_lock const lock(pool._mutex);
863 // Retired entries land in `retired`, a member of this awaitable, so the disconnects they
864 // trigger happen when the awaitable dies rather than under pool._mutex.
865 acquired = pool.TakeIdleLocked(retired);
866 if (acquired.mapper)
867 {
868 if constexpr (Config.growthStrategy == GrowthStrategy::BoundedWait)
869 ++pool._checkedOut;
870 SqlLogger::GetLogger().OnConnectionReuse(acquired.mapper->Connection());
871 LIGHTWEIGHT_STATS_POOL_ACQUIRE(std::chrono::microseconds { 0 }, true, false);
872 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(pool._idleDataMappers.size(), pool._checkedOut);
873 return false; // do not suspend — resume immediately
874 }
875 // Only BoundedWait bounds the pool and parks coroutines on exhaustion. The non-blocking
876 // strategies (BoundedOverflow — the default — and UnboundedGrow) always create a fresh
877 // mapper here, matching the synchronous Acquire() overloads, which also never suspend.
878 if constexpr (Config.growthStrategy == GrowthStrategy::BoundedWait)
879 {
880 if (pool._checkedOut >= Config.maxSize)
881 {
882 node = std::make_shared<WaiterNode>(WaiterNode::Kind::Async);
883 node->handle = handle;
884 node->resume = &resume;
885 pool._waiters.push_back(node);
886 parkedAt = std::chrono::steady_clock::now();
887 return true; // suspend until a mapper is returned
888 }
889 }
890 // Below capacity: create a fresh data mapper. As in AcquireReadyLocked, claim the slot
891 // only once the connection actually stands up — MakeEntry() connects and may throw, and a
892 // slot claimed before that would never be released, permanently shrinking a BoundedWait
893 // pool's capacity.
894 auto fresh = pool.MakeEntry();
895 if constexpr (Config.growthStrategy == GrowthStrategy::BoundedWait)
896 ++pool._checkedOut;
897 acquired = std::move(fresh);
898 LIGHTWEIGHT_STATS_POOL_ACQUIRE(std::chrono::microseconds { 0 }, false, false);
899 LIGHTWEIGHT_STATS_POOL_OCCUPANCY(pool._idleDataMappers.size(), pool._checkedOut);
900 return false;
901 }
902
903 Entry await_resume() noexcept
904 {
905 // If we suspended, Return() placed the entry in the shared node; take it here (on the
906 // resuming thread, with no concurrent access per the destruction contract). That leaves
907 // node->entry empty, so the destructor treats the node as already consumed.
908 if (node)
909 {
910 // This acquisition actually parked the coroutine, so it contributes a wait sample.
911 LIGHTWEIGHT_STATS_POOL_ACQUIRE(
912 std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - parkedAt),
913 true,
914 true);
915 return std::move(node->entry);
916 }
917 return std::move(acquired);
918 }
919 };
920
921 Async::Task<PooledDataMapper> AcquireAsyncImpl(Async::IExecutor* dbWorkers, Async::IResumeScheduler* resume)
922 {
923 auto entry = co_await AsyncAcquireAwaitable { *this, *resume };
924 // Wrap in the RAII PooledDataMapper BEFORE the throwing EnableAsync call: if EnableAsync
925 // throws (e.g. bad_alloc), ~PooledDataMapper returns the mapper to the pool, decrementing
926 // _checkedOut and avoiding a permanent BoundedWait capacity leak.
927 auto pooled = PooledDataMapper(*this, std::move(entry));
928 pooled->Connection().EnableAsync(*dbWorkers, *resume);
929 co_return std::move(pooled);
930 }
931
932 std::mutex _mutex;
933 std::vector<Entry> _idleDataMappers;
934 size_t _checkedOut {};
935 /// Injected clock; the real @c Clock::now is used when unset. @see SetClock
936 ///
937 /// Deliberately not compiled out in non-test builds: this is a data member of a class template
938 /// instantiated both inside the library (@ref GlobalDataMapperPool) and in consumer translation
939 /// units, so making its presence depend on a translation-unit-local macro would give the same
940 /// specialization two different layouts and two different definitions.
941 std::function<Clock::time_point()> _clock {};
942 /// Executors used by the no-argument @ref AcquireAsync() overload; set via @ref SetAsyncExecutors.
943 /// Null until configured. Only references are held; they must outlive the pool's async use.
944 Async::IExecutor* _asyncDbWorkers = nullptr;
945 Async::IResumeScheduler* _asyncResume = nullptr;
946 /// FIFO of parked acquirers (sync @ref Acquire threads and async @ref AcquireAsync coroutines) in
947 /// arrival order. Each sync waiter owns its CV inside its @ref WaiterNode, so no shared CV is needed.
948 std::deque<std::shared_ptr<WaiterNode>> _waiters;
949};
950
951// Default pool configuration, configurable via CMake options:
952// LIGHTWEIGHT_POOL_INITIAL_SIZE (default: 4)
953// LIGHTWEIGHT_POOL_MAX_SIZE (default: 16)
954// LIGHTWEIGHT_POOL_GROWTH_STRATEGY (default: BoundedOverflow)
955// Accepted values: BoundedWait, BoundedOverflow, UnboundedGrow
956// LIGHTWEIGHT_POOL_VALIDATE_ON_BORROW (default: Yes)
957// Accepted values: Yes, No
958// LIGHTWEIGHT_POOL_MAX_IDLE_TIME_MS (default: 0, meaning no bound)
959// LIGHTWEIGHT_POOL_MAX_LIFETIME_MS (default: 0, meaning no bound)
960// LIGHTWEIGHT_POOL_PREPARED_STATEMENT_CACHE_CAPACITY (default: 0, i.e. disabled)
961
962#if !defined(LIGHTWEIGHT_POOL_INITIAL_SIZE)
963 #define LIGHTWEIGHT_POOL_INITIAL_SIZE 4
964#endif
965
966#if !defined(LIGHTWEIGHT_POOL_MAX_SIZE)
967 #define LIGHTWEIGHT_POOL_MAX_SIZE 16
968#endif
969
970#if !defined(LIGHTWEIGHT_POOL_GROWTH_STRATEGY)
971 #define LIGHTWEIGHT_POOL_GROWTH_STRATEGY BoundedOverflow
972#endif
973
974#if !defined(LIGHTWEIGHT_POOL_VALIDATE_ON_BORROW)
975 #define LIGHTWEIGHT_POOL_VALIDATE_ON_BORROW Yes
976#endif
977
978// The lifetime bounds default to 0 (disabled): a default recycle window would silently change the
979// behaviour of every existing deployment, and the right value depends on the infrastructure the
980// connections traverse. See PoolConfig for how to choose one.
981#if !defined(LIGHTWEIGHT_POOL_MAX_IDLE_TIME_MS)
982 #define LIGHTWEIGHT_POOL_MAX_IDLE_TIME_MS 0
983#endif
984
985#if !defined(LIGHTWEIGHT_POOL_MAX_LIFETIME_MS)
986 #define LIGHTWEIGHT_POOL_MAX_LIFETIME_MS 0
987#endif
988
989#if !defined(LIGHTWEIGHT_POOL_PREPARED_STATEMENT_CACHE_CAPACITY)
990 #define LIGHTWEIGHT_POOL_PREPARED_STATEMENT_CACHE_CAPACITY 0
991#endif
992
993inline constexpr PoolConfig DefaultPoolConfig {
994 .initialSize = LIGHTWEIGHT_POOL_INITIAL_SIZE,
995 .maxSize = LIGHTWEIGHT_POOL_MAX_SIZE,
996 .growthStrategy = GrowthStrategy::LIGHTWEIGHT_POOL_GROWTH_STRATEGY,
997 .validateOnBorrow = ValidateOnBorrow::LIGHTWEIGHT_POOL_VALIDATE_ON_BORROW,
998 .maxIdleTimeMs = LIGHTWEIGHT_POOL_MAX_IDLE_TIME_MS,
999 .maxLifetimeMs = LIGHTWEIGHT_POOL_MAX_LIFETIME_MS,
1000 .preparedStatementCacheCapacity = LIGHTWEIGHT_POOL_PREPARED_STATEMENT_CACHE_CAPACITY,
1001};
1002
1003using DataMapperPool = Pool<DefaultPoolConfig>;
1004
1005/// Returns the process-wide global DataMapper pool.
1006///
1007/// The pool is configured at compile time via the LIGHTWEIGHT_POOL_* defines.
1008/// Because the singleton lives inside the Lightweight library, it is shared
1009/// correctly across shared-library boundaries.
1010LIGHTWEIGHT_API DataMapperPool& GlobalDataMapperPool();
1011
1012} // namespace Lightweight
Main API for mapping records to and from the database using high level C++ syntax.
DataMapper * operator->() const noexcept
Access the underlying data mapper via pointer semantics.
Definition Pool.hpp:225
PooledDataMapper(PooledDataMapper &&other) noexcept
Definition Pool.hpp:211
DataMapper & Get() const noexcept
Definition Pool.hpp:233
~Pool() noexcept
Definition Pool.hpp:535
std::expected< PooledDataMapper, PoolError > Acquire(std::chrono::milliseconds timeout)
Definition Pool.hpp:592
std::expected< PooledDataMapper, PoolError > Acquire(std::chrono::milliseconds timeout)
Definition Pool.hpp:652
PooledDataMapper Acquire()
Definition Pool.hpp:625
Async::Task< PooledDataMapper > AcquireAsync(Async::IExecutor &dbWorkers, Async::IResumeScheduler &resume)
Definition Pool.hpp:668
void SetClock(std::function< Clock::time_point()> clock) noexcept
Definition Pool.hpp:719
PooledDataMapper Acquire()
Definition Pool.hpp:561
void SetAsyncExecutors(Async::IExecutor &dbWorkers, Async::IResumeScheduler &resume) noexcept
Definition Pool.hpp:688
Async::Task< PooledDataMapper > AcquireAsync()
Definition Pool.hpp:702
static LIGHTWEIGHT_API SqlLogger & GetLogger()
Retrieves the currently configured logger.
virtual void OnConnectionReuse(SqlConnection const &connection)=0
Invoked when a connection is reused.
virtual void OnConnectionIdle(SqlConnection const &connection)=0
Invoked when a connection is idle.
virtual void OnWarning(std::string_view const &message)=0
Invoked on a warning.
ValidateOnBorrow
Definition Pool.hpp:58
@ Timeout
The timeout elapsed before a data mapper became available.
constexpr std::chrono::milliseconds MaxIdleTime() const noexcept
Definition Pool.hpp:147
constexpr std::chrono::milliseconds MaxLifetime() const noexcept
Definition Pool.hpp:153
ValidateOnBorrow validateOnBorrow
Definition Pool.hpp:106
GrowthStrategy growthStrategy
Definition Pool.hpp:101
size_t initialSize
Initial number of data mappers to pre-create and store in the pool, must be less than or equal to max...
Definition Pool.hpp:94
size_t preparedStatementCacheCapacity
Definition Pool.hpp:144
std::chrono::milliseconds::rep maxLifetimeMs
Definition Pool.hpp:132
std::chrono::milliseconds::rep maxIdleTimeMs
Definition Pool.hpp:120