From 6d63006a0b7bb8b9d96a45ff015f0eb6293c1235 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 7 Jul 2026 22:14:38 -0300 Subject: [PATCH] Add AttachmentPool --- src/fb-cpp/AttachmentPool.cpp | 339 +++++++++++++++++++++++++ src/fb-cpp/AttachmentPool.h | 452 ++++++++++++++++++++++++++++++++++ src/fb-cpp/fb-cpp.h | 1 + src/test/Attachment.cpp | 4 +- src/test/AttachmentPool.cpp | 178 +++++++++++++ 5 files changed, 972 insertions(+), 2 deletions(-) create mode 100644 src/fb-cpp/AttachmentPool.cpp create mode 100644 src/fb-cpp/AttachmentPool.h create mode 100644 src/test/AttachmentPool.cpp diff --git a/src/fb-cpp/AttachmentPool.cpp b/src/fb-cpp/AttachmentPool.cpp new file mode 100644 index 0000000..55516c5 --- /dev/null +++ b/src/fb-cpp/AttachmentPool.cpp @@ -0,0 +1,339 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "AttachmentPool.h" +#include "Client.h" +#include "Exception.h" +#include +#include +#include +#include +#include + +using namespace fbcpp; +using namespace fbcpp::impl; + + +static std::chrono::steady_clock::time_point steadyNow() +{ + return std::chrono::steady_clock::now(); +} + +static std::unique_ptr extractEntry( + std::vector>& entries, AttachmentPoolEntry* entry) +{ + const auto it = std::find_if( + entries.begin(), entries.end(), [entry](const auto& candidate) { return candidate.get() == entry; }); + + if (it == entries.end()) + return nullptr; + + auto owned = std::move(*it); + entries.erase(it); + + return owned; +} + + +AttachmentPool::AttachmentPool(Client& client_, std::string uri_, const AttachmentPoolOptions& options) + : client{&client_}, + uri{std::move(uri_)}, + options{options} +{ + if (options.getAttachmentOptions().getCreateDatabase()) + { + throw FbCppException( + "AttachmentPoolOptions::setAttachmentOptions must not enable AttachmentOptions::setCreateDatabase"); + } + + if (options.getMaxSize() < 1u) + throw FbCppException("AttachmentPoolOptions::setMaxSize must be at least 1"); + + if (options.getMinSize() > options.getMaxSize()) + throw FbCppException("AttachmentPoolOptions::setMinSize must not be greater than setMaxSize"); + + for (std::size_t i = 0u; i < options.getMinSize(); ++i) + { + auto entry = std::make_unique(); + entry->attachment = std::make_unique(*client, uri, options.getAttachmentOptions()); + entry->createdAt = entry->lastReturnedAt = steadyNow(); + + available.push_back(entry.get()); + entries.push_back(std::move(entry)); + } +} + +AttachmentPool::~AttachmentPool() noexcept +{ + assert(inUse == 0u && pending == 0u); + + try + { + close(); + } + catch (...) + { + // swallow + } +} + +std::size_t AttachmentPool::size() +{ + std::lock_guard mutexGuard{mutex}; + return entries.size(); +} + +std::size_t AttachmentPool::availableCount() +{ + std::lock_guard mutexGuard{mutex}; + return available.size(); +} + +std::size_t AttachmentPool::inUseCount() +{ + std::lock_guard mutexGuard{mutex}; + return inUse; +} + +PooledAttachment AttachmentPool::acquire() +{ + auto lease = acquireImpl(true); + assert(lease.has_value()); + return std::move(*lease); +} + +std::optional AttachmentPool::tryAcquire() +{ + return acquireImpl(false); +} + +void AttachmentPool::close() +{ + std::unique_lock mutexGuard{mutex}; + closed = true; + + while (!available.empty()) + { + auto* entry = available.front(); + available.pop_front(); + + auto owned = extractEntry(entries, entry); + + mutexGuard.unlock(); + owned.reset(); // Disconnects (network I/O) outside the lock. + mutexGuard.lock(); + } +} + +std::optional AttachmentPool::acquireImpl(bool throwOnFailure) +{ + std::unique_lock mutexGuard{mutex}; + + if (closed) + { + if (throwOnFailure) + throw FbCppException("AttachmentPool is closed"); + + return std::nullopt; + } + + const auto deadline = steadyNow() + options.getAcquireTimeout(); + + while (true) + { + evictLocked(mutexGuard); + + if (!available.empty()) + { + auto* entry = available.front(); + available.pop_front(); + ++inUse; + + if (options.getValidateOnAcquire()) + { + mutexGuard.unlock(); + + bool valid = true; + + try + { + entry->attachment->ping(); + } + catch (...) + { + valid = false; + } + + mutexGuard.lock(); + + if (!valid) + { + --inUse; + auto owned = extractEntry(entries, entry); + + mutexGuard.unlock(); + owned.reset(); // Disconnects (network I/O) outside the lock. + mutexGuard.lock(); + + continue; + } + } + + return PooledAttachment{*this, *entry}; + } + + if (entries.size() + pending < options.getMaxSize()) + { + ++pending; + mutexGuard.unlock(); + + std::unique_ptr newEntry; + std::exception_ptr failure; + + try + { + newEntry = std::make_unique(); + newEntry->attachment = std::make_unique(*client, uri, options.getAttachmentOptions()); + newEntry->createdAt = newEntry->lastReturnedAt = steadyNow(); + } + catch (...) + { + failure = std::current_exception(); + } + + mutexGuard.lock(); + --pending; + + if (failure) + { + availableCondition.notify_one(); // Let another waiter try instead. + + if (throwOnFailure) + std::rethrow_exception(failure); + + return std::nullopt; + } + + auto* entryPtr = newEntry.get(); + entries.push_back(std::move(newEntry)); + ++inUse; + + return PooledAttachment{*this, *entryPtr}; + } + + const auto remaining = deadline - steadyNow(); + + if (remaining <= std::chrono::steady_clock::duration::zero() || + availableCondition.wait_for(mutexGuard, remaining) == std::cv_status::timeout) + { + if (throwOnFailure) + throw FbCppException("Timed out waiting for an available connection from AttachmentPool"); + + return std::nullopt; + } + } +} + +void AttachmentPool::evictLocked(std::unique_lock& lock) +{ + assert(lock.owns_lock()); + + const auto currentTime = steadyNow(); + std::vector> toDestroy; + + for (auto it = available.begin(); it != available.end();) + { + auto* entry = *it; + bool evict = false; + + if (options.getMaxLifetime() && currentTime - entry->createdAt >= *options.getMaxLifetime()) + evict = true; + else if (options.getIdleTimeout() && entries.size() > options.getMinSize() && + currentTime - entry->lastReturnedAt >= *options.getIdleTimeout()) + evict = true; + + if (evict) + { + it = available.erase(it); + + if (auto owned = extractEntry(entries, entry)) + toDestroy.push_back(std::move(owned)); + } + else + ++it; + } + + if (!toDestroy.empty()) + { + lock.unlock(); + toDestroy.clear(); // Disconnects (network I/O) outside the lock. + lock.lock(); + } +} + +void AttachmentPool::release(AttachmentPoolEntry& entry) noexcept +{ + try + { + bool resetFailed = false; + + if (!closed && options.getSessionResetOnRelease() && entry.attachment && entry.attachment->isValid()) + { + try + { + entry.attachment->resetSession(); + } + catch (...) + { + resetFailed = true; // Force this connection to be discarded below. + } + } + + std::unique_ptr toDestroy; + + { // scope + std::lock_guard mutexGuard{mutex}; + + assert(inUse > 0u); + --inUse; + + const bool discard = closed || !entry.attachment || !entry.attachment->isValid() || resetFailed; + + if (discard) + toDestroy = extractEntry(entries, &entry); + else + { + entry.lastReturnedAt = steadyNow(); + available.push_back(&entry); + } + } + + availableCondition.notify_one(); + // toDestroy (if any) disconnects here, outside the lock. + } + catch (...) + { + // swallow + } +} diff --git a/src/fb-cpp/AttachmentPool.h b/src/fb-cpp/AttachmentPool.h new file mode 100644 index 0000000..7019bdf --- /dev/null +++ b/src/fb-cpp/AttachmentPool.h @@ -0,0 +1,452 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef FBCPP_ATTACHMENT_POOL_H +#define FBCPP_ATTACHMENT_POOL_H + +#include "Attachment.h" +#include "fb-api.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/// +/// fb-cpp namespace. +/// +namespace fbcpp +{ + class Client; + + /// + /// Represents options used when creating an AttachmentPool object. + /// + class AttachmentPoolOptions final + { + public: + /// + /// Returns the options used to create each pooled Attachment. + /// + const AttachmentOptions& getAttachmentOptions() const + { + return attachmentOptions; + } + + /// + /// Sets the options used to create each pooled Attachment. + /// AttachmentOptions::setCreateDatabase must not be enabled, since a pool only attaches to an + /// existing database. + /// + AttachmentPoolOptions& setAttachmentOptions(const AttachmentOptions& value) + { + attachmentOptions = value; + return *this; + } + + /// + /// Returns the minimum number of connections kept warm by the pool. + /// + std::size_t getMinSize() const + { + return minSize; + } + + /// + /// Sets the minimum number of connections kept warm by the pool. + /// + AttachmentPoolOptions& setMinSize(std::size_t value) + { + minSize = value; + return *this; + } + + /// + /// Returns the maximum number of connections the pool may create. + /// + std::size_t getMaxSize() const + { + return maxSize; + } + + /// + /// Sets the maximum number of connections the pool may create. + /// + AttachmentPoolOptions& setMaxSize(std::size_t value) + { + maxSize = value; + return *this; + } + + /// + /// Returns how long acquire() waits for a connection to become available before throwing. + /// + std::chrono::milliseconds getAcquireTimeout() const + { + return acquireTimeout; + } + + /// + /// Sets how long acquire() waits for a connection to become available before throwing. + /// + AttachmentPoolOptions& setAcquireTimeout(std::chrono::milliseconds value) + { + acquireTimeout = value; + return *this; + } + + /// + /// Returns the maximum time a connection may stay idle in the pool before being closed. + /// + const std::optional& getIdleTimeout() const + { + return idleTimeout; + } + + /// + /// Sets the maximum time a connection may stay idle in the pool before being closed. + /// Connections are never evicted below getMinSize() due to idling. + /// + AttachmentPoolOptions& setIdleTimeout(std::chrono::milliseconds value) + { + idleTimeout = value; + return *this; + } + + /// + /// Returns the maximum lifetime of a connection, regardless of usage. + /// + const std::optional& getMaxLifetime() const + { + return maxLifetime; + } + + /// + /// Sets the maximum lifetime of a connection, regardless of usage. + /// + AttachmentPoolOptions& setMaxLifetime(std::chrono::milliseconds value) + { + maxLifetime = value; + return *this; + } + + /// + /// Returns whether a connection is validated (via ping) before being handed out by acquire(). + /// + bool getValidateOnAcquire() const + { + return validateOnAcquire; + } + + /// + /// Sets whether a connection is validated (via ping) before being handed out by acquire(). + /// + AttachmentPoolOptions& setValidateOnAcquire(bool value) + { + validateOnAcquire = value; + return *this; + } + + /// + /// Returns whether a connection has its session reset (via ALTER SESSION RESET) before being + /// returned to the pool. + /// + bool getSessionResetOnRelease() const + { + return sessionResetOnRelease; + } + + /// + /// Sets whether a connection has its session reset (via ALTER SESSION RESET) before being + /// returned to the pool. A connection whose reset fails is discarded instead of being reused. + /// + AttachmentPoolOptions& setSessionResetOnRelease(bool value) + { + sessionResetOnRelease = value; + return *this; + } + + private: + AttachmentOptions attachmentOptions; + std::size_t minSize = 0u; + std::size_t maxSize = 10u; + std::chrono::milliseconds acquireTimeout{30000}; + std::optional idleTimeout; + std::optional maxLifetime; + bool validateOnAcquire = true; + bool sessionResetOnRelease = false; + }; + + + namespace impl + { + /// + /// Internal state tracked by AttachmentPool for a single pooled connection. + /// This is an implementation detail shared by AttachmentPool and PooledAttachment. + /// + struct AttachmentPoolEntry final + { + std::unique_ptr attachment; + std::chrono::steady_clock::time_point createdAt; + std::chrono::steady_clock::time_point lastReturnedAt; + }; + } // namespace impl + + + class AttachmentPool; + + /// + /// RAII lease for an Attachment borrowed from an AttachmentPool. + /// The leased Attachment is confined to the thread holding the lease; it is not thread-safe. + /// Returns the Attachment to the pool when the lease is destroyed, unless it was already released. + /// + class PooledAttachment final + { + friend class AttachmentPool; + + private: + explicit PooledAttachment(AttachmentPool& pool, impl::AttachmentPoolEntry& entry) noexcept + : pool{&pool}, + entry{&entry} + { + } + + public: + /// + /// Move constructor. + /// A moved PooledAttachment object becomes invalid. + /// + PooledAttachment(PooledAttachment&& o) noexcept + : pool{o.pool}, + entry{o.entry} + { + o.pool = nullptr; + o.entry = nullptr; + } + + /// + /// Move assignment. + /// Releases the currently held Attachment (if any) back to the pool before taking ownership of o's. + /// + PooledAttachment& operator=(PooledAttachment&& o) noexcept + { + if (this != &o) + { + release(); + + pool = o.pool; + entry = o.entry; + o.pool = nullptr; + o.entry = nullptr; + } + + return *this; + } + + PooledAttachment(const PooledAttachment&) = delete; + PooledAttachment& operator=(const PooledAttachment&) = delete; + + /// + /// Returns the Attachment to the pool, unless it was already released. + /// + ~PooledAttachment() noexcept + { + try + { + release(); + } + catch (...) + { + // swallow + } + } + + public: + /// + /// Returns whether this object currently holds a leased Attachment. + /// + bool isValid() const noexcept + { + return entry != nullptr; + } + + /// + /// Returns the leased Attachment. + /// + Attachment& get() noexcept + { + assert(entry); + return *entry->attachment; + } + + Attachment& operator*() noexcept + { + return get(); + } + + Attachment* operator->() noexcept + { + return &get(); + } + + /// + /// Returns the leased Attachment to the pool immediately. + /// Does nothing if the lease was already released. + /// + void release(); + + private: + AttachmentPool* pool; + impl::AttachmentPoolEntry* entry; + }; + + /// + /// Represents a thread-safe pool of Attachment objects connected to the same database. + /// The pool itself is thread-safe, but each leased Attachment (and any Transaction or Statement + /// created from it) must be used by a single thread at a time. + /// The Client and the AttachmentPool must exist and remain valid while there are outstanding + /// PooledAttachment leases. + /// + class AttachmentPool final + { + friend class PooledAttachment; + + public: + /// + /// Constructs an AttachmentPool that connects to the database specified by the URI using the + /// specified Client object and options. + /// + explicit AttachmentPool(Client& client, std::string uri, const AttachmentPoolOptions& options = {}); + + /// + /// Closes every idle connection in the pool. + /// All leases must be returned to the pool before it is destroyed. + /// + ~AttachmentPool() noexcept; + + AttachmentPool(const AttachmentPool&) = delete; + AttachmentPool& operator=(const AttachmentPool&) = delete; + + AttachmentPool(AttachmentPool&&) = delete; + AttachmentPool& operator=(AttachmentPool&&) = delete; + + public: + /// + /// Returns the Client object reference used to create this AttachmentPool object. + /// + Client& getClient() noexcept + { + return *client; + } + + /// + /// Returns the database URI this pool connects to. + /// + const std::string& getUri() const noexcept + { + return uri; + } + + /// + /// Returns the options used to create this AttachmentPool object. + /// + const AttachmentPoolOptions& getOptions() const noexcept + { + return options; + } + + /// + /// Returns the total number of connections currently created by the pool (in use plus available). + /// + std::size_t size(); + + /// + /// Returns the number of connections currently available (not leased) in the pool. + /// + std::size_t availableCount(); + + /// + /// Returns the number of connections currently leased out by the pool. + /// + std::size_t inUseCount(); + + /// + /// Acquires a connection from the pool, creating one if needed and allowed by getMaxSize(). + /// Blocks up to getAcquireTimeout() while the pool is exhausted, then throws FbCppException. + /// + PooledAttachment acquire(); + + /// + /// Attempts to acquire a connection from the pool without throwing. + /// Returns std::nullopt if none becomes available within getAcquireTimeout(). + /// + std::optional tryAcquire(); + + /// + /// Closes every available connection and marks the pool as closed. + /// Further calls to acquire() or tryAcquire() will fail. + /// + void close(); + + private: + std::optional acquireImpl(bool throwOnFailure); + void evictLocked(std::unique_lock& lock); + void release(impl::AttachmentPoolEntry& entry) noexcept; + + private: + Client* client; + std::string uri; + AttachmentPoolOptions options; + std::vector> entries; + std::deque available; + std::size_t inUse = 0u; + std::size_t pending = 0u; + bool closed = false; + std::mutex mutex; + std::condition_variable availableCondition; + }; + + inline void PooledAttachment::release() + { + if (!entry) + return; + + const auto poolPtr = pool; + const auto entryPtr = entry; + + pool = nullptr; + entry = nullptr; + + poolPtr->release(*entryPtr); + } +} // namespace fbcpp + + +#endif // FBCPP_ATTACHMENT_POOL_H diff --git a/src/fb-cpp/fb-cpp.h b/src/fb-cpp/fb-cpp.h index b3f8fc8..0bf78de 100644 --- a/src/fb-cpp/fb-cpp.h +++ b/src/fb-cpp/fb-cpp.h @@ -27,6 +27,7 @@ #include "Client.h" #include "Attachment.h" +#include "AttachmentPool.h" #include "Transaction.h" #include "Descriptor.h" #include "Statement.h" diff --git a/src/test/Attachment.cpp b/src/test/Attachment.cpp index d21a814..ed3231d 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -749,8 +749,8 @@ BOOST_AUTO_TEST_CASE(resetSession) FbDropDatabase attachmentDrop{attachment}; Transaction transaction{attachment}; - attachment.execute(transaction, - "select rdb$set_context('USER_SESSION', 'test_var', 'test_value') from rdb$database"); + attachment.execute( + transaction, "select rdb$set_context('USER_SESSION', 'test_var', 'test_value') from rdb$database"); const auto valueBefore = attachment.queryScalar( transaction, "select rdb$get_context('USER_SESSION', 'test_var') from rdb$database"); BOOST_REQUIRE(valueBefore.has_value()); diff --git a/src/test/AttachmentPool.cpp b/src/test/AttachmentPool.cpp new file mode 100644 index 0000000..d8fdcf8 --- /dev/null +++ b/src/test/AttachmentPool.cpp @@ -0,0 +1,178 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "TestUtil.h" +#include "fb-cpp/Attachment.h" +#include "fb-cpp/AttachmentPool.h" +#include "fb-cpp/Exception.h" +#include "fb-cpp/Transaction.h" +#include +#include + +using namespace std::chrono_literals; + + +BOOST_AUTO_TEST_SUITE(AttachmentPoolSuite) + +BOOST_AUTO_TEST_CASE(lifecycleAndCounts) +{ + const auto database = getTempFile("AttachmentPool-lifecycleAndCounts.fdb"); + Attachment setup{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase setupDrop{setup}; + + { // scope + Transaction transaction{setup}; + BOOST_REQUIRE(setup.execute(transaction, "create table t (id integer not null primary key)")); + transaction.commit(); + } + + AttachmentPoolOptions options; + options.setAttachmentOptions(AttachmentOptions().setConnectionCharSet("UTF8")).setMinSize(2u).setMaxSize(4u); + + AttachmentPool pool{CLIENT, database, options}; + BOOST_CHECK_EQUAL(pool.size(), 2u); + BOOST_CHECK_EQUAL(pool.availableCount(), 2u); + BOOST_CHECK_EQUAL(pool.inUseCount(), 0u); + + { // scope + auto lease = pool.acquire(); + BOOST_CHECK(lease.isValid()); + BOOST_CHECK_EQUAL(pool.inUseCount(), 1u); + BOOST_CHECK_EQUAL(pool.availableCount(), 1u); + + Transaction transaction{*lease}; + BOOST_REQUIRE(lease->execute(transaction, "insert into t (id) values (1)")); + const auto count = lease->queryScalar(transaction, "select count(*) from t"); + BOOST_REQUIRE(count.has_value()); + BOOST_CHECK_EQUAL(*count, 1); + transaction.commit(); + } + + BOOST_CHECK_EQUAL(pool.inUseCount(), 0u); + BOOST_CHECK_EQUAL(pool.availableCount(), 2u); +} + +BOOST_AUTO_TEST_CASE(acquireReusesUnderlyingConnection) +{ + const auto database = getTempFile("AttachmentPool-acquireReusesUnderlyingConnection.fdb"); + Attachment setup{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase setupDrop{setup}; + + AttachmentPool pool{CLIENT, database, AttachmentPoolOptions().setMinSize(1u).setMaxSize(1u)}; + + fb::IAttachment* firstHandle = nullptr; + + { // scope + auto lease = pool.acquire(); + firstHandle = lease->getHandle().get(); + } + + { // scope + auto lease = pool.acquire(); + BOOST_CHECK_EQUAL(lease->getHandle().get(), firstHandle); + } + + BOOST_CHECK_EQUAL(pool.size(), 1u); +} + +BOOST_AUTO_TEST_CASE(acquireThrowsWhenExhaustedAndUnblocksOnRelease) +{ + const auto database = getTempFile("AttachmentPool-acquireThrowsWhenExhaustedAndUnblocksOnRelease.fdb"); + Attachment setup{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase setupDrop{setup}; + + AttachmentPool pool{ + CLIENT, database, AttachmentPoolOptions().setMinSize(0u).setMaxSize(1u).setAcquireTimeout(200ms)}; + + auto lease1 = pool.acquire(); + BOOST_CHECK_EQUAL(pool.inUseCount(), 1u); + + BOOST_CHECK_THROW(pool.acquire(), FbCppException); + BOOST_CHECK(!pool.tryAcquire().has_value()); + + lease1.release(); + BOOST_CHECK_EQUAL(pool.inUseCount(), 0u); + + auto lease2 = pool.acquire(); + BOOST_CHECK(lease2.isValid()); +} + +BOOST_AUTO_TEST_CASE(leaseSemantics) +{ + const auto database = getTempFile("AttachmentPool-leaseSemantics.fdb"); + Attachment setup{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase setupDrop{setup}; + + AttachmentPool pool{CLIENT, database, AttachmentPoolOptions().setMinSize(1u).setMaxSize(1u)}; + + auto lease1 = pool.acquire(); + BOOST_CHECK(lease1.isValid()); + + auto lease2 = std::move(lease1); + BOOST_CHECK(!lease1.isValid()); + BOOST_CHECK(lease2.isValid()); + BOOST_CHECK_EQUAL(pool.inUseCount(), 1u); + + lease2.release(); + BOOST_CHECK(!lease2.isValid()); + BOOST_CHECK_EQUAL(pool.inUseCount(), 0u); + + pool.close(); + BOOST_CHECK_THROW(pool.acquire(), FbCppException); +} + +BOOST_AUTO_TEST_CASE(sessionResetOnRelease) +{ + const auto database = getTempFile("AttachmentPool-sessionResetOnRelease.fdb"); + Attachment setup{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase setupDrop{setup}; + + AttachmentPool pool{ + CLIENT, database, AttachmentPoolOptions().setMinSize(1u).setMaxSize(1u).setSessionResetOnRelease(true)}; + + fb::IAttachment* firstHandle = nullptr; + + { // scope + auto lease = pool.acquire(); + firstHandle = lease->getHandle().get(); + + Transaction transaction{*lease}; + lease->queryScalar( + transaction, "select rdb$set_context('USER_SESSION', 'k', 'v') from rdb$database"); + transaction.commit(); + } + + { // scope + auto lease = pool.acquire(); + BOOST_CHECK_EQUAL(lease->getHandle().get(), firstHandle); + + Transaction transaction{*lease}; + const auto value = lease->queryScalar( + transaction, "select rdb$get_context('USER_SESSION', 'k') from rdb$database"); + BOOST_CHECK(!value.has_value()); + transaction.commit(); + } +} + +BOOST_AUTO_TEST_SUITE_END()