Lightweight 0.20260303.0
Loading...
Searching...
No Matches
Pool.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include "DataMapper.hpp"
5
6#include <condition_variable>
7#include <memory>
8#include <mutex>
9#include <vector>
10
11namespace Lightweight
12{
13
14/// Enum to define growth strategies of the pool
15///
16enum class GrowthStrategy : uint8_t
17{
18 /// Pre-create initialSize objects. Allow the total count
19 /// to grow up to maxSize. Once maxSize objects exist, callers BLOCK
20 /// until one is returned.
21 BoundedWait,
22
23 /// Pre-create initialSize objects. The pool stores up to maxSize
24 /// objects. If none are idle, a fresh object is ALWAYS created (no
25 /// waiting). On return: kept if the idle set is below maxSize, otherwise
26 /// destroyed.
27 BoundedOverflow,
28
29 /// Pre-create initialSize objects. Grow without limit. Every returned
30 /// object is always kept in the pool.
31 UnboundedGrow,
32};
33
34/// Structure to hold the configuration of the pool, including the initial size, maximum size and growth strategy.
35/// Structure is used as a template parameter for the Pool class to configure its behavior at compile time.
37{
38 /// Initial number of data mappers to pre-create and store in the pool, must be less than or equal to maxSize
39 size_t initialSize {};
40 /// Maximum number of data mappers that can exist in the pool, must be greater than or equal to initialSize
41 /// this is used for the Bounded* strategies to determine when to block or when to stop accepting returned data mappers,
42 /// for the UnboundedGrow strategy this is ignored
43 size_t maxSize {};
44 /// Strategy to determine how the pool should grow when there are no idle data mappers available, default is BoundedWait
45 /// which blocks until a data mapper is returned to the pool
46 GrowthStrategy growthStrategy { GrowthStrategy::BoundedWait };
47};
48
49/// A thread-safe pool of DataMapper instances with the policy configured by the PoolConfig template parameter.
50/// The pool allows acquiring and returning DataMapper instances, and manages the lifecycle of these instances according to
51/// the specified growth strategy.
52template <PoolConfig Config>
53class Pool
54{
55 public:
56 /// A wrapper around a DataMapper that returns it to the pool when destroyed
57 /// can be created only from the Pool and is move-only to ensure it is always
58 /// returned to the pool when it goes out of scope
60 {
61 private:
62 friend class Pool;
63
64 explicit PooledDataMapper(Pool& pool, std::unique_ptr<DataMapper> dm) noexcept:
65 _dm { std::move(dm) },
66 _pool { pool }
67 {
68 }
69
70 public:
71 PooledDataMapper() = delete;
72 PooledDataMapper(PooledDataMapper const&) = delete;
73
74 /// Move constructor for the pooled data mapper, the only public
75 /// constructor, allows moving the pooled data mapper but not copying it
77 _dm { std::move(other._dm) },
78 _pool { other._pool }
79 {
80 }
81 PooledDataMapper& operator=(PooledDataMapper const&) = delete;
82 PooledDataMapper& operator=(PooledDataMapper&&) = delete;
83 ~PooledDataMapper() noexcept
84 {
85 if (_dm)
86 ReturnToPool();
87 }
88
89 /// Access the underlying data mapper via pointer semantics
90 DataMapper* operator->() const noexcept
91 {
92 return _dm.get();
93 }
94
95 /// Access the underlying data mapper via reference semantics
96 /// This is useful for passing the pooled data mapper to functions
97 /// that expect a DataMapper reference
98 [[nodiscard]] DataMapper& Get() const noexcept
99 {
100 return *_dm;
101 }
102
103 private:
104 void ReturnToPool() noexcept
105 {
106 _pool.Return(std::move(_dm));
107 _dm = nullptr;
108 }
109
110 std::unique_ptr<DataMapper> _dm;
111 Pool& _pool;
112 };
113
114 private:
115 /// always return the data mapper to the pool for this strategy
116 void Return(std::unique_ptr<DataMapper> dm) noexcept
117 requires(Config.growthStrategy == GrowthStrategy::UnboundedGrow)
118 {
119 std::scoped_lock lock(_mutex);
120 _idleDataMappers.push_back(std::move(dm));
121 }
122
123 /// for bounded wait strategy, return the data mapper to the pool and notify one waiting thread if any
124 void Return(std::unique_ptr<DataMapper> dm) noexcept
125 requires(Config.growthStrategy == GrowthStrategy::BoundedWait)
126 {
127 {
128 std::scoped_lock lock(_mutex);
129 _idleDataMappers.push_back(std::move(dm));
130 --_checkedOut;
131 }
132 _cv.notify_one();
133 }
134
135 /// for bounded overflow strategy, only return to pool if we have capacity, otherwise just destroy the data mapper
136 void Return(std::unique_ptr<DataMapper> dm) noexcept
137 requires(Config.growthStrategy == GrowthStrategy::BoundedOverflow)
138 {
139 std::scoped_lock lock(_mutex);
140 if (_idleDataMappers.size() < Config.maxSize)
141 _idleDataMappers.push_back(std::move(dm));
142 }
143
144 public:
145 /// Default constructor that pre-creates the initial number of data mappers and stores them in the pool
146 /// No other constructors are provided, as the pool is configured at compile time via the template parameter
147 explicit Pool()
148 {
149 _idleDataMappers.reserve(Config.initialSize);
150 for ([[maybe_unused]] auto const _: std::views::iota(0U, Config.initialSize))
151 _idleDataMappers.push_back(std::make_unique<DataMapper>());
152 }
153
154 /// Default destructor, the pool manages the lifecycle of the data mappers, so no special cleanup is needed
155 /// bug be aware that any acquired data mappers that are not returned to the pool will be destroyed when the pool is
156 /// destroyed, which may lead to resource leaks if not handled properly
157 ~Pool() noexcept = default;
158
159 Pool(Pool const&) = delete;
160 Pool& operator=(Pool const&) = delete;
161 Pool(Pool&&) = delete;
162 Pool& operator=(Pool&&) = delete;
163
164 /// Function to acquire a data mapper from the pool, the behavior of this function depends on the growth strategy
165 /// this is a specific implementation for the BoundedWait strategy, which blocks until a data mapper is available if the
166 /// pool is at maximum capacity
168 requires(Config.growthStrategy == GrowthStrategy::BoundedWait)
169 {
170 std::unique_lock lock(_mutex);
171 if (_idleDataMappers.empty())
172 {
173 if (_checkedOut >= Config.maxSize)
174 {
175 // wait until a data mapper is returned to the pool
176 _cv.wait(lock, [this] { return !_idleDataMappers.empty(); });
177 }
178 else
179 {
180 // create a new data mapper and return it
181 ++_checkedOut;
182 return PooledDataMapper(*this, std::make_unique<DataMapper>());
183 }
184 }
185
186 // get a data mapper from the pool
187 auto dm = std::move(_idleDataMappers.back());
188 _idleDataMappers.pop_back();
189 ++_checkedOut;
190 return PooledDataMapper(*this, std::move(dm));
191 }
192
193 /// Function to acquire a data mapper from the pool, the behavior of this function depends on the growth strategy
194 /// this is a specific implementation for the strategies that do not block, which always creates a new data mapper if
195 /// the pool is empty, regardless of the maximum capacity
197 requires(Config.growthStrategy != GrowthStrategy::BoundedWait)
198 {
199 std::scoped_lock lock(_mutex);
200 if (_idleDataMappers.empty())
201 {
202 // create a new data mapper and return it
203 return PooledDataMapper(*this, std::make_unique<DataMapper>());
204 }
205
206 // get a data mapper from the pool
207 auto dm = std::move(_idleDataMappers.back());
208 _idleDataMappers.pop_back();
209 return PooledDataMapper(*this, std::move(dm));
210 }
211
212#if defined(BUILD_TESTS)
213 [[nodiscard]] size_t IdleCount() noexcept
214 {
215 std::scoped_lock lock(_mutex);
216 return _idleDataMappers.size();
217 }
218#endif
219
220 private:
221 std::mutex _mutex;
222 std::condition_variable _cv;
223 std::vector<std::unique_ptr<DataMapper>> _idleDataMappers;
224 size_t _checkedOut {};
225};
226
227// Default pool configuration, configurable via CMake options:
228// LIGHTWEIGHT_POOL_INITIAL_SIZE (default: 4)
229// LIGHTWEIGHT_POOL_MAX_SIZE (default: 16)
230// LIGHTWEIGHT_POOL_GROWTH_STRATEGY (default: BoundedOverflow)
231// Accepted values: BoundedWait, BoundedOverflow, UnboundedGrow
232
233#if !defined(LIGHTWEIGHT_POOL_INITIAL_SIZE)
234 #define LIGHTWEIGHT_POOL_INITIAL_SIZE 4
235#endif
236
237#if !defined(LIGHTWEIGHT_POOL_MAX_SIZE)
238 #define LIGHTWEIGHT_POOL_MAX_SIZE 16
239#endif
240
241#if !defined(LIGHTWEIGHT_POOL_GROWTH_STRATEGY)
242 #define LIGHTWEIGHT_POOL_GROWTH_STRATEGY BoundedOverflow
243#endif
244
245inline constexpr PoolConfig DefaultPoolConfig {
246 .initialSize = LIGHTWEIGHT_POOL_INITIAL_SIZE,
247 .maxSize = LIGHTWEIGHT_POOL_MAX_SIZE,
248 .growthStrategy = GrowthStrategy::LIGHTWEIGHT_POOL_GROWTH_STRATEGY,
249};
250
251using DataMapperPool = Pool<DefaultPoolConfig>;
252
253/// Returns the process-wide global DataMapper pool.
254///
255/// The pool is configured at compile time via the LIGHTWEIGHT_POOL_* defines.
256/// Because the singleton lives inside the Lightweight library, it is shared
257/// correctly across shared-library boundaries.
258LIGHTWEIGHT_API DataMapperPool& GlobalDataMapperPool();
259
260} // 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:90
PooledDataMapper(PooledDataMapper &&other) noexcept
Definition Pool.hpp:76
DataMapper & Get() const noexcept
Definition Pool.hpp:98
PooledDataMapper Acquire()
Definition Pool.hpp:196
PooledDataMapper Acquire()
Definition Pool.hpp:167
~Pool() noexcept=default
GrowthStrategy growthStrategy
Definition Pool.hpp:46
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:39