diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..7ad6d7a56 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,124 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +jobs: + build-and-unit-test: + runs-on: ubuntu-24.04 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + submodules: false + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + ccache \ + cmake \ + g++ \ + libboost-all-dev \ + libpq-dev \ + ninja-build + + - name: Configure OZO + run: > + cmake -S . -B build -G Ninja + -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DOZO_BUILD_TESTS=ON + -DOZO_BUILD_EXAMPLES=ON + + - name: Build OZO tests and examples + run: > + cmake --build build + --target + ozo_tests + ozo_request + ozo_transaction + ozo_retry_request + ozo_role_based_request + ozo_connection_pool + + - name: Run OZO unit tests + run: ctest --test-dir build --output-on-failure -R ozo_tests + + - name: Configure vendored resource_pool tests + run: > + cmake -S contrib/resource_pool -B build-resource-pool -G Ninja + -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DRESOURCE_POOL_BUILD_TESTS=ON + + - name: Build vendored resource_pool tests + run: cmake --build build-resource-pool --target resource_pool_test + + - name: Run vendored resource_pool tests + run: ctest --test-dir build-resource-pool --output-on-failure -R resource_pool_test + + postgres-integration-test: + runs-on: ubuntu-24.04 + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: ozo_test_db + POSTGRES_USER: ozo_test_user + POSTGRES_PASSWORD: v4Xpkocl~5l6h219Ynk1lJbM61jIr!ca + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U ozo_test_user -d ozo_test_db" + --health-interval 10s + --health-timeout 5s + --health-retries 12 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + submodules: false + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake \ + g++ \ + libboost-all-dev \ + libpq-dev \ + ninja-build \ + postgresql-client + + - name: Wait for PostgreSQL + env: + PGPASSWORD: v4Xpkocl~5l6h219Ynk1lJbM61jIr!ca + run: | + for _ in $(seq 1 30); do + if pg_isready -h 127.0.0.1 -p 5432 -U ozo_test_user -d ozo_test_db; then + exit 0 + fi + sleep 2 + done + exit 1 + + - name: Configure PostgreSQL integration tests + env: + OZO_PG_TEST_CONNINFO: host=127.0.0.1 port=5432 dbname=ozo_test_db user=ozo_test_user password=v4Xpkocl~5l6h219Ynk1lJbM61jIr!ca + run: > + cmake -S . -B build-pg -G Ninja + -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DOZO_BUILD_TESTS=ON + -DOZO_BUILD_PG_TESTS=ON + -DOZO_PG_TEST_CONNINFO="$OZO_PG_TEST_CONNINFO" + + - name: Build PostgreSQL integration tests + run: cmake --build build-pg --target ozo_tests + + - name: Run PostgreSQL integration tests + run: ctest --test-dir build-pg --output-on-failure -R ozo_tests diff --git a/.gitignore b/.gitignore index 378eac25d..d66047b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ build + +# Ignore clangd cache +.cache/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 44e4425a2..000000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "contrib/resource_pool"] - path = contrib/resource_pool - url = https://github.com/elsid/resource_pool.git - branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt index 98e64e98f..a193033f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/mo # note: boost::coroutine should automatically pull in all the dependencies, # however, due to changes in boost it doesn't always work until CMake # is updated. we therefore look for all the dependencies ourselves. -find_package(Boost COMPONENTS coroutine context system thread atomic REQUIRED) +find_package(Boost 1.74 COMPONENTS coroutine context system thread atomic REQUIRED) find_package(PostgreSQL REQUIRED) # Try and find provided resource pool (e.g. from conan), default to vendored version otherwise @@ -25,7 +25,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) add_library(ozo INTERFACE) add_library(yandex::ozo ALIAS ozo) -target_compile_features(ozo INTERFACE cxx_std_17) +target_compile_features(ozo INTERFACE cxx_std_20) if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") # disable warnings about a compiler-specific option used for gnu-compatible compilers @@ -40,9 +40,6 @@ target_include_directories(ozo INTERFACE target_compile_definitions(ozo INTERFACE -DBOOST_COROUTINES_NO_DEPRECATION_WARNING) target_compile_definitions(ozo INTERFACE -DBOOST_HANA_CONFIG_ENABLE_STRING_UDL) -# For the time OZO may not support Executor TS -# See https://github.com/yandex/ozo/issues/266 -target_compile_definitions(ozo INTERFACE -DBOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT) target_link_libraries(ozo INTERFACE Boost::coroutine) target_link_libraries(ozo INTERFACE PostgreSQL::PostgreSQL) diff --git a/README.md b/README.md index 0406f9fdd..c32b8800f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # ozo -[![Build Status](https://travis-ci.org/yandex/ozo.svg?branch=master)](https://travis-ci.org/yandex/ozo) +[![CI](https://github.com/haomingbai/ozo/actions/workflows/ci.yml/badge.svg)](https://github.com/haomingbai/ozo/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/yandex/ozo/branch/master/graph/badge.svg)](https://codecov.io/gh/yandex/ozo) ## What's this -OZO is a C++17 library for asyncronous communication with PostgreSQL DBMS. +OZO is a C++20 library for asyncronous communication with PostgreSQL DBMS. The library leverages the power of template metaprogramming, providing convenient mapping from C++ types to SQL along with rich query building possibilities. OZO supports different concurrency paradigms (callbacks, futures, coroutines), using Boost.Asio under the hood. Low-level communication with PostgreSQL server is done via libpq. All concepts in the library are designed to be easily extendable (even replaceable) by the user to simplify adaptation to specific project requirements. ### API @@ -19,20 +19,22 @@ Since the project is on early state of development it lacks of documentation. We ## Compatibilities -For the time OZO is not compatible with new executors models that are used by default since Boost 1.74. The `BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT` macro needs to be defined. See Boost 1.74 [changelog](https://www.boost.org/doc/libs/1_74_0/doc/html/boost_asio/history.html#boost_asio.history.asio_1_18_0___boost_1_74) for the details. +OZO now targets the default executor model used by modern Boost.Asio releases. +`BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT` is no longer required and is no longer +exported to consumers. ## Dependencies These things are needed: * **CMake** is used as build system * **GCC** or **Clang** C++ compiler with C++17 support (tested with GCC 7.0, Clang 5.0 and Apple LLVM version 9.0.0) -* **Boost** >= 1.66 with `BOOST_HANA_CONFIG_ENABLE_STRING_UDL` defined. +* **Boost** >= 1.74 with `BOOST_HANA_CONFIG_ENABLE_STRING_UDL` defined. * **libpq** >= 9.3 -* Ozo uses the [resource_pool](https://github.com/elsid/resource_pool) library as a git submodule, so in case of using a package version, this dependency should be satisfied too. +* OZO vendors the [resource_pool](https://github.com/elsid/resource_pool) sources directly under `contrib/resource_pool`. -If you want to run integration tests and/or build inside Docker container: -* **Docker** >= 1.13.0 -* **Docker Compose** >= 1.10.0 +If you want to run PostgreSQL integration tests locally: +* **Podman** +* a local copy of `docker.io/library/postgres:16` or another PostgreSQL image ## Build @@ -118,24 +120,39 @@ scripts/build.sh docker docs ### Test against a local postgres -You can use `scripts/build.sh` but add `pg` first: +The recommended path is the Podman helper script: ```bash -scripts/build.sh pg +scripts/run_pg_tests_podman.sh ``` -or if you want build code in docker: +The script starts a local PostgreSQL container from `docker.io/library/postgres:16` +by default, builds `ozo_tests` with `OZO_BUILD_PG_TESTS=ON`, runs the full test +suite, and removes the container afterwards. + +You can override the image, port, build directory, and PostgreSQL credentials +with environment variables: ```bash -scripts/build.sh pg docker +export OZO_PODMAN_POSTGRES_IMAGE=docker.io/library/postgres:16 +export OZO_PG_TEST_PORT=55432 +export OZO_PG_BUILD_DIR=build-podman-pg + +scripts/run_pg_tests_podman.sh ``` -This will attempt to launch postgres:alpine from your Docker registry. -Or you can point ozo tests to a postgres of your choosing by setting these environment variables prior to building: +Or you can point OZO tests to a PostgreSQL instance of your choosing by setting +these environment variables prior to building: ```bash export OZO_BUILD_PG_TESTS=ON export OZO_PG_TEST_CONNINFO='your conninfo (connection string)' -scripts/build.sh gcc debug +cmake -S . -B build-pg -DOZO_BUILD_TESTS=ON -DOZO_BUILD_PG_TESTS=ON -DOZO_PG_TEST_CONNINFO="$OZO_PG_TEST_CONNINFO" +cmake --build build-pg -j$(nproc) +ctest --test-dir build-pg -V ``` + +The older `docker-compose` based scripts remain in the repository for historical +development workflows, but the maintained path for local PostgreSQL testing is +the Podman flow above. diff --git a/benchmarks/ozo_benchmark.cpp b/benchmarks/ozo_benchmark.cpp index 558ada771..7e3655716 100644 --- a/benchmarks/ozo_benchmark.cpp +++ b/benchmarks/ozo_benchmark.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include diff --git a/benchmarks/performance.cpp b/benchmarks/performance.cpp index 59189aed7..15d9f00b1 100644 --- a/benchmarks/performance.cpp +++ b/benchmarks/performance.cpp @@ -8,7 +8,7 @@ #include -#include +#include #include #include diff --git a/cmake/modules/FindPostgreSQL.cmake b/cmake/modules/FindPostgreSQL.cmake index bd938edb6..4f2248479 100644 --- a/cmake/modules/FindPostgreSQL.cmake +++ b/cmake/modules/FindPostgreSQL.cmake @@ -223,7 +223,7 @@ endif() # Did we find anything? include(FindPackageHandleStandardArgs) find_package_handle_standard_args(PostgreSQL - REQUIRED_VARS PostgreSQL_LIBRARY PostgreSQL_INCLUDE_DIR PostgreSQL_TYPE_INCLUDE_DIR + REQUIRED_VARS PostgreSQL_LIBRARY PostgreSQL_INCLUDE_DIR VERSION_VAR PostgreSQL_VERSION_STRING) set(PostgreSQL_FOUND ${POSTGRESQL_FOUND}) @@ -247,15 +247,19 @@ endfunction() # Now try to get the include and library path. if(PostgreSQL_FOUND) + set(PostgreSQL_INCLUDE_DIRS "${PostgreSQL_INCLUDE_DIR}") + if(PostgreSQL_TYPE_INCLUDE_DIR) + list(APPEND PostgreSQL_INCLUDE_DIRS "${PostgreSQL_TYPE_INCLUDE_DIR}") + endif() + if (NOT TARGET PostgreSQL::PostgreSQL) add_library(PostgreSQL::PostgreSQL UNKNOWN IMPORTED) set_target_properties(PostgreSQL::PostgreSQL PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PostgreSQL_INCLUDE_DIR};${PostgreSQL_TYPE_INCLUDE_DIR}") + INTERFACE_INCLUDE_DIRECTORIES "${PostgreSQL_INCLUDE_DIRS}") __postgresql_import_library(PostgreSQL::PostgreSQL PostgreSQL_LIBRARY "") __postgresql_import_library(PostgreSQL::PostgreSQL PostgreSQL_LIBRARY "RELEASE") __postgresql_import_library(PostgreSQL::PostgreSQL PostgreSQL_LIBRARY "DEBUG") endif () - set(PostgreSQL_INCLUDE_DIRS ${PostgreSQL_INCLUDE_DIR} ${PostgreSQL_TYPE_INCLUDE_DIR} ) set(PostgreSQL_LIBRARY_DIRS ${PostgreSQL_LIBRARY_DIR} ) endif() diff --git a/conanfile.py b/conanfile.py index df0402c32..d62368bcd 100644 --- a/conanfile.py +++ b/conanfile.py @@ -57,8 +57,7 @@ def package_info(self): ] self.cpp_info.components["_ozo"].defines = [ "BOOST_COROUTINES_NO_DEPRECATION_WARNING", - "BOOST_HANA_CONFIG_ENABLE_STRING_UDL", - "BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT" + "BOOST_HANA_CONFIG_ENABLE_STRING_UDL" ] compiler = self.settings.compiler diff --git a/contrib/resource_pool b/contrib/resource_pool deleted file mode 160000 index 3ea1f95fe..000000000 --- a/contrib/resource_pool +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3ea1f95fecc9ce28e66bbbc2a36c1e0142551ab6 diff --git a/contrib/resource_pool/.github/workflows/build.yaml b/contrib/resource_pool/.github/workflows/build.yaml new file mode 100644 index 000000000..48d6b5818 --- /dev/null +++ b/contrib/resource_pool/.github/workflows/build.yaml @@ -0,0 +1,128 @@ +name: Build + +on: +- push +- pull_request + +jobs: + build: + strategy: + matrix: + build_type: + - Release + - Debug + + deps: + - os: ubuntu-20.04 + compiler: g++-7 + compiler_package: g++-7 + boost: '1.67' + + - os: ubuntu-20.04 + compiler: clang++-7 + compiler_package: clang-7 + boost: '1.67' + + - os: ubuntu-22.04 + compiler: g++-11 + compiler_package: g++-11 + boost: '1.74' + + - os: ubuntu-22.04 + compiler: clang++-15 + compiler_package: clang-15 + boost: '1.74' + + name: ${{ matrix.build_type }} ${{ matrix.deps.compiler }} boost ${{ matrix.deps.boost }} ${{ matrix.deps.os }} + runs-on: ${{ matrix.deps.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Update apt cache + run: sudo apt-get update + + - name: Install apt packages for dependencies + run: > + sudo apt-get install -y --no-install-recommends + ccache + cmake + libboost-atomic${{ matrix.deps.boost }}-dev + libboost-context${{ matrix.deps.boost }}-dev + libboost-coroutine${{ matrix.deps.boost }}-dev + libboost-date-time${{ matrix.deps.boost }}-dev + libboost-thread${{ matrix.deps.boost }}-dev + ninja-build + ${{ matrix.deps.compiler_package }} + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.deps.os }}-${{ matrix.deps.compiler }}-${{ matrix.deps.boost }}-${{ matrix.build_type }}-v1 + max-size: 1G + + - name: Configure + run: > + cmake + -B build + -S . + -G Ninja + -D CMAKE_BUILD_TYPE=${{ matrix.build_type }} + -D CMAKE_INSTALL_PREFIX=${{ github.workspace }}/install + -D CMAKE_CXX_COMPILER=${{ matrix.deps.compiler }} + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache + -D RESOURCE_POOL_BUILD_TESTS=ON + -D RESOURCE_POOL_BUILD_EXAMPLES=ON + -D RESOURCE_POOL_BUILD_BENCHMARKS=ON + -D RESOURCE_POOL_USE_SYSTEM_GOOGLETEST=OFF + -D RESOURCE_POOL_USE_SYSTEM_BENCHMARK=OFF + + - name: Build + run: cmake --build build + + - name: Install + run: cmake --install build + + - name: Run tests + run: build/tests/resource_pool_test + + - name: Run benchmark + run: build/benchmarks/resource_pool_benchmark_async + + - name: Run async_pool example + run: build/examples/async_pool + + - name: Run async_strand example + run: build/examples/async_strand + + - name: Run coro_pool example + run: build/examples/coro_pool + + - name: Run sync_pool example + run: build/examples/sync_pool + + test_conan_package: + name: Test conan package + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Update apt cache + run: sudo apt-get update + + - name: Install apt packages for dependencies + run: > + sudo apt-get install -y --no-install-recommends + clang-15 + cmake + libboost-atomic1.74-dev + libboost-context1.74-dev + libboost-coroutine1.74-dev + libboost-date-time1.74-dev + libboost-thread1.74-dev + python3 + python3-pip + + - name: Create depending package + run: scripts/ci/conan.sh diff --git a/contrib/resource_pool/.gitignore b/contrib/resource_pool/.gitignore new file mode 100644 index 000000000..5d6a24902 --- /dev/null +++ b/contrib/resource_pool/.gitignore @@ -0,0 +1,2 @@ +# Out of source build +build/ diff --git a/contrib/resource_pool/AUTHORS b/contrib/resource_pool/AUTHORS new file mode 100644 index 000000000..d9a143cc5 --- /dev/null +++ b/contrib/resource_pool/AUTHORS @@ -0,0 +1,8 @@ +The following authors have created the source code of "resource_pool": + +Roman Siromakha +Petr Reznikov +Roman Gornak +Sergei Khandrikov +Denis Kutukov +Jonas Stoehr diff --git a/contrib/resource_pool/CMakeLists.txt b/contrib/resource_pool/CMakeLists.txt new file mode 100644 index 000000000..917d27b90 --- /dev/null +++ b/contrib/resource_pool/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.12) +project(resource_pool VERSION 0.1.0 LANGUAGES CXX) + +option(RESOURCE_POOL_BUILD_EXAMPLES OFF) +option(RESOURCE_POOL_BUILD_TESTS OFF) +option(RESOURCE_POOL_BUILD_BENCHMARKS OFF) +option(RESOURCE_POOL_USE_SYSTEM_GOOGLETEST OFF) +option(RESOURCE_POOL_USE_SYSTEM_BENCHMARK OFF) + +add_subdirectory(include) + +if(RESOURCE_POOL_BUILD_TESTS) + enable_testing() + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + add_subdirectory(tests) +endif() + +if(RESOURCE_POOL_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + +if(RESOURCE_POOL_BUILD_BENCHMARKS) + add_subdirectory(benchmarks) +endif() diff --git a/contrib/resource_pool/LICENSE b/contrib/resource_pool/LICENSE new file mode 100644 index 000000000..f0875a251 --- /dev/null +++ b/contrib/resource_pool/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Roman Siromakha + +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. diff --git a/contrib/resource_pool/README.md b/contrib/resource_pool/README.md new file mode 100644 index 000000000..14945e289 --- /dev/null +++ b/contrib/resource_pool/README.md @@ -0,0 +1,300 @@ +# Resource pool + +![Build Status](https://github.com/elsid/resource_pool/actions/workflows/build.yaml/badge.svg) + +Header only library purposed to create pool of some resources like keep-alive connections. +Supports sync and async interfaces. Based on boost. + +## Dependencies + +* **CMake** >= 3.12 +* **GCC** or **Clang** C++ compilers with C++17 support. +* **Boost** >= 1.74 + +## Build + +Project uses CMake. Build need only to run tests, examples and benchmarks: +```bash +mkdir build +cd build +cmake .. +make -j $(nproc) +ctest -V +examples/sync_pool +examples/async_pool +benchmarks/resource_pool_benchmark_async +``` + +## Install + +Include as subdirectory into your CMake project or copy folder include. In OZO, +this copy is maintained directly in-tree rather than as a git submodule. + +## Usage + +### Handle + +The handle contains iterator to ```boost::optional``` of resource value in pool. +Declared as type [handle](include/yamail/resource_pool/handle.hpp#L11-L56). +Constructs with one of strategies that uses in destructor: +* waste -- resets iterator if handle is usable. +* recycle -- returns iterator to pool if handle is usable. + +Pool contains slots for resources that means handle iterator may refers to empty ```boost::optional```. +Client always must check value before using by method: +```c++ +bool empty() const; +``` + +Access to value provides by methods: +```c++ +value_type& get(); +const value_type& get() const; +value_type *operator ->(); +const value_type *operator ->() const; +value_type &operator *(); +const value_type &operator *() const; +``` + +```value_type``` -- resource type. + +Value of handle changes by method: +```c++ +void reset(value_type&& res); +``` + +To drop resource use method: +```c++ +void waste(); +``` + +To return resource into pool use method: +```c++ +void recycle(); +``` + +Both methods makes handle unusable that could be checked by method: +```c++ +bool unusable() const; +``` + +Calling one of these methods for unusable handle throws an exception ```error::unusable_handle```. + +### Synchronous pool + +Based on ```std::condition_variable```. + +#### Create pool + +Use type [sync::pool](include/yamail/resource_pool/sync/pool.hpp#L14-L65). Parametrize resource type: +```c++ +template > +class pool; +``` + +Pool holds ```boost::optional``` of resource type. + +Example: +```c++ +using fstream_pool = pool; +``` + +Object construction requires pool capacity: +```c++ +pool( + std::size_t capacity, + time_traits::duration idle_timeout = time_traits::duration::max(), + time_traits::duration lifespan = time_traits::duration::max() +); +``` + +* `idle_timeout` defines maximum time interval to keep unused resource in the pool. + Check for elapsed time happens on resource allocation. +* `lifespan` defines maximum time interval to keep resource. + Check for elapsed time happens on resource allocation and recycle. + +Example: +```c++ +fstream_pool pool(42); +``` + +#### Get handle + +Use one of these methods: +```c++ +std::pair get_auto_waste( + time_traits::duration wait_duration = time_traits::duration(0) +); +``` +returns resource handle when it will be available with auto waste strategy. + +```c++ +std::pair get_auto_recycle( + time_traits::duration wait_duration = time_traits::duration(0) +); +``` +returns resource handle when it will be available with auto recycle strategy. + +Recommends to use ```get_auto_waste``` and explicit call ```recycle```. + +Example: +```c++ +auto r = pool.get(time_traits::duration(1)); +const auto& ec = r.first; +if (ec) { + std::cerr << "Can't get resource: " << ec.message() << std::endl; + return; +} +auto& h = r.second; +if (h.empty()) { + h.reset(create_resource()); +} +use_resource(h.get()); +``` + +#### Invalidate pool + +Following method allows to force all available and used handles to be wasted: +```c++ +void invalidate(); +``` + +All currently available but not used handles will be wasted. All currently used handles will be wasted on return to the pool. + +### Asynchronous pool + +Based on ```boost::asio::io_context```. Uses async queue with deadline timer to store waiting resources requests. + +#### Create pool + +Use type [async::pool](include/yamail/resource_pool/async/pool.hpp#L34-L105). Parametrize resource type: +```c++ +template ::type> +class pool; +``` + +Pool holds ```boost::optional``` of resource type. + +Example: +```c++ +using fstream_pool = pool; +``` + +Object construction requires reference to io service, capacity of pool, queue capacity: +```c++ +pool( + io_context_t& io_context, + std::size_t capacity, + std::size_t queue_capacity, + time_traits::duration idle_timeout = time_traits::duration::max(), + time_traits::duration lifespan = time_traits::duration::max() +); +``` + +* `idle_timeout` defines maximum time interval to keep unused resource in the pool. + Check for elapsed time happens on resource allocation. +* `lifespan` defines maximum time interval to keep resource. + Check for elapsed time happens on resource allocation and recycle. + +Example: +```c++ +boost::asio::io_context io; +fstream_pool pool(io, 13, 42); +``` + +#### Get handle + +Use one of these methods: +```c++ +template +void get_auto_waste( + boost::asio::io_context& io, + CompletionToken&& token, + const time_traits::duration& wait_duration = time_traits::duration(0) +); +``` +returns resource handle when it will be available with auto waste strategy. + +```c++ +template +void get_auto_recycle( + boost::asio::io_context& io, + CompletionToken&& token, + const time_traits::duration& wait_duration = time_traits::duration(0) +); +``` +returns resource handle when it will be available with auto recycle strategy. + +Type ```CompetionToken``` must provide interface: +```c++ +void operator ()( + const boost::system::error_code&, + handle +); +``` +or it can be any valid completion token from **Boost.Asio** like ```boost::asio::yield_context```, ```boost::asio::use_future``` and so on. + +Recommends to use ```get_auto_waste``` and explicit call ```recycle```. + +If error occurs ```ec``` will be not ok and ```handle``` will be unusable. + +Example with classic callbacks: +```c++ +struct on_create_resource { + handle h; + + on_create_resource(handle h) : h(std::move(h)) {} + + void operator ()(const boost::system::error_code& ec, handle::value_type&& r) { + if (ec) { + std::cerr << "Can't create resource: " << ec.message() << std::endl; + return; + } + h.reset(std::move(r)); + use_resource(h.get()); + } +}; + +struct handle_get { + void operator ()(const boost::system::error_code& ec, handle h) { + if (ec) { + std::cerr << "Can't get resource: " << ec.message() << std::endl; + return; + } + if (h.empty()) { + async_create_resource(on_create_resource(std::move(h))); + } else { + use_resource(h.get()); + } + } +}; + +pool.get(handle_get(), time_traits::duration(1)); +``` + +Example with Boost.Coroutines: +```c++ +boost::asio::spawn(io, [&](boost::asio::yield_context yield) { + auto h = pool.get(yield, time_traits::duration(1)); + if (h.empty()) { + h.reset(create_resource(yield)); + } + use_resource(h.get()); +} +``` + +#### Invalidate pool + +Following method allows to force all available and used handles to be wasted: +```c++ +void invalidate(); +``` + +All currently available but not used handles will be wasted. All currently used handles will be wasted on return to the pool. + +## Examples + +Source code can be found in [examples](examples) directory. diff --git a/contrib/resource_pool/benchmarks/CMakeLists.txt b/contrib/resource_pool/benchmarks/CMakeLists.txt new file mode 100644 index 000000000..a02493ddc --- /dev/null +++ b/contrib/resource_pool/benchmarks/CMakeLists.txt @@ -0,0 +1,36 @@ +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic -Werror") + +include_directories(SYSTEM ${RESOURCE_POOL_DEPENDENCY_INCLUDE_DIRS}) +include_directories(${PROJECT_SOURCE_DIR}/include) + +find_package(Boost COMPONENTS coroutine context REQUIRED) + +if(RESOURCE_POOL_USE_SYSTEM_BENCHMARK) + find_package(benchmark) +else() + set(BENCHMARK_ENABLE_TESTING OFF) + set(BENCHMARK_ENABLE_INSTALL OFF) + set(BENCHMARK_ENABLE_GTEST_TESTS OFF) + + include(FetchContent) + FetchContent_Declare(benchmark + URL https://github.com/google/benchmark/archive/refs/tags/v1.8.3.zip + URL_HASH SHA512=d73587ad9c49338749e1d117a6f8c7ff9c603a91a2ffa91a7355c7df7dea82710b9a810d34ddfef20973ecdc77092ec10fb2b4e4cc8d2e7810cbed79617b3828 + SOURCE_DIR fetched/benchmark + ) + FetchContent_MakeAvailable(benchmark) +endif() + +add_executable(resource_pool_benchmark_async async.cc) + +set(LIBRARIES + pthread + benchmark::benchmark + Boost::coroutine + Boost::context + elsid::resource_pool +) + +target_link_libraries(resource_pool_benchmark_async ${LIBRARIES}) diff --git a/contrib/resource_pool/benchmarks/async.cc b/contrib/resource_pool/benchmarks/async.cc new file mode 100644 index 000000000..37f704004 --- /dev/null +++ b/contrib/resource_pool/benchmarks/async.cc @@ -0,0 +1,310 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace yamail::resource_pool; + +class benchmark_args { +public: + constexpr benchmark_args sequences(std::size_t value) const { + auto copy = *this; + copy.sequences_ = value; + return copy; + } + + constexpr std::size_t sequences() const { + return sequences_; + } + + constexpr benchmark_args threads(std::size_t value) const { + auto copy = *this; + copy.threads_ = value; + return copy; + } + + constexpr std::size_t threads() const { + return threads_; + } + + constexpr benchmark_args resources(std::size_t value) const { + auto copy = *this; + copy.resources_ = value; + return copy; + } + + constexpr std::size_t resources() const { + return resources_; + } + + constexpr benchmark_args queue_size(std::size_t value) const { + auto copy = *this; + copy.queue_size_ = value; + return copy; + } + + constexpr std::size_t queue_size() const { + return queue_size_; + } + +private: + std::size_t sequences_ = 0; + std::size_t threads_ = 0; + std::size_t resources_ = 0; + std::size_t queue_size_ = 0; +}; + +struct resource { + std::int64_t value = 0; +}; + +struct stub_mutex { + stub_mutex() = default; + stub_mutex(const stub_mutex&) = delete; + void lock() {} + void unlock() {} +}; + +struct single_thread {}; +struct multi_thread {}; + +template +struct context { + boost::asio::io_context io_context; + boost::asio::executor_work_guard guard = boost::asio::make_work_guard(io_context); + std::conditional_t, std::atomic_bool, bool> stop {false}; + time_traits::duration timeout {std::chrono::milliseconds(100)}; + std::vector durations; + std::mutex get_next_mutex; + std::condition_variable get_next; + std::unique_lock get_next_lock {get_next_mutex}; + std::conditional_t, std::atomic, std::int64_t> ready_count {0}; + + void wait_next() { + if (--ready_count < 0) { + get_next.wait(get_next_lock); + } + } + + void allow_next() { + ++ready_count; + if constexpr (std::is_same_v) { + get_next.notify_one(); + } + } + + void finish() { + stop = true; + guard.reset(); + } +}; + +template +struct callback { + using mutex_t = std::conditional_t, std::mutex, stub_mutex>; + using pool_t = async::pool; + using handle_t = typename pool_t::handle; + + context& ctx; + pool_t& pool; + + void operator ()(const boost::system::error_code& ec, handle_t handle) { + impl(ec, std::move(handle)); + if (!ctx.stop) { + pool.get_auto_waste(ctx.io_context, *this, ctx.timeout); + } + ctx.allow_next(); + } + + void impl(const boost::system::error_code& ec, handle_t handle) { + static thread_local std::minstd_rand generator(std::hash()(std::this_thread::get_id())); + static std::uniform_real_distribution<> distrubution(0, 1); + constexpr const double recycle_probability = 0.5; + if (!ec) { + if (handle.empty()) { + handle.reset(resource {}); + } + benchmark::DoNotOptimize(++handle->value); + if (distrubution(generator) < recycle_probability) { + handle.recycle(); + } + } + } +}; + +const std::array benchmarks{{ + benchmark_args().sequences(1).threads(1).resources(1).queue_size(0), // 0 + benchmark_args().sequences(2).threads(1).resources(1).queue_size(1), // 1 + benchmark_args().sequences(2).threads(1).resources(2).queue_size(0), // 2 + benchmark_args().sequences(10).threads(1).resources(10).queue_size(0), // 3 + benchmark_args().sequences(10).threads(1).resources(1).queue_size(9), // 4 + benchmark_args().sequences(10).threads(1).resources(5).queue_size(5), // 5 + benchmark_args().sequences(10).threads(1).resources(9).queue_size(1), // 6 + benchmark_args().sequences(10).threads(2).resources(5).queue_size(5), // 7 + benchmark_args().sequences(100).threads(1).resources(100).queue_size(0), // 8 + benchmark_args().sequences(100).threads(1).resources(10).queue_size(90), // 9 + benchmark_args().sequences(100).threads(1).resources(50).queue_size(50), // 10 + benchmark_args().sequences(100).threads(1).resources(90).queue_size(10), // 11 + benchmark_args().sequences(100).threads(2).resources(50).queue_size(50), // 12 + benchmark_args().sequences(1000).threads(1).resources(10).queue_size(990), // 13 + benchmark_args().sequences(1000).threads(2).resources(10).queue_size(990), // 14 + benchmark_args().sequences(10000).threads(1).resources(10).queue_size(9990), // 15 + benchmark_args().sequences(10000).threads(2).resources(10).queue_size(9990), // 16 +}}; + +void get_auto_waste_callbacks_st(benchmark::State& state) { + const auto& args = benchmarks[static_cast(state.range(0))]; + context ctx; + async::pool pool(args.resources(), args.queue_size()); + callback cb {ctx, pool}; + for (std::size_t i = 0; i < args.sequences(); ++i) { + pool.get_auto_waste(ctx.io_context, cb, ctx.timeout); + } + while (state.KeepRunning()) { + const auto ready_count = ctx.ready_count; + do { + ctx.io_context.run_one(); + } while (ready_count == ctx.ready_count); + } + ctx.finish(); +} + +struct thread_context { + context impl; + std::thread thread; + + thread_context() + : thread([this] { this->impl.io_context.run(); }) {} +}; + +void get_auto_waste_callbacks_mt(benchmark::State& state) { + const auto& args = benchmarks[static_cast(state.range(0))]; + std::vector> threads; + for (std::size_t i = 0; i < args.threads(); ++i) { + threads.emplace_back(std::make_unique()); + } + async::pool pool(args.resources(), args.queue_size()); + for (const auto& ctx : threads) { + callback cb {ctx->impl, pool}; + for (std::size_t i = 0; i < args.sequences(); ++i) { + pool.get_auto_waste(ctx->impl.io_context, cb, ctx->impl.timeout); + } + } + while (state.KeepRunning()) { + std::for_each(threads.begin(), threads.end(), [] (const auto& ctx) { ctx->impl.wait_next(); }); + } + std::for_each(threads.begin(), threads.end(), [] (const auto& ctx) { ctx->impl.finish(); }); + std::for_each(threads.begin(), threads.end(), [] (const auto& ctx) { ctx->thread.join(); }); +} + +void get_auto_waste_callbacks(benchmark::State& state) { + const auto& args = benchmarks[static_cast(state.range(0))]; + if (args.threads() > 1) { + get_auto_waste_callbacks_mt(state); + } else { + get_auto_waste_callbacks_st(state); + } +} + +void get_auto_waste_coroutines_st(benchmark::State& state) { + const auto& args = benchmarks[static_cast(state.range(0))]; + context ctx; + async::pool pool(args.resources(), args.queue_size()); + for (std::size_t i = 0; i < args.sequences(); ++i) { + boost::asio::spawn(ctx.io_context, [&] (boost::asio::yield_context yield) { + static thread_local std::minstd_rand generator(std::hash()(std::this_thread::get_id())); + std::uniform_real_distribution<> distrubution(0, 1); + constexpr const double recycle_probability = 0.5; + while (!ctx.stop) { + boost::system::error_code ec; + auto handle = pool.get_auto_waste(ctx.io_context, yield[ec], ctx.timeout); + if (!ec) { + if (handle.empty()) { + handle.reset(resource {}); + } + benchmark::DoNotOptimize(++handle->value); + if (distrubution(generator) < recycle_probability) { + handle.recycle(); + } + ctx.allow_next(); + } + } + }); + } + while (state.KeepRunning()) { + const auto ready_count = ctx.ready_count; + do { + ctx.io_context.run_one(); + } while (ready_count == ctx.ready_count); + } + ctx.finish(); +} + +void get_auto_waste_coroutines_mt(benchmark::State& state) { + const auto& args = benchmarks[static_cast(state.range(0))]; + std::vector> threads; + for (std::size_t i = 0; i < args.threads(); ++i) { + threads.emplace_back(std::make_unique()); + } + async::pool pool(args.resources(), args.queue_size()); + for (const auto& ctx : threads) { + for (std::size_t i = 0; i < args.sequences(); ++i) { + boost::asio::spawn(ctx->impl.io_context, [&] (boost::asio::yield_context yield) { + static thread_local std::minstd_rand generator(std::hash()(std::this_thread::get_id())); + std::uniform_real_distribution<> distrubution(0, 1); + constexpr const double recycle_probability = 0.5; + while (!ctx->impl.stop) { + boost::system::error_code ec; + auto handle = pool.get_auto_waste(ctx->impl.io_context, yield[ec], ctx->impl.timeout); + if (!ec) { + if (handle.empty()) { + handle.reset(resource {}); + } + benchmark::DoNotOptimize(++handle->value); + if (distrubution(generator) < recycle_probability) { + handle.recycle(); + } + ctx->impl.allow_next(); + } + } + }); + } + } + while (state.KeepRunning()) { + std::for_each(threads.begin(), threads.end(), [] (const auto& ctx) { ctx->impl.wait_next(); }); + } + std::for_each(threads.begin(), threads.end(), [] (const auto& ctx) { ctx->impl.finish(); }); + std::for_each(threads.begin(), threads.end(), [] (const auto& ctx) { ctx->thread.join(); }); +} + +void get_auto_waste_coroutines(benchmark::State& state) { + const auto& args = benchmarks[static_cast(state.range(0))]; + if (args.threads() > 1) { + get_auto_waste_coroutines_st(state); + } else { + get_auto_waste_coroutines_mt(state); + } +} + +void all_benchmarks(benchmark::internal::Benchmark* b) { + for (std::size_t n = 0; n < benchmarks.size(); ++n) { + b->Arg(static_cast(n)); + } +} + +} + +BENCHMARK(get_auto_waste_callbacks)->Apply(all_benchmarks); +BENCHMARK(get_auto_waste_coroutines)->Apply(all_benchmarks); + +BENCHMARK_MAIN(); diff --git a/contrib/resource_pool/cmake/CodeCoverage.cmake b/contrib/resource_pool/cmake/CodeCoverage.cmake new file mode 100644 index 000000000..2539ebdfe --- /dev/null +++ b/contrib/resource_pool/cmake/CodeCoverage.cmake @@ -0,0 +1,286 @@ +# Copyright (c) 2012 - 2017, Lars Bilke +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# CHANGES: +# +# 2012-01-31, Lars Bilke +# - Enable Code Coverage +# +# 2013-09-17, Joakim Söderberg +# - Added support for Clang. +# - Some additional usage instructions. +# +# 2016-02-03, Lars Bilke +# - Refactored functions to use named parameters +# +# 2017-06-02, Lars Bilke +# - Merged with modified version from github.com/ufz/ogs +# +# +# USAGE: +# +# 1. Copy this file into your cmake modules path. +# +# 2. Add the following line to your CMakeLists.txt: +# include(CodeCoverage) +# +# 3. Append necessary compiler flags: +# APPEND_COVERAGE_COMPILER_FLAGS() +# +# 4. If you need to exclude additional directories from the report, specify them +# using the COVERAGE_EXCLUDES variable before calling SETUP_TARGET_FOR_COVERAGE. +# Example: +# set(COVERAGE_EXCLUDES 'dir1/*' 'dir2/*') +# +# 5. Use the functions described below to create a custom make target which +# runs your test executable and produces a code coverage report. +# +# 6. Build a Debug build: +# cmake -DCMAKE_BUILD_TYPE=Debug .. +# make +# make my_coverage_target +# + +include(CMakeParseArguments) + +# Check prereqs +find_program( GCOV_PATH gcov ) +find_program( LCOV_PATH lcov ) +find_program( GENHTML_PATH genhtml ) +find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test) +find_program( SIMPLE_PYTHON_EXECUTABLE python ) + +if(NOT GCOV_PATH) + message(FATAL_ERROR "gcov not found! Aborting...") +endif() # NOT GCOV_PATH + +if("${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") + if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS 3) + message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...") + endif() +elseif(NOT CMAKE_COMPILER_IS_GNUCXX) + message(FATAL_ERROR "Compiler is not GNU gcc! Aborting...") +endif() + +set(COVERAGE_COMPILER_FLAGS "-g -O0 --coverage -fprofile-arcs -ftest-coverage" + CACHE INTERNAL "") + +set(CMAKE_CXX_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C++ compiler during coverage builds." + FORCE ) +set(CMAKE_C_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C compiler during coverage builds." + FORCE ) +set(CMAKE_EXE_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE ) +set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE ) +mark_as_advanced( + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) + +if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading") +endif() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug" + +if(CMAKE_C_COMPILER_ID STREQUAL "GNU") + link_libraries(gcov) +else() + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") +endif() + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# SETUP_TARGET_FOR_COVERAGE( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# ) +function(SETUP_TARGET_FOR_COVERAGE) + + set(options NONE) + set(oneValueArgs NAME) + set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT LCOV_PATH) + message(FATAL_ERROR "lcov not found! Aborting...") + endif() # NOT LCOV_PATH + + if(NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() # NOT GENHTML_PATH + + # Setup target + add_custom_target(${Coverage_NAME} + + # Cleanup lcov + COMMAND ${LCOV_PATH} --directory . --zerocounters + # Create baseline to make sure untouched files show up in the report + COMMAND ${LCOV_PATH} -c -i -d . -o ${Coverage_NAME}.base + + # Run tests + COMMAND ${Coverage_EXECUTABLE} + + # Capturing lcov counters and generating report + COMMAND ${LCOV_PATH} --directory . --capture --output-file ${Coverage_NAME}.info + # add baseline counters + COMMAND ${LCOV_PATH} -a ${Coverage_NAME}.base -a ${Coverage_NAME}.info --output-file ${Coverage_NAME}.total + COMMAND ${LCOV_PATH} --remove ${Coverage_NAME}.total ${COVERAGE_EXCLUDES} --output-file ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned + COMMAND ${GENHTML_PATH} -o ${Coverage_NAME} ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned + COMMAND ${CMAKE_COMMAND} -E remove ${Coverage_NAME}.base ${Coverage_NAME}.info ${Coverage_NAME}.total ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned + + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # SETUP_TARGET_FOR_COVERAGE + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# SETUP_TARGET_FOR_COVERAGE_COBERTURA( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# ) +function(SETUP_TARGET_FOR_COVERAGE_COBERTURA) + + set(options NONE) + set(oneValueArgs NAME) + set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT SIMPLE_PYTHON_EXECUTABLE) + message(FATAL_ERROR "python not found! Aborting...") + endif() # NOT SIMPLE_PYTHON_EXECUTABLE + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Combine excludes to several -e arguments + set(COBERTURA_EXCLUDES "") + foreach(EXCLUDE ${COVERAGE_EXCLUDES}) + set(COBERTURA_EXCLUDES "-e ${EXCLUDE} ${COBERTURA_EXCLUDES}") + endforeach() + + add_custom_target(${Coverage_NAME} + + # Run tests + ${Coverage_EXECUTABLE} + + # Running gcovr + COMMAND ${GCOVR_PATH} -x -r ${CMAKE_SOURCE_DIR} ${COBERTURA_EXCLUDES} + -o ${Coverage_NAME}.xml + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + COMMENT "Running gcovr to produce Cobertura code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml." + ) + +endfunction() # SETUP_TARGET_FOR_COVERAGE_COBERTURA + +function(SETUP_TARGET_FOR_COVERAGE_GCOVR) + + set(options NONE) + set(oneValueArgs NAME) + set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT SIMPLE_PYTHON_EXECUTABLE) + message(FATAL_ERROR "python not found! Aborting...") + endif() # NOT SIMPLE_PYTHON_EXECUTABLE + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}") + message(STATUS "GCOVR_EXCLUDES: ${GCOVR_EXCLUDES}") + + add_custom_target(${Coverage_NAME} + + # Run tests + ${Coverage_EXECUTABLE} + + # Cleanup + COMMAND rm -rf ${Coverage_NAME} + + # Prepare environment + COMMAND mkdir -p ${Coverage_NAME} + + # Running gcovr + COMMAND python2 ${GCOVR_PATH} + --html --html-details + -e "${COVERAGE_EXCLUDES}" + --root "${CMAKE_SOURCE_DIR}/" + --output "${CMAKE_BINARY_DIR}/${Coverage_NAME}/index.html" + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + COMMENT "Running gcovr to produce Cobertura code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Gcovr html code coverage report saved in ${Coverage_NAME}/index.html" + ) + +endfunction() # SETUP_TARGET_FOR_COVERAGE_GCOVR + +function(APPEND_COVERAGE_COMPILER_FLAGS) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}") +endfunction() # APPEND_COVERAGE_COMPILER_FLAGS diff --git a/contrib/resource_pool/cmake/Findbenchmark.cmake b/contrib/resource_pool/cmake/Findbenchmark.cmake new file mode 100644 index 000000000..f9208f845 --- /dev/null +++ b/contrib/resource_pool/cmake/Findbenchmark.cmake @@ -0,0 +1,38 @@ +# Findbenchmark.cmake +# - Try to find benchmark +# +# The following variables are optionally searched for defaults +# benchmark_ROOT_DIR: Base directory where all benchmark components are found +# +# Once done this will define +# benchmark_FOUND - System has benchmark +# benchmark_INCLUDE_DIRS - The benchmark include directories +# benchmark_LIBRARIES - The libraries needed to use benchmark + +set(benchmark_ROOT_DIR "" CACHE PATH "Folder containing benchmark") + +find_path(benchmark_INCLUDE_DIR "benchmark/benchmark.h" + PATHS ${benchmark_ROOT_DIR} + PATH_SUFFIXES include + NO_DEFAULT_PATH) +find_path(benchmark_INCLUDE_DIR "benchmark/benchmark.h") + +find_library(benchmark_LIBRARY NAMES "benchmark" + PATHS ${benchmark_ROOT_DIR} + PATH_SUFFIXES lib lib64 + NO_DEFAULT_PATH) +find_library(benchmark_LIBRARY NAMES "benchmark") + +include(FindPackageHandleStandardArgs) +# handle the QUIETLY and REQUIRED arguments and set benchmark_FOUND to TRUE +# if all listed variables are TRUE +find_package_handle_standard_args(benchmark FOUND_VAR benchmark_FOUND + REQUIRED_VARS benchmark_LIBRARY + benchmark_INCLUDE_DIR) + +if(benchmark_FOUND) + set(benchmark_LIBRARIES ${benchmark_LIBRARY}) + set(benchmark_INCLUDE_DIRS ${benchmark_INCLUDE_DIR}) +endif() + +mark_as_advanced(benchmark_INCLUDE_DIR benchmark_LIBRARY) diff --git a/contrib/resource_pool/conanfile.py b/contrib/resource_pool/conanfile.py new file mode 100644 index 000000000..b1e1f7051 --- /dev/null +++ b/contrib/resource_pool/conanfile.py @@ -0,0 +1,51 @@ +from conans import ConanFile, CMake +from conans.tools import load +import re + + +def get_version(): + try: + content = load("CMakeLists.txt") + version = re.search(r"^\s*project\(resource_pool\s+VERSION\s+([^\s]+)", content, re.M).group(1) + return version.strip() + except Exception: + return None + + +class ResourcePool(ConanFile): + name = 'resource_pool' + version = get_version() + license = 'MIT' + url = 'https://github.com/elsid/resource_pool' + description = 'Conan package for elsid resource pool' + + exports_sources = 'include/*', 'CMakeLists.txt', 'resource_poolConfig.cmake', 'LICENCE', 'AUTHORS' + + generators = 'cmake_find_package' + requires = 'boost/1.74.0' + + def _configure_cmake(self): + cmake = CMake(self) + cmake.configure() + return cmake + + def build(self): + cmake = self._configure_cmake() + cmake.build() + + def package(self): + cmake = self._configure_cmake() + cmake.install() + + def package_id(self): + self.info.header_only() + + def package_info(self): + self.cpp_info.components["_resource_pool"].includedirs = ["include"] + self.cpp_info.components["_resource_pool"].requires = ["boost::boost", "boost::system", "boost::thread"] + self.cpp_info.filenames["cmake_find_package"] = "resource_pool" + self.cpp_info.filenames["cmake_find_package_multi"] = "resource_pool" + self.cpp_info.names["cmake_find_package"] = "elsid" + self.cpp_info.names["cmake_find_package_multi"] = "elsid" + self.cpp_info.components["_resource_pool"].names["cmake_find_package"] = "resource_pool" + self.cpp_info.components["_resource_pool"].names["cmake_find_package_multi"] = "resource_pool" diff --git a/contrib/resource_pool/examples/CMakeLists.txt b/contrib/resource_pool/examples/CMakeLists.txt new file mode 100644 index 000000000..7619eb3bc --- /dev/null +++ b/contrib/resource_pool/examples/CMakeLists.txt @@ -0,0 +1,19 @@ +set(EXAMPLE_FLAGS -Wall -Wextra -pedantic -Werror) + +find_package(Boost REQUIRED COMPONENTS context system coroutine date_time atomic) + +add_executable(async_pool "async/pool.cc") +target_link_libraries(async_pool elsid::resource_pool) +target_compile_options(async_pool PRIVATE ${EXAMPLE_FLAGS}) + +add_executable(coro_pool "async/coro.cc") +target_link_libraries(coro_pool elsid::resource_pool Boost::coroutine Boost::context) +target_compile_options(coro_pool PRIVATE ${EXAMPLE_FLAGS}) + +add_executable(async_strand "async/strand.cc") +target_link_libraries(async_strand elsid::resource_pool) +target_compile_options(async_strand PRIVATE ${EXAMPLE_FLAGS}) + +add_executable(sync_pool "sync/pool.cc") +target_link_libraries(sync_pool elsid::resource_pool) +target_compile_options(sync_pool PRIVATE ${EXAMPLE_FLAGS}) diff --git a/contrib/resource_pool/examples/async/coro.cc b/contrib/resource_pool/examples/async/coro.cc new file mode 100644 index 000000000..719010846 --- /dev/null +++ b/contrib/resource_pool/examples/async/coro.cc @@ -0,0 +1,37 @@ +#include + +#include +#include +#include +#include + +using ofstream_pool = yamail::resource_pool::async::pool>; +using time_traits = yamail::resource_pool::time_traits; + +int main() { + boost::asio::io_context service; + ofstream_pool pool(1, 10); + boost::asio::spawn(service, [&](boost::asio::yield_context yield){ + boost::system::error_code ec; + auto handle = pool.get_auto_waste(service, yield[ec], time_traits::duration::max()); + if (ec) { + std::cout << "handle error: " << ec.message() << std::endl; + return; + } + std::cout << "got resource handle" << std::endl; + if (handle.empty()) { + auto file = std::make_unique("pool.log", std::ios::app); + if (!file->good()) { + std::cout << "open file pool.log error: " << file->rdstate() << std::endl; + return; + } + handle.reset(std::move(file)); + } + *(handle.get()) << (time_traits::time_point::min() - time_traits::now()).count() << std::endl; + if (handle.get()->good()) { + handle.recycle(); + } + }); + service.run(); + return 0; +} diff --git a/contrib/resource_pool/examples/async/pool.cc b/contrib/resource_pool/examples/async/pool.cc new file mode 100644 index 000000000..a075964c8 --- /dev/null +++ b/contrib/resource_pool/examples/async/pool.cc @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include + +using ofstream_pool = yamail::resource_pool::async::pool>; +using time_traits = yamail::resource_pool::time_traits; + +struct on_get { + void operator ()(const boost::system::error_code& ec, ofstream_pool::handle handle) { + try { + if (ec) { + std::cerr << ec.message() << std::endl; + return; + } + std::cout << "got resource handle" << std::endl; + if (handle.empty()) { + auto file = std::make_unique("pool.log", std::ios::app); + if (!file->good()) { + std::cout << "open file pool.log error: " << file->rdstate() << std::endl; + return; + } + handle.reset(std::move(file)); + } + *(handle.get()) << (time_traits::time_point::min() - time_traits::now()).count() << std::endl; + if (handle.get()->good()) { + handle.recycle(); + } + } catch (const std::exception& exception) { + std::cerr << exception.what() << std::endl; + return; + } + } +}; + +int main() { + boost::asio::io_context service; + ofstream_pool pool(1, 10); + pool.get_auto_waste(service, on_get(), time_traits::duration::max()); + std::thread worker([&] { return service.run(); }); + worker.join(); + return 0; +} diff --git a/contrib/resource_pool/examples/async/strand.cc b/contrib/resource_pool/examples/async/strand.cc new file mode 100644 index 000000000..a2c299c49 --- /dev/null +++ b/contrib/resource_pool/examples/async/strand.cc @@ -0,0 +1,58 @@ +#include + +#include +#include +#include +#include + +using ofstream_pool = yamail::resource_pool::async::pool>; +using time_traits = yamail::resource_pool::time_traits; + +struct on_get { + using executor_type = boost::asio::io_context::strand; + + boost::asio::io_context::strand& strand; + + void operator ()(const boost::system::error_code& ec, ofstream_pool::handle handle) { + try { + if (ec) { + std::cerr << ec.message() << std::endl; + return; + } + std::cout << "got resource handle" << std::endl; + if (handle.empty()) { + auto file = std::make_unique("pool.log", std::ios::app); + if (!file->good()) { + std::cout << "open file pool.log error: " << file->rdstate() << std::endl; + return; + } + handle.reset(std::move(file)); + } + *(handle.get()) << (time_traits::time_point::min() - time_traits::now()).count() << std::endl; + if (handle.get()->good()) { + handle.recycle(); + } + } catch (const std::exception& exception) { + std::cerr << exception.what() << std::endl; + return; + } + } + + executor_type get_executor() const noexcept { + return strand; + } +}; + +int main() { + boost::asio::io_context service; + boost::asio::io_context::strand strand(service); + ofstream_pool pool(2, 10); + pool.get_auto_waste(service, on_get {strand}, time_traits::duration::max()); + pool.get_auto_waste(service, on_get {strand}, time_traits::duration::max()); + pool.get_auto_waste(service, on_get {strand}, time_traits::duration::max()); + std::thread worker1([&] { return service.run(); }); + std::thread worker2([&] { return service.run(); }); + worker1.join(); + worker2.join(); + return 0; +} diff --git a/contrib/resource_pool/examples/sync/pool.cc b/contrib/resource_pool/examples/sync/pool.cc new file mode 100644 index 000000000..b50ce8c0d --- /dev/null +++ b/contrib/resource_pool/examples/sync/pool.cc @@ -0,0 +1,31 @@ +#include + +#include +#include +#include + +using ofstream_pool = yamail::resource_pool::sync::pool>; +using time_traits = yamail::resource_pool::time_traits; + +int main() { + ofstream_pool pool(1); + auto res = pool.get_auto_recycle(time_traits::duration::max()); + auto& ec = res.first; + if (ec) { + std::cerr << ec.message() << std::endl; + return -1; + } + auto& handle = res.second; + if (handle.empty()) { + std::unique_ptr file; + try { + file.reset(new std::ofstream("pool.log", std::ios::app)); + } catch (const std::exception& exception) { + std::cerr << "Open file pool.log error: " << exception.what() << std::endl; + return -1; + } + handle.reset(std::move(file)); + } + *(handle.get()) << (time_traits::time_point::min() - time_traits::now()).count() << std::endl; + return 0; +} diff --git a/contrib/resource_pool/include/CMakeLists.txt b/contrib/resource_pool/include/CMakeLists.txt new file mode 100644 index 000000000..a35cad48c --- /dev/null +++ b/contrib/resource_pool/include/CMakeLists.txt @@ -0,0 +1,43 @@ +find_package(Boost 1.74 REQUIRED COMPONENTS system thread REQUIRED) + +add_library(resource_pool INTERFACE) +target_compile_features(resource_pool INTERFACE cxx_std_17) +target_include_directories(resource_pool + INTERFACE + $ + $) +target_link_libraries(resource_pool INTERFACE Boost::system Boost::thread Boost::boost) +target_compile_definitions(resource_pool + INTERFACE + -DBOOST_COROUTINES_NO_DEPRECATION_WARNING + ) +add_library(elsid::resource_pool ALIAS resource_pool) + +install(TARGETS resource_pool EXPORT resource_poolTargets + LIBRARY DESTINATION lib COMPONENT Runtime + ARCHIVE DESTINATION lib COMPONENT Development + RUNTIME DESTINATION bin COMPONENT Runtime + PUBLIC_HEADER DESTINATION include COMPONENT Development + BUNDLE DESTINATION bin COMPONENT Runtime) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DESTINATION .) + +include(CMakePackageConfigHelpers) +write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/resource_pool/resource_poolConfigVersion.cmake" + COMPATIBILITY AnyNewerVersion) + +export(EXPORT resource_poolTargets + FILE "${CMAKE_CURRENT_BINARY_DIR}/resource_pool/resource_poolTargets.cmake" + NAMESPACE elsid::) +configure_file(../resource_poolConfig.cmake + "${CMAKE_CURRENT_BINARY_DIR}/resource_pool/resource_poolConfig.cmake" + COPYONLY) + +set(ConfigPackageLocation lib/cmake/resource_pool) +install(EXPORT resource_poolTargets + FILE resource_poolTargets.cmake + NAMESPACE elsid:: + DESTINATION ${ConfigPackageLocation}) +install(FILES ../resource_poolConfig.cmake + "${CMAKE_CURRENT_BINARY_DIR}/resource_pool/resource_poolConfigVersion.cmake" + DESTINATION ${ConfigPackageLocation} + COMPONENT Devel) diff --git a/contrib/resource_pool/include/yamail/resource_pool.hpp b/contrib/resource_pool/include/yamail/resource_pool.hpp new file mode 100644 index 000000000..c27516a0d --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool.hpp @@ -0,0 +1,7 @@ +#ifndef YAMAIL_RESOURCE_POOL_HPP +#define YAMAIL_RESOURCE_POOL_HPP + +#include +#include + +#endif diff --git a/contrib/resource_pool/include/yamail/resource_pool/async/detail/pool_impl.hpp b/contrib/resource_pool/include/yamail/resource_pool/async/detail/pool_impl.hpp new file mode 100644 index 000000000..7ef4f3a51 --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/async/detail/pool_impl.hpp @@ -0,0 +1,439 @@ +#ifndef YAMAIL_RESOURCE_POOL_ASYNC_DETAIL_POOL_IMPL_HPP +#define YAMAIL_RESOURCE_POOL_ASYNC_DETAIL_POOL_IMPL_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace yamail { +namespace resource_pool { +namespace async { + +struct stats { + std::size_t size; + std::size_t available; + std::size_t used; + std::size_t queue_size; +}; + +namespace detail { + +namespace asio = boost::asio; + +using resource_pool::detail::cell_iterator; +using resource_pool::detail::cell_value; +using resource_pool::detail::pool_returns; + +template +class on_list_iterator_handler { + static_assert(std::is_invocable_v>); + + boost::system::error_code error; + cell_iterator list_iterator; + Handler handler; + +public: + using executor_type = std::decay_t; + + on_list_iterator_handler() = default; + + template + on_list_iterator_handler(boost::system::error_code error, cell_iterator list_iterator, HandlerT&& handler) + : error(error), + list_iterator(list_iterator), + handler(std::forward(handler)) {} + + template + void operator ()() { + return handler(error, list_iterator); + } + + auto get_executor() const noexcept { + return asio::get_associated_executor(handler); + } +}; + +template +on_list_iterator_handler(boost::system::error_code, ListIterator, Handler&&) + -> on_list_iterator_handler, std::decay_t>; + +template +struct base_list_iterator_handler_impl { + virtual void operator ()(boost::system::error_code ec, cell_iterator iterator) = 0; + virtual ~base_list_iterator_handler_impl() = default; +}; + +template +class list_iterator_handler_impl final : public base_list_iterator_handler_impl { + static_assert(std::is_invocable_v>); + +public: + template + list_iterator_handler_impl(HandlerT&& handler, + std::enable_if_t, list_iterator_handler_impl>, void*> = nullptr) + : handler(std::forward(handler)) { + static_assert(std::is_same_v, Handler>, "HandlerT is not Handler"); + } + + void operator ()(boost::system::error_code ec, cell_iterator iterator) final { + handler(ec, iterator); + } + +private: + Handler handler; +}; + +template +class list_iterator_handler { +public: + using executor_type = asio::any_io_executor; + + list_iterator_handler() = default; + + template + list_iterator_handler(Handler&& handler, + std::enable_if_t, list_iterator_handler>, void*> = nullptr) + : executor(asio::get_associated_executor(handler)), + impl(std::make_unique>>(std::forward(handler))) { + } + + void operator ()(boost::system::error_code ec, cell_iterator iterator) { + (*impl)(ec, iterator); + } + + void operator ()(boost::system::error_code ec) { + (*impl)(ec, cell_iterator()); + } + + void operator ()(cell_iterator iterator) { + (*impl)(boost::system::error_code(), iterator); + } + + auto get_executor() const noexcept { + return executor; + } + +private: + asio::any_io_executor executor; + std::unique_ptr> impl; +}; + +template +class on_error_handler { + static_assert(std::is_invocable_v); + + boost::system::error_code error; + Handler handler; + +public: + using executor_type = std::decay_t; + + template + on_error_handler(boost::system::error_code error, HandlerT&& handler) + : error(error), + handler(std::forward(handler)) { + static_assert(std::is_same_v, Handler>, "HandlerT is not Handler"); + } + + void operator ()() { + return handler(error); + } + + auto get_executor() const noexcept { + return asio::get_associated_executor(handler); + } +}; + +template +on_error_handler(boost::system::error_code, Handler&&) -> on_error_handler>; + +template +class on_serve_queued_handler { + static_assert(std::is_invocable_v>); + + cell_iterator list_iterator; + Handler handler; + +public: + using executor_type = std::decay_t; + + template + on_serve_queued_handler(cell_iterator list_iterator, HandlerT&& handler) + : list_iterator(list_iterator), + handler(std::forward(handler)) { + static_assert(std::is_same_v, Handler>, "HandlerT is not Handler"); + } + + void operator ()() { + return handler(list_iterator); + } + + auto get_executor() const noexcept { + return asio::get_associated_executor(handler); + } +}; + +template +on_serve_queued_handler(ListIterator, Handler&&) + -> on_serve_queued_handler, std::decay_t>; + +template +class pool_impl : public pool_returns { +public: + using value_type = Value; + using io_context_t = IoContext; + using idle = resource_pool::detail::idle; + using storage_type = resource_pool::detail::storage; + using list_iterator = typename storage_type::cell_iterator; + using queue_type = Queue; + + pool_impl(std::size_t capacity, + std::size_t queue_capacity, + time_traits::duration idle_timeout, + time_traits::duration lifespan) + : storage_(assert_capacity(capacity), idle_timeout, lifespan), + _capacity(capacity), + _callbacks(std::make_shared(queue_capacity)) { + } + + template + pool_impl(Generator&& gen_value, + std::size_t capacity, + std::size_t queue_capacity, + time_traits::duration idle_timeout, + time_traits::duration lifespan) + : storage_(std::forward(gen_value), assert_capacity(capacity), idle_timeout, lifespan), + _capacity(assert_capacity(capacity)), + _callbacks(std::make_shared(queue_capacity)) { + } + + template + pool_impl(Iter first, Iter last, + std::size_t queue_capacity, + time_traits::duration idle_timeout, + time_traits::duration lifespan) + : pool_impl([&]{ return std::move(*first++); }, + static_cast(std::distance(first, last)), + queue_capacity, + idle_timeout, + lifespan) { + } + + pool_impl(const pool_impl&) = delete; + + pool_impl(pool_impl&&) = delete; + + std::size_t capacity() const noexcept { return _capacity; } + std::size_t size() const noexcept; + std::size_t available() const noexcept; + std::size_t used() const noexcept; + async::stats stats() const noexcept; + + const queue_type& queue() const noexcept { return *_callbacks; } + + template + void get(io_context_t& io_context, Handler&& handler, time_traits::duration wait_duration = time_traits::duration(0)); + void recycle(list_iterator res_it) final; + void waste(list_iterator res_it) final; + void disable(); + void invalidate(); + + static std::size_t assert_capacity(std::size_t value); + +private: + using mutex_t = Mutex; + using unique_lock = std::unique_lock; + using lock_guard = std::lock_guard; + + mutable mutex_t _mutex; + storage_type storage_; + const std::size_t _capacity; + std::shared_ptr _callbacks; + bool _disabled = false; +}; + +template +std::size_t pool_impl::size() const noexcept { + const auto stats = [&] { + const lock_guard lock(_mutex); + return storage_.stats(); + } (); + return stats.available + stats.used; +} + +template +std::size_t pool_impl::available() const noexcept { + const lock_guard lock(_mutex); + return storage_.stats().available; +} + +template +std::size_t pool_impl::used() const noexcept { + const lock_guard lock(_mutex); + return storage_.stats().used; +} + +template +async::stats pool_impl::stats() const noexcept { + const auto stats = [&] { + const lock_guard lock(_mutex); + return storage_.stats(); + } (); + async::stats result; + result.size = stats.available + stats.used; + result.available = stats.available; + result.used = stats.used; + result.queue_size = _callbacks->size(); + return result; +} + +template +void pool_impl::recycle(list_iterator res_it) { + unique_lock lock(_mutex); + auto queued = _callbacks->pop(); + if (!queued) { + storage_.recycle(res_it); + return; + } + const auto valid = storage_.is_valid(res_it); + lock.unlock(); + if (!valid) { + res_it->value.reset(); + } + asio::post(queued->io_context.get_executor(), on_serve_queued_handler(res_it, std::move(queued->request))); +} + +template +void pool_impl::waste(list_iterator res_it) { + unique_lock lock(_mutex); + auto queued = _callbacks->pop(); + if (!queued) { + storage_.waste(res_it); + return; + } + lock.unlock(); + res_it->value.reset(); + asio::post(queued->io_context.get_executor(), on_serve_queued_handler(res_it, std::move(queued->request))); +} + +template +template +void pool_impl::get(io_context_t& io_context, Handler&& handler, time_traits::duration wait_duration) { + static_assert(std::is_invocable_v, boost::system::error_code, list_iterator>); + + unique_lock lock(_mutex); + if (_disabled) { + lock.unlock(); + asio::dispatch(io_context.get_executor(), + on_list_iterator_handler( + make_error_code(error::disabled), + list_iterator(), + std::forward(handler) + )); + return; + } + if (const auto cell = storage_.lease()) { + lock.unlock(); + asio::post(io_context.get_executor(), + on_list_iterator_handler( + boost::system::error_code(), + *cell, + std::forward(handler) + )); + return; + } + lock.unlock(); + if (wait_duration.count() == 0) { + asio::post(io_context.get_executor(), + on_list_iterator_handler( + make_error_code(error::get_resource_timeout), + list_iterator(), + std::forward(handler) + )); + return; + } + list_iterator_handler wrapped(std::forward(handler)); + const bool pushed = _callbacks->push(io_context, wait_duration, std::move(wrapped)); + if (pushed) { + return; + } + asio::post(io_context.get_executor(), + on_error_handler( + make_error_code(error::request_queue_overflow), + std::move(wrapped) + )); +} + +template +void pool_impl::disable() { + const lock_guard lock(_mutex); + _disabled = true; + while (true) { + auto queued = _callbacks->pop(); + if (!queued) { + break; + } + asio::dispatch(queued->io_context.get_executor(), + on_error_handler( + make_error_code(error::disabled), + std::move(queued->request) + )); + } +} + +template +void pool_impl::invalidate() { + const lock_guard lock(_mutex); + storage_.invalidate(); +} + +template +std::size_t pool_impl::assert_capacity(std::size_t value) { + if (value == 0) { + throw error::zero_pool_capacity(); + } + return value; +} + +using boost::asio::async_result; +template +struct handler_type { + using type = typename boost::asio::async_result::completion_handler_type; +}; + +#if BOOST_VERSION < 106600 +template +struct async_completion { + explicit async_completion(CompletionToken& token) + : completion_handler(std::move(token)), + result(completion_handler) { + } + + using completion_handler_type = typename handler_type::type; + + completion_handler_type completion_handler; + async_result result; +}; +#else +using boost::asio::async_completion; +#endif + +template +using async_return_type = typename ::boost::asio::async_result::return_type; + +} // namespace detail +} // namespace async +} // namespace resource_pool +} // namespace yamail + +#endif // YAMAIL_RESOURCE_POOL_ASYNC_DETAIL_POOL_IMPL_HPP diff --git a/contrib/resource_pool/include/yamail/resource_pool/async/detail/queue.hpp b/contrib/resource_pool/include/yamail/resource_pool/async/detail/queue.hpp new file mode 100644 index 000000000..bb6cf8fdc --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/async/detail/queue.hpp @@ -0,0 +1,225 @@ +#ifndef YAMAIL_RESOURCE_POOL_ASYNC_DETAIL_QUEUE_HPP +#define YAMAIL_RESOURCE_POOL_ASYNC_DETAIL_QUEUE_HPP + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace yamail { +namespace resource_pool { +namespace async { + +namespace asio = boost::asio; + +namespace detail { + +using clock = std::chrono::steady_clock; + +template +class expired_handler { + Handler handler; + +public: + using executor_type = std::decay_t; + + expired_handler() = default; + + template + explicit expired_handler(HandlerT&& handler, + std::enable_if_t, expired_handler>, void*> = nullptr) + : handler(std::forward(handler)) { + static_assert(std::is_same_v, Handler>, "HandlerT is not Handler"); + } + + void operator ()() { + handler(make_error_code(error::get_resource_timeout)); + } + + void operator ()() const { + handler(make_error_code(error::get_resource_timeout)); + } + + auto get_executor() const noexcept { + return asio::get_associated_executor(handler); + } +}; + +template +expired_handler(Handler&&) -> expired_handler>; + +template +struct queued_value { + Value request; + IoContext& io_context; +}; + +template +class queue : public std::enable_shared_from_this> { +public: + using value_type = Value; + using io_context_t = IoContext; + using timer_t = Timer; + using queued_value_t = queued_value; + + queue(std::size_t capacity) : _capacity(capacity) {} + + queue(const queue&) = delete; + + queue(queue&&) = delete; + + std::size_t capacity() const noexcept { return _capacity; } + std::size_t size() const noexcept; + bool empty() const noexcept; + const timer_t& timer(io_context_t& io_context); + + bool push(io_context_t& io_context, time_traits::duration wait_duration, value_type&& request); + boost::optional pop(); + +private: + using mutex_t = Mutex; + using lock_guard = std::lock_guard; + + struct expiring_request { + using list = std::list; + using list_it = typename list::iterator; + using multimap = std::multimap; + using multimap_it = typename multimap::iterator; + + io_context_t* io_context; + queue::value_type request; + list_it order_it; + multimap_it expires_at_it; + + expiring_request() = default; + }; + + using request_multimap_value = typename expiring_request::multimap::value_type; + using timers_map = typename std::unordered_map; + + const std::size_t _capacity; + mutable mutex_t _mutex; + typename expiring_request::list _ordered_requests_pool; + typename expiring_request::list _ordered_requests; + typename expiring_request::multimap _expires_at_requests; + timers_map _timers; + + bool fit_capacity() const { return _expires_at_requests.size() < _capacity; } + void cancel(boost::system::error_code ec, time_traits::time_point expires_at); + void update_timer(); + timer_t& get_timer(io_context_t& io_context); +}; + +template +std::size_t queue::size() const noexcept { + const lock_guard lock(_mutex); + return _expires_at_requests.size(); +} + +template +bool queue::empty() const noexcept { + const lock_guard lock(_mutex); + return _ordered_requests.empty(); +} + +template +const typename queue::timer_t& queue::timer(io_context_t& io_context) { + const lock_guard lock(_mutex); + return get_timer(io_context); +} + +template +bool queue::push(io_context_t& io_context, time_traits::duration wait_duration, value_type&& request) { + const lock_guard lock(_mutex); + if (!fit_capacity()) { + return false; + } + if (_ordered_requests_pool.empty()) { + _ordered_requests_pool.emplace_back(); + } + const auto order_it = _ordered_requests_pool.begin(); + _ordered_requests.splice(_ordered_requests.end(), _ordered_requests_pool, order_it); + expiring_request& req = *order_it; + req.io_context = std::addressof(io_context); + req.request = std::move(request); + req.order_it = order_it; + const auto expires_at = time_traits::add(time_traits::now(), wait_duration); + req.expires_at_it = _expires_at_requests.insert(std::make_pair(expires_at, &req)); + update_timer(); + return true; +} + +template +boost::optional::queued_value_t> queue::pop() { + const lock_guard lock(_mutex); + if (_ordered_requests.empty()) { + return {}; + } + const auto ordered_it = _ordered_requests.begin(); + expiring_request& req = *ordered_it; + queued_value_t result {std::move(req.request), *req.io_context}; + _expires_at_requests.erase(req.expires_at_it); + _ordered_requests_pool.splice(_ordered_requests_pool.begin(), _ordered_requests, ordered_it); + update_timer(); + return { std::move(result) }; +} + +template +void queue::cancel(boost::system::error_code ec, time_traits::time_point expires_at) { + if (ec) { + return; + } + const lock_guard lock(_mutex); + const auto begin = _expires_at_requests.begin(); + const auto end = _expires_at_requests.upper_bound(expires_at); + std::for_each(begin, end, [&] (request_multimap_value& v) { + const auto req = v.second; + asio::post(req->io_context->get_executor(), expired_handler(std::move(req->request))); + _ordered_requests_pool.splice(_ordered_requests_pool.begin(), _ordered_requests, req->order_it); + }); + _expires_at_requests.erase(_expires_at_requests.begin(), end); + update_timer(); +} + +template +void queue::update_timer() { + using timers_map_value = typename timers_map::value_type; + if (_expires_at_requests.empty()) { + std::for_each(_timers.begin(), _timers.end(), [] (timers_map_value& v) { v.second.cancel(); }); + _timers.clear(); + return; + } + const auto earliest_expire = _expires_at_requests.begin(); + const auto expires_at = earliest_expire->first; + auto& timer = get_timer(*earliest_expire->second->io_context); + timer.expires_at(expires_at); + std::weak_ptr weak(this->shared_from_this()); + timer.async_wait([weak, expires_at] (boost::system::error_code ec) { + if (const auto locked = weak.lock()) { + locked->cancel(ec, expires_at); + } + }); +} + +template +typename queue::timer_t& queue::get_timer(io_context_t& io_context) { + auto it = _timers.find(&io_context); + if (it != _timers.end()) { + return it->second; + } + return _timers.emplace(&io_context, timer_t(io_context)).first->second; +} + +} // namespace detail +} // namespace async +} // namespace resource_pool +} // namespace yamail + +#endif // YAMAIL_RESOURCE_POOL_ASYNC_DETAIL_QUEUE_HPP diff --git a/contrib/resource_pool/include/yamail/resource_pool/async/pool.hpp b/contrib/resource_pool/include/yamail/resource_pool/async/pool.hpp new file mode 100644 index 000000000..d002493de --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/async/pool.hpp @@ -0,0 +1,187 @@ +#ifndef YAMAIL_RESOURCE_POOL_ASYNC_POOL_HPP +#define YAMAIL_RESOURCE_POOL_ASYNC_POOL_HPP + +#include +#include +#include + +#include +#include + +namespace yamail { +namespace resource_pool { +namespace async { + +namespace asio = boost::asio; + +template +struct default_pool_queue { + using value_type = Value; + using io_context_t = IoContext; + using mutex_t = Mutex; + using idle = resource_pool::detail::idle; + using list = std::list; + using list_iterator = typename list::iterator; + using type = detail::queue, mutex_t, io_context_t, time_traits::timer>; +}; + +template +struct default_pool_impl { + using type = typename detail::pool_impl< + Value, + Mutex, + IoContext, + typename default_pool_queue::type + >; +}; + +template ::type > +class pool { +public: + using value_type = Value; + using io_context_t = IoContext; + using pool_impl = Impl; + using handle = resource_pool::handle; + + pool(std::size_t capacity, + std::size_t queue_capacity, + time_traits::duration idle_timeout = time_traits::duration::max(), + time_traits::duration lifespan = time_traits::duration::max()) + : _impl(std::make_shared( + capacity, + queue_capacity, + idle_timeout, + lifespan)) {} + + template + pool(Generator&& gen_value, + std::size_t capacity, + std::size_t queue_capacity, + time_traits::duration idle_timeout = time_traits::duration::max(), + time_traits::duration lifespan = time_traits::duration::max()) + : _impl(std::make_shared( + std::forward(gen_value), + capacity, + queue_capacity, + idle_timeout, + lifespan)) {} + + template + pool(Iter first, Iter last, + std::size_t queue_capacity, + time_traits::duration idle_timeout = time_traits::duration::max(), + time_traits::duration lifespan = time_traits::duration::max()) + : _impl(std::make_shared( + first, last, + queue_capacity, + idle_timeout, + lifespan)) {} + + pool(std::shared_ptr impl) + : _impl(std::move(impl)) {} + + pool(const pool&) = delete; + pool(pool&&) = default; + + ~pool() { + if (_impl) { + _impl->disable(); + } + } + + pool& operator =(const pool&) = delete; + pool& operator =(pool&&) = default; + + std::size_t capacity() const noexcept { return _impl->capacity(); } + std::size_t size() const noexcept { return _impl->size(); } + std::size_t available() const noexcept { return _impl->available(); } + std::size_t used() const noexcept { return _impl->used(); } + async::stats stats() const noexcept { return _impl->stats(); } + + const pool_impl& impl() const noexcept { return *_impl; } + + template + auto get_auto_waste(io_context_t& io_context, CompletionToken&& token, + time_traits::duration wait_duration = time_traits::duration(0)) { + return asio::async_initiate( + [this, &io_context, wait_duration] (auto&& handler) { + get(io_context, std::forward(handler), &handle::waste, wait_duration); + }, + token + ); + } + + template + auto get_auto_recycle(io_context_t& io_context, CompletionToken&& token, + time_traits::duration wait_duration = time_traits::duration(0)) { + return asio::async_initiate( + [this, &io_context, wait_duration] (auto&& handler) { + get(io_context, std::forward(handler), &handle::recycle, wait_duration); + }, + token + ); + } + + void invalidate() { + _impl->invalidate(); + } + +private: + using list_iterator = typename pool_impl::list_iterator; + + template + class on_get_handler { + std::shared_ptr impl; + UseStrategy use_strategy; + Handler handler; + + public: + using executor_type = std::decay_t; + + template + on_get_handler(std::shared_ptr impl, UseStrategy use_strategy, HandlerT&& handler) + : impl(std::move(impl)), + use_strategy(std::move(use_strategy)), + handler(std::forward(handler)) { + static_assert(std::is_same, Handler>::value, "HandlerT is not Handler"); + } + + void operator ()(boost::system::error_code ec, list_iterator res) { + if (ec) { + handler(ec, handle()); + } else { + handler(ec, handle(impl, use_strategy, std::move(res))); + } + } + + auto get_executor() const noexcept { + return asio::get_associated_executor(handler); + } + }; + + template + auto make_on_get_handler(UseStrategy&& use_strategy, Handler&& handler) { + using result_type = on_get_handler, std::decay_t>; + return result_type(_impl, std::forward(use_strategy), std::forward(handler)); + } + + std::shared_ptr _impl; + + template + void get(io_context_t &io_context, Handler&& handler, UseStrategy&& use_strategy, time_traits::duration wait_duration) { + _impl->get( + io_context, + make_on_get_handler(std::forward(use_strategy), std::forward(handler)), + wait_duration + ); + } +}; + +} // namespace async +} // namespace resource_pool +} // namespace yamail + +#endif // YAMAIL_RESOURCE_POOL_ASYNC_POOL_HPP diff --git a/contrib/resource_pool/include/yamail/resource_pool/detail/idle.hpp b/contrib/resource_pool/include/yamail/resource_pool/detail/idle.hpp new file mode 100644 index 000000000..ca3e37c6c --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/detail/idle.hpp @@ -0,0 +1,31 @@ +#ifndef YAMAIL_RESOURCE_POOL_DETAIL_IDLE_HPP +#define YAMAIL_RESOURCE_POOL_DETAIL_IDLE_HPP + +#include + +#include + +namespace yamail { +namespace resource_pool { +namespace detail { + +template +struct idle { + using value_type = Value; + + boost::optional value; + time_traits::time_point drop_time; + time_traits::time_point reset_time; + bool waste_on_recycle = false; + + idle(time_traits::time_point drop_time = time_traits::time_point::max()) + : drop_time(drop_time) {} + idle(value_type&& value, time_traits::time_point drop_time, time_traits::time_point reset_time) + : value(std::move(value)), drop_time(drop_time), reset_time(reset_time) {} +}; + +} // namespace detail +} // namespace resource_pool +} // namespace yamail + +#endif // YAMAIL_RESOURCE_POOL_DETAIL_IDLE_HPP diff --git a/contrib/resource_pool/include/yamail/resource_pool/detail/pool_returns.hpp b/contrib/resource_pool/include/yamail/resource_pool/detail/pool_returns.hpp new file mode 100644 index 000000000..38b513722 --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/detail/pool_returns.hpp @@ -0,0 +1,23 @@ +#ifndef YAMAIL_RESOURCE_POOL_DETAIL_POOL_RETURNS_HPP +#define YAMAIL_RESOURCE_POOL_DETAIL_POOL_RETURNS_HPP + +#include + +namespace yamail { +namespace resource_pool { +namespace detail { + +template +struct pool_returns { + virtual ~pool_returns() = default; + + virtual void waste(cell_iterator resource_iterator) = 0; + + virtual void recycle(cell_iterator resource_iterator) = 0; +}; + +} // namespace detail +} // namespace resource_pool +} // namespace yamail + +#endif diff --git a/contrib/resource_pool/include/yamail/resource_pool/detail/storage.hpp b/contrib/resource_pool/include/yamail/resource_pool/detail/storage.hpp new file mode 100644 index 000000000..cd1fcfe63 --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/detail/storage.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace yamail { +namespace resource_pool { +namespace detail { + +struct storage_stats { + std::size_t available; + std::size_t used; + std::size_t wasted; +}; + +template +class storage { +public: + using cell_iterator = typename std::list>::iterator; + using const_cell_iterator = typename std::list>::iterator; + + inline storage(std::size_t capacity, time_traits::duration idle_timeout, time_traits::duration lifespan); + + template + inline storage(Generator&& generator, std::size_t capacity, time_traits::duration idle_timeout, time_traits::duration lifespan); + + template + inline storage(InputIterator begin, InputIterator end, time_traits::duration idle_timeout, time_traits::duration lifespan); + + storage(const storage& other) = delete; + + storage(storage&& other) = default; + + inline storage_stats stats() const; + + inline boost::optional lease(); + + inline void recycle(cell_iterator cell); + + inline void waste(cell_iterator cell); + + inline bool is_valid(const_cell_iterator cell) const; + + inline void invalidate(); + +private: + time_traits::duration idle_timeout_; + time_traits::duration lifespan_; + std::list> available_; + std::list> used_; + std::list> wasted_; +}; + +template +using cell_iterator = typename storage::cell_iterator; + +template +using cell_value = typename CellIterator::value_type::value_type; + +template +storage::storage(std::size_t capacity, time_traits::duration idle_timeout, time_traits::duration lifespan) + : idle_timeout_(idle_timeout), + lifespan_(lifespan), + wasted_(capacity) { +} + +template +template +storage::storage(Generator&& generator, std::size_t capacity, time_traits::duration idle_timeout, time_traits::duration lifespan) + : idle_timeout_(idle_timeout), lifespan_(lifespan) { + const auto now = time_traits::now(); + const auto drop_time = std::min(time_traits::add(now, idle_timeout_), time_traits::add(now, lifespan_)); + for (std::size_t i = 0; i < capacity; ++i) { + available_.emplace_back(generator(), drop_time, now); + } +} + +template +template +storage::storage(InputIterator begin, InputIterator end, time_traits::duration idle_timeout, time_traits::duration lifespan) + : idle_timeout_(idle_timeout), lifespan_(lifespan) { + const auto now = time_traits::now(); + const auto drop_time = std::min(time_traits::add(now, idle_timeout_), time_traits::add(now, lifespan_)); + std::for_each(begin, end, [&] (auto&& v) { + available_.emplace_back(std::forward(v), drop_time, now); + }); +} + +template +storage_stats storage::stats() const { + storage_stats result; + result.available = available_.size(); + result.used = used_.size(); + result.wasted = wasted_.size(); + return result; +} + +template +boost::optional::cell_iterator> storage::lease() { + const auto now = time_traits::now(); + while (!available_.empty()) { + const auto candidate = available_.begin(); + if (candidate->drop_time > now) { + used_.splice(used_.end(), available_, candidate); + return candidate; + } + candidate->value.reset(); + wasted_.splice(wasted_.end(), available_, candidate); + } + if (!wasted_.empty()) { + const auto result = wasted_.begin(); + result->waste_on_recycle = false; + used_.splice(used_.end(), wasted_, result); + return result; + } + return {}; +} + +template +void storage::recycle(typename storage::cell_iterator cell) { + if (cell->waste_on_recycle) { + return waste(cell); + } + const auto now = time_traits::now(); + const auto life_end = time_traits::add(cell->reset_time, lifespan_); + if (life_end <= now) { + return waste(cell); + } + cell->drop_time = std::min(time_traits::add(now, idle_timeout_), life_end); + available_.splice(available_.end(), used_, cell); +} + +template +void storage::waste(typename storage::cell_iterator cell) { + cell->value.reset(); + wasted_.splice(wasted_.end(), used_, cell); +} + +template +bool storage::is_valid(typename storage::const_cell_iterator cell) const { + if (cell->waste_on_recycle) { + return false; + } + const auto now = time_traits::now(); + const auto life_end = time_traits::add(cell->reset_time, lifespan_); + if (life_end <= now) { + return false; + } + return true; +} + +template +void storage::invalidate() { + for (auto& cell : available_) { + cell.value.reset(); + } + wasted_.splice(wasted_.end(), available_, available_.begin(), available_.end()); + for (auto& cell : used_) { + cell.waste_on_recycle = true; + } +} + +} // namespace detail +} // namespace resource_pool +} // namespace yamail diff --git a/contrib/resource_pool/include/yamail/resource_pool/error.hpp b/contrib/resource_pool/include/yamail/resource_pool/error.hpp new file mode 100644 index 000000000..8bc1dfd7a --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/error.hpp @@ -0,0 +1,90 @@ +#ifndef YAMAIL_RESOURCE_POOL_ERROR_HPP +#define YAMAIL_RESOURCE_POOL_ERROR_HPP + +#include + +#include + +namespace yamail { +namespace resource_pool { +namespace error { + +struct empty_handle final : std::logic_error { + empty_handle() : std::logic_error("handle is empty") {} +}; + +struct unusable_handle final : std::logic_error { + unusable_handle() : std::logic_error("handle is unusable") {} +}; + +struct zero_pool_capacity final : std::logic_error { + zero_pool_capacity() : std::logic_error("pool capacity is 0") {} +}; + +enum code { + ok, + get_resource_timeout, + request_queue_overflow, + disabled, +}; + +namespace detail { + +class category final : public boost::system::error_category { +public: + const char* name() const noexcept final { + return "yamail::resource_pool::error::detail::category"; + } + + std::string message(int value) const final { + switch (code(value)) { + case ok: + return "no error"; + case get_resource_timeout: + return "get resource timeout"; + case request_queue_overflow: + return "request queue overflow"; + case disabled: + return "resource pool is disabled"; + } + std::ostringstream error; + error << "no message for yamail::resource_pool::error: " << value; + throw std::logic_error(error.str()); + } +}; + +} + +inline const boost::system::error_category& get_category() { + static detail::category instance; + return instance; +} + +} +} +} + +namespace boost { +namespace system { + +template <> +struct is_error_code_enum { + static const bool value = true; +}; + +} +} + +namespace yamail { +namespace resource_pool { +namespace error { + +inline boost::system::error_code make_error_code(const code e) { + return boost::system::error_code(static_cast(e), error::get_category()); +} + +} +} +} + +#endif diff --git a/contrib/resource_pool/include/yamail/resource_pool/handle.hpp b/contrib/resource_pool/include/yamail/resource_pool/handle.hpp new file mode 100644 index 000000000..a6d50610c --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/handle.hpp @@ -0,0 +1,139 @@ +#ifndef YAMAIL_RESOURCE_POOL_HANDLE_HPP +#define YAMAIL_RESOURCE_POOL_HANDLE_HPP + +#include +#include +#include + +#include + +#include + +namespace yamail { +namespace resource_pool { + +template +class handle { +public: + using value_type = T; + using strategy = void (handle::*)(); + using list_iterator = detail::cell_iterator; + + handle() = default; + handle(const handle& other) = delete; + handle(handle&& other); + + handle(std::shared_ptr> pool_impl, + strategy use_strategy, + list_iterator resource_it) + : _pool_impl(std::move(pool_impl)), + _use_strategy(use_strategy), + _resource_it(resource_it) {} + + ~handle(); + + handle& operator =(const handle& other) = delete; + handle& operator =(handle&& other); + + bool unusable() const noexcept { return !static_cast(_resource_it); } + bool empty() const noexcept { return unusable() || !_resource_it.get()->value; } + value_type& get(); + const value_type& get() const; + value_type *operator ->() { return &get(); } + const value_type *operator ->() const { return &get(); } + value_type &operator *() { return get(); } + const value_type &operator *() const { return get(); } + + void recycle(); + void waste(); + void reset(value_type&& res); + +private: + std::shared_ptr> _pool_impl; + strategy _use_strategy; + boost::optional _resource_it; + + void assert_not_empty() const; + void assert_not_unusable() const; +}; + +template +handle

::handle(handle&& other) + : _pool_impl(other._pool_impl), + _use_strategy(other._use_strategy), + _resource_it(other._resource_it) { + other._resource_it = boost::none; + other._pool_impl.reset(); +} + +template +handle

::~handle() { + if (!unusable()) { + (this->*_use_strategy)(); + } +} + +template +handle

& handle

::operator =(handle&& other) { + if (!unusable()) { + (this->*_use_strategy)(); + } + _pool_impl = other._pool_impl; + _use_strategy = other._use_strategy; + _resource_it = other._resource_it; + other._resource_it = boost::none; + other._pool_impl.reset(); + return *this; +} + +template +typename handle

::value_type& handle

::get() { + assert_not_empty(); + return *_resource_it.get()->value; +} + +template +const typename handle

::value_type& handle

::get() const { + assert_not_empty(); + return *_resource_it.get()->value; +} + +template +void handle

::recycle() { + assert_not_unusable(); + _pool_impl->recycle(_resource_it.get()); + _resource_it = boost::none; +} + +template +void handle

::waste() { + assert_not_unusable(); + _pool_impl->waste(_resource_it.get()); + _resource_it = boost::none; +} + +template +void handle

::reset(value_type &&res) { + assert_not_unusable(); + _resource_it.get()->value = std::move(res); + _resource_it.get()->reset_time = time_traits::now(); +} + +template +void handle

::assert_not_empty() const { + if (empty()) { + throw error::empty_handle(); + } +} + +template +void handle

::assert_not_unusable() const { + if (unusable()) { + throw error::unusable_handle(); + } +} + +} +} + +#endif diff --git a/contrib/resource_pool/include/yamail/resource_pool/sync/detail/pool_impl.hpp b/contrib/resource_pool/include/yamail/resource_pool/sync/detail/pool_impl.hpp new file mode 100644 index 000000000..4bf4c9ae3 --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/sync/detail/pool_impl.hpp @@ -0,0 +1,181 @@ +#ifndef YAMAIL_RESOURCE_POOL_SYNC_DETAIL_POOL_IMPL_HPP +#define YAMAIL_RESOURCE_POOL_SYNC_DETAIL_POOL_IMPL_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace yamail { +namespace resource_pool { +namespace sync { + +struct stats { + std::size_t size; + std::size_t available; + std::size_t used; +}; + +namespace detail { + +using resource_pool::detail::pool_returns; + +template +class pool_impl : public pool_returns { +public: + using value_type = Value; + using condition_variable = ConditionVariable; + using idle = resource_pool::detail::idle; + using storage_type = resource_pool::detail::storage; + using list_iterator = typename storage_type::cell_iterator; + using get_result = std::pair; + + pool_impl(std::size_t capacity, time_traits::duration idle_timeout, time_traits::duration lifespan) + : storage_(assert_capacity(capacity), idle_timeout, lifespan), + _capacity(capacity) { + } + + template + pool_impl(Generator&& gen_value, + std::size_t capacity, + time_traits::duration idle_timeout, + time_traits::duration lifespan) + : storage_(std::forward(gen_value), assert_capacity(capacity), idle_timeout, lifespan), + _capacity(capacity) { + } + + std::size_t capacity() const { return _capacity; } + std::size_t size() const; + std::size_t available() const; + std::size_t used() const; + sync::stats stats() const; + + const condition_variable& has_capacity() const { return _has_capacity; } + + get_result get(time_traits::duration wait_duration = time_traits::duration(0)); + void recycle(list_iterator res_it) final; + void waste(list_iterator res_it) final; + void disable(); + void invalidate(); + + static std::size_t assert_capacity(std::size_t value); + +private: + using mutex_t = Mutex; + using lock_guard = std::lock_guard; + using unique_lock = std::unique_lock; + + mutable mutex_t _mutex; + storage_type storage_; + const std::size_t _capacity; + condition_variable _has_capacity; + bool _disabled = false; + + bool wait_for(unique_lock& lock, time_traits::duration wait_duration); +}; + +template +std::size_t pool_impl::size() const { + const auto stats = [&] { + const lock_guard lock(_mutex); + return storage_.stats(); + } (); + return stats.available + stats.used; +} + +template +std::size_t pool_impl::available() const { + const lock_guard lock(_mutex); + return storage_.stats().available; +} + +template +std::size_t pool_impl::used() const { + const lock_guard lock(_mutex); + return storage_.stats().used; +} + +template +sync::stats pool_impl::stats() const { + const auto stats = [&] { + const lock_guard lock(_mutex); + return storage_.stats(); + } (); + sync::stats result; + result.size = stats.available + stats.used; + result.available = stats.available; + result.used = stats.used; + return result; +} + +template +void pool_impl::recycle(list_iterator res_it) { + const lock_guard lock(_mutex); + storage_.recycle(res_it); + _has_capacity.notify_one(); +} + +template +void pool_impl::waste(list_iterator res_it) { + const lock_guard lock(_mutex); + storage_.waste(res_it); + _has_capacity.notify_one(); +} + +template +void pool_impl::disable() { + const lock_guard lock(_mutex); + _disabled = true; + _has_capacity.notify_all(); +} + +template +typename pool_impl::get_result pool_impl::get(time_traits::duration wait_duration) { + unique_lock lock(_mutex); + while (true) { + if (_disabled) { + lock.unlock(); + return std::make_pair(make_error_code(error::disabled), list_iterator()); + } + if (const auto cell = storage_.lease()) { + lock.unlock(); + return std::make_pair(boost::system::error_code(), *cell); + } + if (!wait_for(lock, wait_duration)) { + lock.unlock(); + return std::make_pair(make_error_code(error::get_resource_timeout), + list_iterator()); + } + } +} + +template +void pool_impl::invalidate() { + const lock_guard lock(_mutex); + storage_.invalidate(); +} + +template +bool pool_impl::wait_for(unique_lock& lock, time_traits::duration wait_duration) { + return _has_capacity.wait_for(lock, wait_duration) == std::cv_status::no_timeout; +} + +template +std::size_t pool_impl::assert_capacity(std::size_t value) { + if (value == 0) { + throw error::zero_pool_capacity(); + } + return value; +} + +} +} +} +} + +#endif diff --git a/contrib/resource_pool/include/yamail/resource_pool/sync/pool.hpp b/contrib/resource_pool/include/yamail/resource_pool/sync/pool.hpp new file mode 100644 index 000000000..d489f80a5 --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/sync/pool.hpp @@ -0,0 +1,80 @@ +#ifndef YAMAIL_RESOURCE_POOL_SYNC_POOL_HPP +#define YAMAIL_RESOURCE_POOL_SYNC_POOL_HPP + +#include +#include +#include + +#include + +namespace yamail { +namespace resource_pool { +namespace sync { + +template > +class pool { +public: + using value_type = Value; + using pool_impl = Impl; + using handle = resource_pool::handle; + using get_result = std::pair; + + pool(std::size_t capacity, + time_traits::duration idle_timeout = time_traits::duration::max(), + time_traits::duration lifespan = time_traits::duration::max()) + : _impl(std::make_shared(capacity, idle_timeout, lifespan)) + {} + + pool(std::shared_ptr impl) + : _impl(std::move(impl)) + {} + + pool(const pool&) = delete; + pool(pool&&) = default; + + ~pool() { + if (_impl) { + _impl->disable(); + } + } + + pool& operator =(const pool&) = delete; + pool& operator =(pool&&) = default; + + std::size_t capacity() const { return _impl->capacity(); } + std::size_t size() const { return _impl->size(); } + std::size_t available() const { return _impl->available(); } + std::size_t used() const { return _impl->used(); } + sync::stats stats() const { return _impl->stats(); } + + get_result get_auto_waste(time_traits::duration wait_duration = time_traits::duration(0)) { + return get_handle(&handle::waste, wait_duration); + } + + get_result get_auto_recycle(time_traits::duration wait_duration = time_traits::duration(0)) { + return get_handle(&handle::recycle, wait_duration); + } + + void invalidate() { + _impl->invalidate(); + } + +private: + using strategy = typename handle::strategy; + using pool_impl_ptr = std::shared_ptr; + + pool_impl_ptr _impl; + + get_result get_handle(strategy use_strategy, time_traits::duration wait_duration) { + const typename pool_impl::get_result& res = _impl->get(wait_duration); + return std::make_pair(res.first, handle(_impl, use_strategy, res.second)); + } +}; + +} +} +} + +#endif diff --git a/contrib/resource_pool/include/yamail/resource_pool/time_traits.hpp b/contrib/resource_pool/include/yamail/resource_pool/time_traits.hpp new file mode 100644 index 000000000..9936878f0 --- /dev/null +++ b/contrib/resource_pool/include/yamail/resource_pool/time_traits.hpp @@ -0,0 +1,38 @@ +#ifndef YAMAIL_RESOURCE_POOL_TIME_TRAITS_HPP +#define YAMAIL_RESOURCE_POOL_TIME_TRAITS_HPP + +#include + +#include + +namespace yamail { +namespace resource_pool { + +struct time_traits { + using duration = std::chrono::steady_clock::duration; + using time_point = std::chrono::steady_clock::time_point; + using timer = boost::asio::basic_waitable_timer; + + static time_point now() { + return std::chrono::steady_clock::now(); + } + + static time_point add(time_point t, duration d) { + const time_point epoch; + if (t >= epoch) { + if (time_point::max() - t < d) { + return time_point::max(); + } + } else { + if (-(t - time_point::min()) > d) { + return time_point::min(); + } + } + return t + d; + } +}; + +} // namespace resource_pool +} // namespace yamail + +#endif // YAMAIL_RESOURCE_POOL_TIME_TRAITS_HPP diff --git a/contrib/resource_pool/resource_pool.spec b/contrib/resource_pool/resource_pool.spec new file mode 100644 index 000000000..7bbb9a22e --- /dev/null +++ b/contrib/resource_pool/resource_pool.spec @@ -0,0 +1,55 @@ +%define _builddir . +%define _sourcedir . +%define _specdir . +%define _rpmdir . + +Name: resource_pool +Version: %{yandex_mail_version} +Release: %{yandex_mail_release} +Summary: Resource pool +License: Yandex License +Group: System Environment/Libraries +Packager: Roman Siromakha +Distribution: Red Hat Enterprise Linux + +Requires: boost >= 1.53.0 + +BuildRequires: boost-devel >= 1.53.0 + +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root + +%description +Resource pool + +%package devel +Summary: Development environment for %{name} +Group: System Environment/Libraries + +%description devel +Resource pool + +%build +cmake . -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_BUILD_TYPE=Release +%{__make} %{?_smp_mflags} + +%install +%{__rm} -rf %{buildroot} +%{__make} install DESTDIR=%{buildroot} +%{__mkdir_p} %{buildroot}%{_initrddir} + +%clean +%{__rm} -rf $RPM_BUILD_ROOT + +%files devel +%defattr(-,root,root) +%dir %{_includedir}/yamail +%{_includedir}/yamail/*.hpp +%{_includedir}/yamail/resource_pool/*.hpp +%{_includedir}/yamail/resource_pool/sync/*.hpp +%{_includedir}/yamail/resource_pool/sync/detail/*.hpp +%{_includedir}/yamail/resource_pool/async/*.hpp +%{_includedir}/yamail/resource_pool/async/detail/*.hpp +%{_includedir}/yamail/resource_pool/detail/*.hpp + +%changelog diff --git a/contrib/resource_pool/resource_poolConfig.cmake b/contrib/resource_pool/resource_poolConfig.cmake new file mode 100644 index 000000000..6e189ec16 --- /dev/null +++ b/contrib/resource_pool/resource_poolConfig.cmake @@ -0,0 +1,4 @@ +include(CMakeFindDependencyMacro) +find_dependency(Boost 1.66 COMPONENTS system thread) + +include("${CMAKE_CURRENT_LIST_DIR}/resource_poolTargets.cmake") diff --git a/contrib/resource_pool/scripts/ci/conan.sh b/contrib/resource_pool/scripts/ci/conan.sh new file mode 100755 index 000000000..6cd03df89 --- /dev/null +++ b/contrib/resource_pool/scripts/ci/conan.sh @@ -0,0 +1,28 @@ +#!/bin/bash -ex + +DIR="${PWD}" + +pip3 install --user virtualenv +virtualenv conan-temp +. conan-temp/bin/activate +pip3 install conan==1.64.0 +conan profile new --detect default + +export CC=/usr/bin/clang-15 +export CXX=/usr/bin/clang++-15 + +conan profile update settings.compiler=clang default +conan profile update settings.compiler.version=15 default +conan profile update settings.compiler.libcxx=libstdc++11 default + +cd "${DIR}" + +PKG_REPO="elsid/testing" +conan export . "${PKG_REPO}" + +PKG_NAME="$(conan inspect --raw name .)/$(conan inspect --raw version .)" +PKG_ID="${PKG_NAME}@${PKG_REPO}" +echo "package-id: ${PKG_ID}" + +conan install "${PKG_ID}" --build=missing +conan test test_package "${PKG_ID}" diff --git a/contrib/resource_pool/test_package/CMakeLists.txt b/contrib/resource_pool/test_package/CMakeLists.txt new file mode 100644 index 000000000..ad05a3b37 --- /dev/null +++ b/contrib/resource_pool/test_package/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.12) +project(PackageTest CXX) + +find_package(resource_pool REQUIRED) + +add_executable(example ../examples/async/coro.cc) +target_compile_features(example PRIVATE cxx_std_17) +target_link_libraries(example PRIVATE elsid::resource_pool) diff --git a/contrib/resource_pool/test_package/conanfile.py b/contrib/resource_pool/test_package/conanfile.py new file mode 100644 index 000000000..40b9419c0 --- /dev/null +++ b/contrib/resource_pool/test_package/conanfile.py @@ -0,0 +1,21 @@ +from conans import ConanFile, CMake + + +class ResourcePoolTestConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "cmake_find_package" + + def build(self): + cmake = CMake(self) + # Current dir is "test_package/build/" and CMakeLists.txt is + # in "test_package" + cmake.configure() + cmake.build() + + def imports(self): + self.copy("*.dll", dst="bin", src="bin") + self.copy("*.dylib*", dst="bin", src="lib") + self.copy('*.so*', dst='bin', src='lib') + + def test(self): + pass # Building alone is sufficient diff --git a/contrib/resource_pool/tests/CMakeLists.txt b/contrib/resource_pool/tests/CMakeLists.txt new file mode 100644 index 000000000..629bb4d45 --- /dev/null +++ b/contrib/resource_pool/tests/CMakeLists.txt @@ -0,0 +1,71 @@ +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + +message(STATUS "CMAKE_CURRENT_LIST_DIR: ${CMAKE_CURRENT_LIST_DIR}") + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic -Werror") + +find_package(Boost COMPONENTS coroutine context REQUIRED) + +include_directories(SYSTEM ${RESOURCE_POOL_DEPENDENCY_INCLUDE_DIRS}) +include_directories(SYSTEM ${Boost_INCLUDE_DIR}) +include_directories(${PROJECT_SOURCE_DIR}/include) + +if(RESOURCE_POOL_USE_SYSTEM_GOOGLETEST) + find_package(GTest) + find_package(GMock) +else() + include(FetchContent) + FetchContent_Declare(googletest + URL https://github.com/google/googletest/archive/refs/tags/release-1.12.1.zip + URL_HASH SHA512=1479ea2f3172c622c0ca305f5b2bc45a42941221ec0ac7865e6d6d020ec4d008d952fc64e01a4c5138d7bed4148cf75596f25bb9e9044a98bbbf5662053ea11c + SOURCE_DIR fetched/googletest + ) + FetchContent_MakeAvailable(googletest) + + add_library(GTest::GTest ALIAS gtest) + add_library(GMock::GMock ALIAS gmock) +endif() + +add_executable(resource_pool_test + main.cc + error.cc + handle.cc + time_traits.cc + sync/pool.cc + sync/pool_impl.cc + async/pool.cc + async/pool_impl.cc + async/queue.cc + async/integration.cc +) + +set(LIBRARIES + pthread + GTest::GTest + GMock::GMock + Boost::coroutine + Boost::context + elsid::resource_pool +) + +target_link_libraries(resource_pool_test ${LIBRARIES}) + +if(NOT TARGET check) + add_custom_target(check ctest -V) +endif() + +add_test(resource_pool_test resource_pool_test) +add_dependencies(check resource_pool_test) + +option(RESOURCE_POOL_COVERAGE "Check coverage" OFF) + +if(RESOURCE_POOL_COVERAGE AND CMAKE_COMPILER_IS_GNUCXX) + include(CodeCoverage) + APPEND_COVERAGE_COMPILER_FLAGS() + set(COVERAGE_EXCLUDES "'.*/(tests|contrib|examples|gmock|gtest)/.*'") + SETUP_TARGET_FOR_COVERAGE_GCOVR( + NAME resource_pool_coverage + EXECUTABLE ctest + DEPENDENCIES resource_pool_test + ) +endif() diff --git a/contrib/resource_pool/tests/async/integration.cc b/contrib/resource_pool/tests/async/integration.cc new file mode 100644 index 000000000..d8c78323a --- /dev/null +++ b/contrib/resource_pool/tests/async/integration.cc @@ -0,0 +1,492 @@ +#include + +#include + +#include + +namespace { + +using namespace testing; +using namespace yamail::resource_pool; +using namespace yamail::resource_pool::async; + +namespace asio = boost::asio; + +struct resource { + int value; + + resource(int value) : value(value) {} + resource(const resource&) = delete; + resource(resource&&) = default; + resource& operator =(const resource&) = delete; + resource& operator =(resource&&) = default; + + friend bool operator ==(const resource& lhs, const resource& rhs) { + return lhs.value == rhs.value; + } +}; + +using resource_pool = pool; +using boost::system::error_code; + +struct async_resource_pool_integration : Test { + asio::io_context io; + std::atomic_flag coroutine_finished = ATOMIC_FLAG_INIT; + std::atomic_flag coroutine1_finished = ATOMIC_FLAG_INIT; + std::atomic_flag coroutine2_finished = ATOMIC_FLAG_INIT; + std::atomic_flag coroutine3_finished = ATOMIC_FLAG_INIT; + std::atomic_flag on_get_called = ATOMIC_FLAG_INIT; + std::atomic_flag on_get1_called = ATOMIC_FLAG_INIT; + std::atomic_flag on_get2_called = ATOMIC_FLAG_INIT; + std::atomic_flag on_get3_called = ATOMIC_FLAG_INIT; +}; + +TEST_F(async_resource_pool_integration, first_get_auto_recycle_should_return_usable_empty_handle_to_resource) { + resource_pool pool(1, 0); + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, after_get_auto_recycle_pool_should_save_handle_state) { + resource_pool pool(1, 0); + + asio::spawn(io, [&] (asio::yield_context yield) { + { + auto handle = pool.get_auto_recycle(io, yield); + ASSERT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {42}); + } + { + const auto handle = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle.unusable()); + ASSERT_FALSE(handle.empty()); + EXPECT_EQ(*handle, resource {42}); + } + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, parallel_requests_should_get_different_handles) { + resource_pool pool(2, 0); + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {42}); + + ASSERT_FALSE(coroutine1_finished.test_and_set()); + }); + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {13}); + + ASSERT_FALSE(coroutine2_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine1_finished.test_and_set()); + EXPECT_TRUE(coroutine2_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, sequenced_requests_should_get_different_handles) { + resource_pool pool(2, 0); + + asio::spawn(io, [&] (asio::yield_context yield) { + const auto handle1 = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle1.unusable()); + EXPECT_TRUE(handle1.empty()); + + const auto handle2 = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle2.unusable()); + EXPECT_TRUE(handle2.empty()); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, request_with_zero_wait_duration_should_not_be_pending) { + resource_pool pool(1, 1); + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle1 = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle1.unusable()); + EXPECT_TRUE(handle1.empty()); + + error_code ec; + auto handle2 = pool.get_auto_recycle(io, yield[ec], time_traits::duration(0)); + EXPECT_EQ(ec, error_code(error::get_resource_timeout)); + EXPECT_TRUE(handle2.unusable()); + EXPECT_TRUE(handle2.empty()); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, queue_should_store_pending_requests) { + resource_pool pool(1, 1); + + const auto on_get = [&] (error_code ec, auto handle) { + ASSERT_FALSE(on_get_called.test_and_set()); + + EXPECT_FALSE(ec); + EXPECT_FALSE(handle.unusable()); + ASSERT_FALSE(handle.empty()); + EXPECT_EQ(*handle, resource {42}); + }; + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool.get_auto_recycle(io, yield); + ASSERT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {42}); + + pool.get_auto_recycle(io, on_get, time_traits::duration::max()); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(on_get_called.test_and_set()); + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, for_zero_queue_capacity_should_not_be_pending_requests) { + resource_pool pool(1, 0); + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle1 = pool.get_auto_recycle(io, yield); + EXPECT_FALSE(handle1.unusable()); + EXPECT_TRUE(handle1.empty()); + + error_code ec; + auto handle2 = pool.get_auto_recycle(io, yield[ec], time_traits::duration::max()); + EXPECT_EQ(ec, error_code(error::request_queue_overflow)); + EXPECT_TRUE(handle2.unusable()); + EXPECT_TRUE(handle2.empty()); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, recursive_get_auto_recycle_should_not_lead_to_locked_resources_for_all_calls) { + resource_pool pool(2, 0); + + const auto on_get3 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get3_called.test_and_set()); + + EXPECT_FALSE(ec); + ASSERT_EQ(pool.used(), 1u); + }; + + const auto on_get2 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get2_called.test_and_set()); + + EXPECT_FALSE(ec); + ASSERT_EQ(pool.used(), 1u); + + pool.get_auto_recycle(io, on_get3); + }; + + const auto on_get1 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get1_called.test_and_set()); + + EXPECT_FALSE(ec); + ASSERT_EQ(pool.used(), 1u); + + pool.get_auto_recycle(io, on_get2); + }; + + asio::dispatch([&] { + pool.get_auto_recycle(io, on_get1); + }); + + io.run(); + + EXPECT_TRUE(on_get1_called.test_and_set()); + EXPECT_TRUE(on_get2_called.test_and_set()); + EXPECT_TRUE(on_get3_called.test_and_set()); +} + +TEST_F(async_resource_pool_integration, first_get_auto_waste_should_return_usable_empty_handle_to_resource) { + resource_pool pool(1, 0); + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool.get_auto_waste(io, yield); + EXPECT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, after_get_auto_waste_pool_should_reset_handle_state) { + resource_pool pool(1, 0); + + asio::spawn(io, [&] (asio::yield_context yield) { + { + auto handle = pool.get_auto_waste(io, yield); + ASSERT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {42}); + } + { + const auto handle = pool.get_auto_waste(io, yield); + EXPECT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + } + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, disabled_pool_should_cancel_all_pending_requests) { + auto pool = std::make_unique(1, 1); + + const auto on_get = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get_called.test_and_set()); + + EXPECT_EQ(ec, error_code(error::disabled)); + }; + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool->get_auto_recycle(io, yield); + ASSERT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {42}); + + pool->get_auto_recycle(io, on_get, time_traits::duration::max()); + pool.reset(); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(on_get_called.test_and_set()); + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, retries_to_get_resource_should_not_lead_to_infinite_timeout_errors) { + resource_pool pool(1, 1); + + const auto on_get3 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get3_called.test_and_set()); + + EXPECT_FALSE(ec); + ASSERT_EQ(pool.used(), 1u); + }; + + const auto on_get2 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get2_called.test_and_set()); + + EXPECT_EQ(ec, error_code(error::get_resource_timeout)); + ASSERT_EQ(pool.used(), 0u); + + pool.get_auto_recycle(io, on_get3); + }; + + const auto on_get1 = [&] (error_code ec, auto handle) { + ASSERT_FALSE(on_get1_called.test_and_set()); + + EXPECT_FALSE(ec); + ASSERT_EQ(pool.used(), 1u); + + pool.get_auto_recycle(io, on_get2); + handle.recycle(); + }; + + asio::dispatch([&] { + pool.get_auto_recycle(io, on_get1); + }); + + io.run(); + + EXPECT_TRUE(on_get1_called.test_and_set()); + EXPECT_TRUE(on_get2_called.test_and_set()); + EXPECT_TRUE(on_get3_called.test_and_set()); +} + +TEST_F(async_resource_pool_integration, retries_to_get_resource_should_not_lead_to_infinite_queue_overflow_errors) { + resource_pool pool(1, 0); + + const auto on_get3 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get3_called.test_and_set()); + + EXPECT_FALSE(ec); + ASSERT_EQ(pool.used(), 1u); + }; + + const auto on_get2 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get2_called.test_and_set()); + + EXPECT_EQ(ec, error_code(error::request_queue_overflow)); + ASSERT_EQ(pool.used(), 0u); + + pool.get_auto_recycle(io, on_get3); + }; + + const auto on_get1 = [&] (error_code ec, auto handle) { + ASSERT_FALSE(on_get1_called.test_and_set()); + + EXPECT_FALSE(ec); + ASSERT_EQ(pool.used(), 1u); + + pool.get_auto_recycle(io, on_get2, time_traits::duration::max()); + handle.recycle(); + }; + + asio::dispatch([&] { + pool.get_auto_recycle(io, on_get1); + }); + + io.run(); + + EXPECT_TRUE(on_get1_called.test_and_set()); + EXPECT_TRUE(on_get2_called.test_and_set()); + EXPECT_TRUE(on_get3_called.test_and_set()); +} + +TEST_F(async_resource_pool_integration, enqueue_pending_request_on_timeout_should_not_lead_to_deadlock) { + resource_pool pool(1, 1); + std::atomic_bool finish_coroutine {false}; + + const auto on_get2 = [&] (error_code ec, auto handle) { + ASSERT_FALSE(on_get2_called.test_and_set()); + + EXPECT_FALSE(ec); + EXPECT_FALSE(handle.unusable()); + ASSERT_FALSE(handle.empty()); + EXPECT_EQ(*handle, resource {42}); + }; + + const auto on_get1 = [&] (error_code ec, auto) { + ASSERT_FALSE(on_get1_called.test_and_set()); + + finish_coroutine = true; + + EXPECT_EQ(ec, error_code(error::get_resource_timeout)); + ASSERT_EQ(pool.used(), 1u); + + pool.get_auto_recycle(io, on_get2, time_traits::duration::max()); + }; + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool.get_auto_recycle(io, yield); + ASSERT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {42}); + + pool.get_auto_recycle(io, on_get1, std::chrono::nanoseconds(1)); + + while (!finish_coroutine) { + asio::post(io, yield); + } + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(on_get1_called.test_and_set()); + EXPECT_TRUE(on_get2_called.test_and_set()); + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +TEST_F(async_resource_pool_integration, pending_request_should_get_empty_handle_after_waste) { + resource_pool pool(1, 1); + + const auto on_get = [&] (error_code ec, auto handle) { + ASSERT_FALSE(on_get_called.test_and_set()); + + EXPECT_FALSE(ec); + EXPECT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + }; + + asio::spawn(io, [&] (asio::yield_context yield) { + auto handle = pool.get_auto_waste(io, yield); + ASSERT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + handle.reset(resource {42}); + + pool.get_auto_recycle(io, on_get, time_traits::duration::max()); + + ASSERT_FALSE(coroutine_finished.test_and_set()); + }); + + io.run(); + + EXPECT_TRUE(on_get_called.test_and_set()); + EXPECT_TRUE(coroutine_finished.test_and_set()); +} + +struct move_only_handler { + std::atomic_flag* called = nullptr; + + move_only_handler() = default; + move_only_handler(std::atomic_flag* called) : called(called) {} + move_only_handler(const move_only_handler&) = delete; + move_only_handler(move_only_handler&&) = default; + + void operator ()(error_code, resource_pool::handle) { + ASSERT_FALSE(called->test_and_set()); + } +}; + +TEST_F(async_resource_pool_integration, get_auto_recycle_should_support_move_only_handler) { + resource_pool pool(1, 1); + pool.get_auto_recycle(io, move_only_handler(std::addressof(on_get_called))); + io.run(); + EXPECT_TRUE(on_get_called.test_and_set()); +} + +TEST_F(async_resource_pool_integration, get_auto_waste_should_support_move_only_handler) { + resource_pool pool(1, 1); + pool.get_auto_waste(io, move_only_handler(std::addressof(on_get_called))); + io.run(); + EXPECT_TRUE(on_get_called.test_and_set()); +} + +} diff --git a/contrib/resource_pool/tests/async/pool.cc b/contrib/resource_pool/tests/async/pool.cc new file mode 100644 index 000000000..348d88529 --- /dev/null +++ b/contrib/resource_pool/tests/async/pool.cc @@ -0,0 +1,267 @@ +#include "tests.hpp" + +#include + +#include + +namespace { + +using namespace tests; +using namespace yamail::resource_pool; +using namespace yamail::resource_pool::async; + +using yamail::resource_pool::detail::pool_returns; + +struct mocked_pool_impl : pool_returns { + using value_type = resource; + using idle = yamail::resource_pool::detail::idle; + using list = std::list; + using list_iterator = list::iterator; + using callback = std::function; + + MOCK_CONST_METHOD0(capacity, std::size_t ()); + MOCK_CONST_METHOD0(size, std::size_t ()); + MOCK_CONST_METHOD0(available, std::size_t ()); + MOCK_CONST_METHOD0(used, std::size_t ()); + MOCK_CONST_METHOD0(stats, async::stats ()); + MOCK_METHOD3(get, void (mocked_io_context&, const callback&, time_traits::duration)); + MOCK_METHOD1(recycle, void (list_iterator)); + MOCK_METHOD1(waste, void (list_iterator)); + MOCK_METHOD0(disable, void ()); +}; + +using resource_pool = pool>; + +using boost::system::error_code; + +struct async_resource_pool : Test { + StrictMock executor; + mocked_executor executor_wrapper {&executor}; + mocked_io_context io {&executor_wrapper}; + mocked_pool_impl::callback on_get; + mocked_pool_impl::list resources; + mocked_pool_impl::list_iterator resource_iterator; + + async_resource_pool() + : resources(1), resource_iterator(resources.begin()) {} +}; + +TEST_F(async_resource_pool, call_capacity_should_call_impl_capacity) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, capacity()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.capacity(); +} + +TEST_F(async_resource_pool, call_size_should_call_impl_size) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, size()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.size(); +} + +TEST_F(async_resource_pool, call_available_should_call_impl_available) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, available()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.available(); +} + +TEST_F(async_resource_pool, call_used_should_call_impl_used) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, used()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.used(); +} + +TEST_F(async_resource_pool, call_stats_should_call_impl_stats) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, stats()).WillOnce(Return(async::stats {0, 0, 0, 0})); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.stats(); +} + +TEST_F(async_resource_pool, move_than_dtor_should_call_disable_only_for_destination) { + const auto pool_impl = std::make_shared>(); + resource_pool src(pool_impl); + + EXPECT_CALL(*pool_impl, disable()).Times(0); + + const auto dst = std::move(src); + + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); +} + +class check_error { +public: + check_error(const error_code& error) : error(error) {} + check_error(const error::code& error) : error(make_error_code(error)) {} + + void operator ()(const error_code& err, resource_pool::handle res) const { + EXPECT_EQ(err, error); + EXPECT_EQ(res.unusable(), error != error_code()); + } + +private: + const error_code error; +}; + +struct check_no_error : check_error { + check_no_error() : check_error(error_code()) {} +}; + +TEST_F(async_resource_pool, get_auto_recylce_handle_should_call_recycle) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + EXPECT_CALL(*pool_impl, get(_, _, _)).WillOnce(SaveArg<1>(&on_get)); + EXPECT_CALL(*pool_impl, recycle(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.get_auto_recycle(io, check_no_error()); + on_get(error_code(), resource_iterator); +} + +TEST_F(async_resource_pool, get_auto_waste_handle_should_call_waste) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + EXPECT_CALL(*pool_impl, get(_, _, _)).WillOnce(SaveArg<1>(&on_get)); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.get_auto_waste(io, check_no_error()); + on_get(error_code(), resource_iterator); +} + +struct recycle_resource { + void operator ()(const error_code& err, resource_pool::handle res) const { + EXPECT_EQ(err, error_code()); + EXPECT_FALSE(res.unusable()); + res.recycle(); + } +}; + +TEST_F(async_resource_pool, get_auto_recylce_handle_and_recycle_should_call_recycle_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + EXPECT_CALL(*pool_impl, get(_, _, _)).WillOnce(SaveArg<1>(&on_get)); + EXPECT_CALL(*pool_impl, recycle(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.get_auto_recycle(io, recycle_resource()); + on_get(error_code(), resource_iterator); +} + +TEST_F(async_resource_pool, get_auto_waste_handle_and_recycle_should_call_recycle_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + EXPECT_CALL(*pool_impl, get(_, _, _)).WillOnce(SaveArg<1>(&on_get)); + EXPECT_CALL(*pool_impl, recycle(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.get_auto_waste(io, recycle_resource()); + on_get(error_code(), resource_iterator); +} + +struct waste_resource { + void operator ()(const error_code& err, resource_pool::handle res) const { + EXPECT_EQ(err, error_code()); + EXPECT_FALSE(res.unusable()); + res.waste(); + } +}; + +TEST_F(async_resource_pool, get_auto_recylce_handle_and_waste_should_call_waste_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + EXPECT_CALL(*pool_impl, get(_, _, _)).WillOnce(SaveArg<1>(&on_get)); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.get_auto_recycle(io, waste_resource()); + on_get(error_code(), resource_iterator); +} + +TEST_F(async_resource_pool, get_auto_waste_handle_and_waste_should_call_waste_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + EXPECT_CALL(*pool_impl, get(_, _, _)).WillOnce(SaveArg<1>(&on_get)); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.get_auto_waste(io, waste_resource()); + on_get(error_code(), resource_iterator); +} + +TEST_F(async_resource_pool, get_from_pool_returns_error_should_not_call_waste_or_recycle) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + EXPECT_CALL(*pool_impl, get(_, _, _)).WillOnce(SaveArg<1>(&on_get)); + EXPECT_CALL(*pool_impl, waste(_)).Times(0); + EXPECT_CALL(*pool_impl, recycle(_)).Times(0); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.get_auto_waste(io, check_error(error::get_resource_timeout)); + on_get(make_error_code(error::get_resource_timeout), mocked_pool_impl::list_iterator()); +} + +struct mocked_callback { + MOCK_METHOD0(call, void ()); +}; + +TEST_F(async_resource_pool, asio_use_executor) { + boost::asio::io_context service; + boost::asio::io_context callback_service; + async::pool pool(1, 0); + const auto call = std::make_shared(); + bool called = false; + EXPECT_CALL(*call, call()).WillOnce(Return()); + + pool.get_auto_waste( + service, + boost::asio::bind_executor( + callback_service.get_executor(), + [call, &called](const boost::system::error_code&, async::pool::handle) { + called = true; + call->call(); + } + ) + ); + service.run(); + EXPECT_FALSE(called); + callback_service.run(); + EXPECT_TRUE(called); +} + +} diff --git a/contrib/resource_pool/tests/async/pool_impl.cc b/contrib/resource_pool/tests/async/pool_impl.cc new file mode 100644 index 000000000..5cf3b3822 --- /dev/null +++ b/contrib/resource_pool/tests/async/pool_impl.cc @@ -0,0 +1,578 @@ +#include "tests.hpp" + +#include + +#include + +namespace { + +using namespace tests; +using namespace yamail::resource_pool; +using namespace yamail::resource_pool::async::detail; + +struct mocked_queue { + using list_iterator = std::list>::iterator; + using value_type = list_iterator_handler; + using queued_value_t = queued_value; + + MOCK_CONST_METHOD3(push, bool (mocked_io_context&, time_traits::duration, const value_type&)); + MOCK_CONST_METHOD0(pop, boost::optional ()); + MOCK_CONST_METHOD0(size, std::size_t ()); + + mocked_queue(std::size_t) {} +}; + +using resource_pool_impl = pool_impl; +using resource_ptr_list_iterator = resource_pool_impl::list_iterator; +using queued_value_t = mocked_queue::queued_value_t; + +auto make_queued_value(mocked_queue::value_type&& request, mocked_io_context& io) { + return boost::optional(queued_value_t {std::move(request), io}); +} + +} + +namespace yamail::resource_pool::async::detail { + +template +inline std::ostream& operator <<(std::ostream& stream, const queued_value&) { + return stream; +} + +} + +namespace boost { + +inline std::ostream& operator <<(std::ostream& stream, resource_ptr_list_iterator res) { + return stream << &*res; +} + +} + +namespace { + +using boost::system::error_code; + +struct mocked_callback { + MOCK_CONST_METHOD2(call, void (const error_code&, resource_ptr_list_iterator)); +}; + +using mocked_callback_ptr = std::shared_ptr; + +struct async_resource_pool_impl : Test { + StrictMock executor; + mocked_executor executor_wrapper {&executor}; + mocked_io_context io {&executor_wrapper}; + + std::function on_get; + std::function on_first_get; + std::function on_second_get; + + mocked_queue::value_type on_get_res; +}; + +TEST_F(async_resource_pool_impl, create_with_zero_capacity_should_throw_exception) { + EXPECT_THROW(resource_pool_impl(0, 0, time_traits::duration::max(), time_traits::duration::max()), + error::zero_pool_capacity); +} + +TEST_F(async_resource_pool_impl, create_const_with_non_zero_capacity_then_check) { + const resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.capacity(), 1u); +} + +TEST_F(async_resource_pool_impl, create_const_then_check_size_should_be_0) { + const resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.size(), 0u); +} + +TEST_F(async_resource_pool_impl, create_const_then_check_available_should_be_0) { + const resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.available(), 0u); +} + +TEST_F(async_resource_pool_impl, create_const_then_check_used_should_be_0) { + const resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.used(), 0u); +} + +TEST_F(async_resource_pool_impl, create_const_with_range_len2_then_check_capacity_should_be_2) { + std::vector res; + res.emplace_back(); + res.emplace_back(); + const resource_pool_impl pool( + std::make_move_iterator(std::begin(res)), std::make_move_iterator(std::end(res)), + 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_EQ(pool.capacity(), 2u); +} + +TEST_F(async_resource_pool_impl, create_const_with_range_len2_then_check_size_should_be_2) { + std::vector res; + res.emplace_back(); + res.emplace_back(); + const resource_pool_impl pool( + std::make_move_iterator(std::begin(res)), std::make_move_iterator(std::end(res)), + 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_EQ(pool.size(), 2u); +} + +TEST_F(async_resource_pool_impl, create_const_with_range_len2_then_check_available_should_be_2) { + std::vector res; + res.emplace_back(); + res.emplace_back(); + const resource_pool_impl pool( + std::make_move_iterator(std::begin(res)), std::make_move_iterator(std::end(res)), + 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_EQ(pool.available(), 2u); +} + +TEST_F(async_resource_pool_impl, create_const_with_generator_and_capacity_2_then_check_capacity_should_be_2) { + const resource_pool_impl pool([]{ return resource{}; }, 2, 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_EQ(pool.capacity(), 2u); +} + +TEST_F(async_resource_pool_impl, create_const_with_generator_and_capacity_2_then_check_size_should_be_2) { + const resource_pool_impl pool([]{ return resource{}; }, 2, 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_EQ(pool.size(), 2u); +} + +TEST_F(async_resource_pool_impl, create_const_with_generator_and_capacity_2_then_check_available_should_be_2) { + const resource_pool_impl pool([]{ return resource{}; }, 2, 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_EQ(pool.available(), 2u); +} + + +TEST_F(async_resource_pool_impl, create_const_then_check_stats_should_be_0_0_0_0) { + const resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + const async::stats expected {0, 0, 0, 0}; + + EXPECT_CALL(pool.queue(), size()).WillOnce(Return(0)); + + const auto actual = pool.stats(); + + EXPECT_EQ(actual.size, expected.size); + EXPECT_EQ(actual.available, expected.available); + EXPECT_EQ(actual.used, expected.used); + EXPECT_EQ(actual.queue_size, expected.queue_size); +} + +TEST_F(async_resource_pool_impl, create_const_then_call_queue_should_succeed) { + const resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_NO_THROW(pool.queue()); +} + +class callback { +public: + using result_type = void; + + callback() = default; + + callback(const callback&) = delete; + + callback(callback&&) = default; + + callback(const mocked_callback_ptr& impl) : impl(impl) {} + + callback& operator =(const callback&) = delete; + + callback& operator =(callback&&) = default; + + result_type operator ()(const error_code& err, + resource_ptr_list_iterator res) const { + impl->call(err, res); + } + +private: + mocked_callback_ptr impl; +}; + +class recycle_resource { +public: + recycle_resource(resource_pool_impl& pool) : pool(pool) {} + + void operator ()(const error_code& err, resource_ptr_list_iterator res) const { + EXPECT_EQ(err, error_code()); + pool.recycle(res); + } + +private: + resource_pool_impl& pool; +}; + +class waste_resource { +public: + waste_resource(resource_pool_impl& pool) : pool(pool) {} + + void operator ()(const error_code& err, resource_ptr_list_iterator res) const { + EXPECT_EQ(err, error_code()); + pool.waste(res); + } + +private: + resource_pool_impl& pool; +}; + +ACTION_P(SaveMoveArg2, ptr) { + *ptr = std::move(const_cast&>(arg2)); +} + +TEST_F(async_resource_pool_impl, get_one_should_call_callback) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + mocked_callback_ptr get = std::make_shared(); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_get)); + EXPECT_CALL(*get, call(_, _)).WillOnce(Return()); + + pool.get(io, callback(get)); + on_get(); +} + +TEST_F(async_resource_pool_impl, get_one_and_recycle_should_make_one_available_resource) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + + pool.get(io, recycle_resource(pool)); + on_get(); + + EXPECT_EQ(pool.available(), 1u); +} + +TEST_F(async_resource_pool_impl, get_one_and_waste_should_make_no_available_resources) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + + pool.get(io, waste_resource(pool)); + on_get(); + + EXPECT_EQ(pool.available(), 0u); +} + +TEST_F(async_resource_pool_impl, get_twice_and_recycle_should_make_one_available_resource) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, recycle_resource(pool)); + on_first_get(); + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, recycle_resource(pool), time_traits::duration(1)); + on_second_get(); + + EXPECT_EQ(pool.available(), 1u); +} + +TEST_F(async_resource_pool_impl, get_twice_and_recycle_should_use_queue_and_make_one_available_resource) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(DoAll(SaveMoveArg2(&on_get_res), Return(true))); + pool.get(io, recycle_resource(pool)); + pool.get(io, recycle_resource(pool), time_traits::duration(1)); + + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(make_queued_value(std::move(on_get_res), io)))); + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + on_first_get(); + on_second_get(); + + EXPECT_EQ(pool.available(), 1u); +} + +TEST_F(async_resource_pool_impl, get_twice_and_recycle_with_zero_idle_timeout_should_use_queue_and_make_one_available_resource) { + resource_pool_impl pool(1, 0, time_traits::duration(0), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(DoAll(SaveMoveArg2(&on_get_res), Return(true))); + pool.get(io, recycle_resource(pool)); + pool.get(io, recycle_resource(pool), time_traits::duration(1)); + + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(make_queued_value(std::move(on_get_res), io)))); + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + on_first_get(); + on_second_get(); + + EXPECT_EQ(pool.available(), 1u); +} + +TEST_F(async_resource_pool_impl, get_twice_and_waste_then_get_should_use_queue) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(DoAll(SaveMoveArg2(&on_get_res), Return(true))); + pool.get(io, waste_resource(pool)); + pool.get(io, waste_resource(pool), time_traits::duration(1)); + + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(make_queued_value(std::move(on_get_res), io)))); + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + on_first_get(); + on_second_get(); + + EXPECT_EQ(pool.available(), 0u); +} + +class check_error { +public: + check_error(const error_code& error) : error(error) {} + check_error(const error::code& error) : error(make_error_code(error)) {} + + void operator ()(const error_code& err, resource_ptr_list_iterator) const { + EXPECT_EQ(err, error); + } + +private: + const error_code error; +}; + +struct check_no_error : check_error { + check_no_error() : check_error(error_code()) {} +}; + +TEST_F(async_resource_pool_impl, get_with_queue_zero_capacity_use_should_return_error) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(Return(false)); + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + + pool.get(io, recycle_resource(pool)); + pool.get(io, check_error(error::request_queue_overflow), time_traits::duration(1)); + on_first_get(); + on_second_get(); + + EXPECT_EQ(pool.available(), 1u); +} + +TEST_F(async_resource_pool_impl, get_with_queue_use_and_timer_timeout_should_return_error) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(DoAll(SaveMoveArg2(&on_get_res), Return(true))); + + pool.get(io, check_no_error()); + pool.get(io, check_error(error::get_resource_timeout), time_traits::duration(1)); + on_first_get(); + on_get_res(make_error_code(error::get_resource_timeout)); +} + +TEST_F(async_resource_pool_impl, get_with_queue_use_with_zero_wait_duration_should_return_error) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + + pool.get(io, recycle_resource(pool)); + pool.get(io, check_error(error::get_resource_timeout), time_traits::duration(0)); + on_first_get(); + on_second_get(); + + EXPECT_EQ(pool.available(), 1u); +} + +TEST_F(async_resource_pool_impl, get_after_disable_returns_error) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + EXPECT_CALL(executor, dispatch(_)).WillOnce(SaveArg<0>(&on_get)); + + pool.disable(); + pool.get(io, check_error(error::disabled)); + on_get(); +} + +TEST_F(async_resource_pool_impl, get_recycled_after_disable_returns_error) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(DoAll(SaveMoveArg2(&on_get_res), Return(true))); + pool.get(io, recycle_resource(pool)); + pool.get(io, check_error(error::disabled), time_traits::duration(1)); + + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(make_queued_value(std::move(on_get_res), io)))); + EXPECT_CALL(executor, dispatch(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.disable(); + on_first_get(); + on_second_get(); +} + +class set_and_recycle_resource { +public: + set_and_recycle_resource(resource_pool_impl& pool) : pool(pool) {} + + void operator ()(const error_code& err, resource_ptr_list_iterator res) const { + EXPECT_EQ(err, error_code()); + res->value = resource {}; + res->reset_time = time_traits::now(); + pool.recycle(res); + } + +private: + resource_pool_impl& pool; +}; + +struct assert_empty { +public: + assert_empty(resource_pool_impl& pool) : pool(pool) {} + + void operator ()(const error_code& err, resource_ptr_list_iterator res) const { + EXPECT_EQ(err, error_code()); + EXPECT_FALSE(res->value); + pool.waste(res); + } + +private: + resource_pool_impl& pool; +}; + +TEST_F(async_resource_pool_impl, get_one_set_and_recycle_with_zero_idle_timeout_then_get_should_return_empty) { + resource_pool_impl pool(1, 0, time_traits::duration(0), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, set_and_recycle_resource(pool)); + on_first_get(); + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, assert_empty(pool), time_traits::duration(1)); + on_second_get(); +} + +TEST_F(async_resource_pool_impl, should_waste_resource_when_lifespan_ends) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration(0)); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, set_and_recycle_resource(pool)); + on_first_get(); + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, assert_empty(pool), time_traits::duration(1)); + on_second_get(); +} + +TEST_F(async_resource_pool_impl, should_waste_resource_when_lifespan_ends_and_queue_is_not_empty) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration(0)); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(DoAll(SaveMoveArg2(&on_get_res), Return(true))); + pool.get(io, set_and_recycle_resource(pool)); + pool.get(io, assert_empty(pool), time_traits::duration(1)); + + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(make_queued_value(std::move(on_get_res), io)))); + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + on_first_get(); + on_second_get(); +} + +TEST_F(async_resource_pool_impl, should_waste_used_resource_after_invalidate) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, set_and_recycle_resource(pool)); + pool.invalidate(); + on_first_get(); + + EXPECT_EQ(pool.available(), 0u); + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, assert_empty(pool), time_traits::duration(1)); + on_second_get(); +} + +TEST_F(async_resource_pool_impl, should_waste_available_resource_after_invalidate) { + resource_pool_impl pool([]{ return resource{}; }, 1, 0, time_traits::duration::max(), time_traits::duration::max()); + + pool.invalidate(); + + EXPECT_EQ(pool.available(), 0u); +} + +TEST_F(async_resource_pool_impl, should_restore_wasted_cell) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, set_and_recycle_resource(pool)); + pool.invalidate(); + on_first_get(); + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + pool.get(io, set_and_recycle_resource(pool), time_traits::duration(1)); + on_second_get(); + + EXPECT_EQ(pool.available(), 1u); +} + +TEST_F(async_resource_pool_impl, should_waste_used_resource_after_invalidate_when_queue_is_not_empty) { + resource_pool_impl pool(1, 0, time_traits::duration::max(), time_traits::duration::max()); + + InSequence s; + + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_first_get)); + EXPECT_CALL(pool.queue(), push(_, _, _)).WillOnce(DoAll(SaveMoveArg2(&on_get_res), Return(true))); + pool.get(io, set_and_recycle_resource(pool)); + pool.get(io, assert_empty(pool), time_traits::duration(1)); + pool.invalidate(); + + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(make_queued_value(std::move(on_get_res), io)))); + EXPECT_CALL(executor, post(_)).WillOnce(SaveArg<0>(&on_second_get)); + EXPECT_CALL(pool.queue(), pop()).WillOnce(Return(ByMove(boost::none))); + on_first_get(); + on_second_get(); +} + +} diff --git a/contrib/resource_pool/tests/async/queue.cc b/contrib/resource_pool/tests/async/queue.cc new file mode 100644 index 000000000..1816209b9 --- /dev/null +++ b/contrib/resource_pool/tests/async/queue.cc @@ -0,0 +1,288 @@ +#include "tests.hpp" + +#include + +namespace { + +using namespace tests; +using namespace yamail::resource_pool; +using namespace yamail::resource_pool::async::detail; + +namespace asio = boost::asio; + +struct mocked_timer { + MOCK_CONST_METHOD0(expires_at, time_traits::time_point ()); + MOCK_CONST_METHOD0(cancel, void ()); + MOCK_CONST_METHOD1(expires_at, void (const time_traits::time_point&)); + MOCK_CONST_METHOD1(async_wait, void (std::function)); +}; + +struct timer { + std::unique_ptr impl = std::make_unique(); + + timer(mocked_io_context&) {} + + time_traits::time_point expires_at() const { + return impl->expires_at(); + } + + void cancel() const { + return impl->cancel(); + } + + void expires_at(const time_traits::time_point& v) const { + return impl->expires_at(v); + } + + void async_wait(std::function v) const { + return impl->async_wait(std::move(v)); + } +}; + +using boost::system::error_code; + +struct mocked_callback { + MOCK_CONST_METHOD1(call, void (error_code)); +}; + +using mocked_callback_ptr = std::shared_ptr; + +struct callback { + mocked_callback_ptr impl; + + using result_type = void; + + callback() = default; + + callback(const callback&) = delete; + + callback(callback&&) = default; + + callback(const mocked_callback_ptr& impl) : impl(impl) {} + + callback& operator =(const callback&) = delete; + + callback& operator =(callback&&) = default; + + result_type operator ()(error_code ec) const { + return impl->call(ec); + } + + void reset() { + impl.reset(); + } +}; + +using request_queue = queue; +using request_queue_ptr = std::shared_ptr; + +struct async_request_queue : Test { + StrictMock executor1; + mocked_executor executor_wrapper1 {&executor1}; + mocked_io_context io1 {&executor_wrapper1}; + StrictMock executor2; + mocked_executor executor_wrapper2 {&executor2}; + mocked_io_context io2 {&executor_wrapper2}; + mocked_callback_ptr expired; + std::function on_async_wait; + + async_request_queue() : expired(std::make_shared()) {} + + request_queue_ptr make_queue(std::size_t capacity) { + return std::make_shared(capacity); + } +}; + +TEST_F(async_request_queue, create_const_with_capacity_1_then_check_capacity_should_be_1) { + const request_queue queue(1); + EXPECT_EQ(queue.capacity(), 1u); +} + +TEST_F(async_request_queue, create_const_then_check_size_should_be_0) { + const request_queue queue(1); + EXPECT_EQ(queue.size(), 0u); +} + +TEST_F(async_request_queue, create_const_then_check_empty_should_be_true) { + const request_queue queue(1); + EXPECT_EQ(queue.empty(), true); +} + +TEST_F(async_request_queue, create_const_then_call_timer_should_succeed) { + request_queue queue(1); + EXPECT_NO_THROW(queue.timer(io1)); +} + +TEST_F(async_request_queue, create_ptr_then_call_shared_from_this_should_return_equal) { + const request_queue_ptr queue = make_queue(1); + EXPECT_EQ(queue->shared_from_this(), queue); +} + +TEST_F(async_request_queue, push_then_timeout_request_queue_should_be_empty) { + request_queue_ptr queue = make_queue(1); + + InSequence s; + + EXPECT_CALL(*queue->timer(io1).impl, expires_at(_)).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, async_wait(_)).WillOnce(SaveArg<0>(&on_async_wait)); + EXPECT_CALL(executor1, post(_)).WillOnce(InvokeArgument<0>()); + EXPECT_CALL(*expired, call(_)).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, cancel()).WillOnce(Return()); + + ASSERT_TRUE(queue->push(io1, time_traits::duration(0), callback(expired))); + + on_async_wait(error_code()); + + EXPECT_TRUE(queue->empty()); +} + +TEST_F(async_request_queue, push_then_pop_should_return_request) { + request_queue_ptr queue = make_queue(1); + + InSequence s; + + EXPECT_CALL(*queue->timer(io1).impl, expires_at(_)).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, async_wait(_)).WillOnce(SaveArg<0>(&on_async_wait)); + EXPECT_CALL(*queue->timer(io1).impl, cancel()).WillOnce(Return()); + EXPECT_CALL(*expired, call(_)).Times(0); + + EXPECT_TRUE(queue->push(io1, time_traits::duration(1), callback(expired))); + + using namespace boost::system::errc; + + on_async_wait(error_code(make_error_code(operation_canceled))); + + EXPECT_FALSE(queue->empty()); + const auto result = queue->pop(); + ASSERT_TRUE(result); + EXPECT_EQ(&result->io_context, &io1); + EXPECT_EQ(result->request.impl, expired); +} + +TEST_F(async_request_queue, push_into_queue_with_null_capacity_should_return_error) { + request_queue_ptr queue = make_queue(0); + + const bool result = queue->push(io1, time_traits::duration(0), callback(expired)); + EXPECT_FALSE(result); +} + +TEST_F(async_request_queue, pop_from_empty_should_return_error) { + request_queue_ptr queue = make_queue(1); + + EXPECT_TRUE(queue->empty()); + EXPECT_FALSE(queue->pop()); +} + +TEST_F(async_request_queue, push_twice_with_different_io_contexts_then_pop_twice_should_return_both_requests) { + auto& expired1 = expired; + auto expired2 = std::make_shared(); + const auto queue = make_queue(2); + + Sequence s; + + (void) queue->timer(io1); + (void) queue->timer(io2); + + EXPECT_CALL(*queue->timer(io1).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, async_wait(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, async_wait(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, async_wait(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, cancel()).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, cancel()).WillOnce(Return()); + EXPECT_CALL(*expired1, call(_)).Times(0); + EXPECT_CALL(*expired2, call(_)).Times(0); + + EXPECT_TRUE(queue->push(io1, time_traits::duration(1), callback(expired1))); + EXPECT_TRUE(queue->push(io2, time_traits::duration(1), callback(expired2))); + + using namespace boost::system::errc; + + ASSERT_FALSE(queue->empty()); + + const auto result1 = queue->pop(); + ASSERT_TRUE(result1); + EXPECT_EQ(&result1->io_context, &io1); + EXPECT_EQ(result1->request.impl, expired1); + + const auto result2 = queue->pop(); + ASSERT_TRUE(result2); + EXPECT_EQ(&result2->io_context, &io2); + EXPECT_EQ(result2->request.impl, expired2); +} + +TEST_F(async_request_queue, push_twice_with_different_io_contexts_where_second_expires_before_first_then_pop_twice_should_return_both_requests) { + auto& expired1 = expired; + auto expired2 = std::make_shared(); + const auto queue = make_queue(2); + + Sequence s; + + (void) queue->timer(io1); + (void) queue->timer(io2); + + EXPECT_CALL(*queue->timer(io1).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, async_wait(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, async_wait(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, async_wait(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, cancel()).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, cancel()).WillOnce(Return()); + EXPECT_CALL(*expired1, call(_)).Times(0); + EXPECT_CALL(*expired2, call(_)).Times(0); + + EXPECT_TRUE(queue->push(io1, time_traits::duration::max(), callback(expired1))); + EXPECT_TRUE(queue->push(io2, time_traits::duration::max() / 2, callback(expired2))); + + using namespace boost::system::errc; + + ASSERT_FALSE(queue->empty()); + + const auto result1 = queue->pop(); + ASSERT_TRUE(result1); + EXPECT_EQ(&result1->io_context, &io1); + EXPECT_EQ(result1->request.impl, expired1); + + const auto result2 = queue->pop(); + ASSERT_TRUE(result2); + EXPECT_EQ(&result2->io_context, &io2); + EXPECT_EQ(result2->request.impl, expired2); +} + +TEST_F(async_request_queue, push_twice_with_different_io_serivices_and_timeout_both_requests_then_queue_should_be_empty) { + auto& expired1 = expired; + auto& on_async_wait1 = on_async_wait; + auto expired2 = std::make_shared(); + std::function on_async_wait2; + const auto queue = make_queue(2); + + Sequence s; + + (void) queue->timer(io1); + (void) queue->timer(io2); + + EXPECT_CALL(*queue->timer(io1).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, async_wait(_)).InSequence(s).WillOnce(SaveArg<0>(&on_async_wait1)); + EXPECT_CALL(*queue->timer(io1).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, async_wait(_)).InSequence(s).WillOnce(SaveArg<0>(&on_async_wait1)); + EXPECT_CALL(executor1, post(_)).InSequence(s).WillOnce(InvokeArgument<0>()); + EXPECT_CALL(*expired1, call(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, expires_at(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, async_wait(_)).InSequence(s).WillOnce(SaveArg<0>(&on_async_wait2)); + EXPECT_CALL(executor2, post(_)).InSequence(s).WillOnce(InvokeArgument<0>()); + EXPECT_CALL(*expired2, call(_)).InSequence(s).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io1).impl, cancel()).WillOnce(Return()); + EXPECT_CALL(*queue->timer(io2).impl, cancel()).WillOnce(Return()); + + ASSERT_TRUE(queue->push(io1, time_traits::duration(0), callback(expired1))); + ASSERT_TRUE(queue->push(io2, time_traits::duration(0), callback(expired2))); + + on_async_wait1(error_code()); + on_async_wait2(error_code()); + + EXPECT_TRUE(queue->empty()); +} + +} diff --git a/contrib/resource_pool/tests/async/tests.hpp b/contrib/resource_pool/tests/async/tests.hpp new file mode 100644 index 000000000..a40fa8923 --- /dev/null +++ b/contrib/resource_pool/tests/async/tests.hpp @@ -0,0 +1,126 @@ +#ifndef YAMAIL_RESOURCE_POOL_TEST_ASYNC_TESTS_HPP +#define YAMAIL_RESOURCE_POOL_TEST_ASYNC_TESTS_HPP + +#include +#include + +#include +#include + +#include +#include + +#include + +namespace tests { + +namespace asio = boost::asio; + +using namespace testing; +using namespace yamail::resource_pool; + +struct resource { + resource() = default; + resource(const resource&) = delete; + resource(resource&&) = default; + resource& operator =(const resource&) = delete; + resource& operator =(resource&&) = default; +}; + +struct request { + int value; +}; + +struct executor_mock { + virtual ~executor_mock() = default; + virtual void on_work_started() const = 0; + virtual void on_work_finished() const = 0; + virtual void dispatch(std::function) const = 0; + virtual void post(std::function) const = 0; + virtual void defer(std::function) const = 0; +}; + +struct executor_gmock : executor_mock { + MOCK_CONST_METHOD0(on_work_started, void ()); + MOCK_CONST_METHOD0(on_work_finished, void ()); + MOCK_CONST_METHOD1(dispatch, void (std::function)); + MOCK_CONST_METHOD1(post, void (std::function)); + MOCK_CONST_METHOD1(defer, void (std::function)); +}; + +template +struct shared_wrapper { + std::shared_ptr ptr; + + template + void operator ()(Args&& ... args) { + return (*ptr)(std::forward(args) ...); + } +}; + +template +auto wrap_shared(Function&& f) { + return shared_wrapper> {std::make_shared>(std::forward(f))}; +} + +struct mocked_executor { + const executor_mock* impl = nullptr; + asio::execution_context* context_ = nullptr; + + asio::execution_context& context() noexcept { + return *context_; + } + + void on_work_started() const { + return impl->on_work_started(); + } + + void on_work_finished() const { + return impl->on_work_finished(); + } + + template + void dispatch(Function&& f, Allocator&&) const { + return impl->dispatch(wrap_shared(std::forward(f))); + } + + template + void post(Function&& f, Allocator&&) const { + return impl->post(wrap_shared(std::forward(f))); + } + + template + void defer(Function&& f, Allocator&&) const { + return impl->defer(wrap_shared(std::forward(f))); + } + + friend bool operator ==(const mocked_executor& lhs, const mocked_executor& rhs) { + return lhs.context_ == rhs.context_ && lhs.impl == rhs.impl; + } +}; + +struct mocked_io_context : asio::execution_context { + using executor_type = mocked_executor; + + executor_type* executor = nullptr; + + mocked_io_context(executor_type* executor) + : executor(executor) {} + + executor_type get_executor() const { + return *executor; + } +}; + +} // namespace tests + +namespace boost { +namespace asio { + +template <> +struct is_executor : true_type {}; + +} // namespace asio +} // namespace boost + +#endif // YAMAIL_RESOURCE_POOL_TEST_ASYNC_TESTS_HPP diff --git a/contrib/resource_pool/tests/cmake/FindGMock.cmake b/contrib/resource_pool/tests/cmake/FindGMock.cmake new file mode 100644 index 000000000..6979c5876 --- /dev/null +++ b/contrib/resource_pool/tests/cmake/FindGMock.cmake @@ -0,0 +1,332 @@ +# Get the Google C++ Mocking Framework. +# (This file is almost an copy of the original FindGTest.cmake file, +# altered to download and compile GMock and GTest if not found +# in GMOCK_ROOT or GTEST_ROOT respectively, +# feel free to use it as it is or modify it for your own needs.) +# +# Defines the following variables: +# +# GMOCK_FOUND - Found or got the Google Mocking framework +# GTEST_FOUND - Found or got the Google Testing framework +# GMOCK_INCLUDE_DIRS - GMock include directory +# GTEST_INCLUDE_DIRS - GTest include direcotry +# +# Also defines the library variables below as normal variables +# +# GMOCK_BOTH_LIBRARIES - Both libgmock & libgmock_main +# GMOCK_LIBRARIES - libgmock +# GMOCK_MAIN_LIBRARIES - libgmock-main +# +# GTEST_BOTH_LIBRARIES - Both libgtest & libgtest_main +# GTEST_LIBRARIES - libgtest +# GTEST_MAIN_LIBRARIES - libgtest_main +# +# Accepts the following variables as input: +# +# GMOCK_ROOT - The root directory of the gmock install prefix +# GTEST_ROOT - The root directory of the gtest install prefix +# GMOCK_SRC_DIR -The directory of the gmock sources +# GMOCK_VER - The version of the gmock sources to be downloaded +# +#----------------------- +# Example Usage: +# +# set(GMOCK_ROOT "~/gmock") +# find_package(GMock REQUIRED) +# include_directories(${GMOCK_INCLUDE_DIRS}) +# +# add_executable(foo foo.cc) +# target_link_libraries(foo ${GMOCK_BOTH_LIBRARIES}) +# +#============================================================================= +# Copyright (c) 2016 Michel Estermann +# Copyright (c) 2016 Kamil Strzempowicz +# Copyright (c) 2011 Matej Svec +# +# CMake - Cross Platform Makefile Generator +# Copyright 2000-2016 Kitware, Inc. +# Copyright 2000-2011 Insight Software Consortium +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the names of Kitware, Inc., the Insight Software Consortium, +# nor the names of their contributors may be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# ------------------------------------------------------------------------------ +# +# The above copyright and license notice applies to distributions of +# CMake in source and binary form. Some source files contain additional +# notices of original copyright by their contributors; see each source +# for details. Third-party software packages supplied with CMake under +# compatible licenses provide their own copyright notices documented in +# corresponding subdirectories. +# +# ------------------------------------------------------------------------------ +# +# CMake was initially developed by Kitware with the following sponsorship: +# +# * National Library of Medicine at the National Institutes of Health +# as part of the Insight Segmentation and Registration Toolkit (ITK). +# +# * US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel +# Visualization Initiative. +# +# * National Alliance for Medical Image Computing (NAMIC) is funded by the +# National Institutes of Health through the NIH Roadmap for Medical Research, +# Grant U54 EB005149. +# +# * Kitware, Inc. +#============================================================================= +# Thanks to Daniel Blezek for the GTEST_ADD_TESTS code + +function(gtest_add_tests executable extra_args) + if(NOT ARGN) + message(FATAL_ERROR "Missing ARGN: Read the documentation for GTEST_ADD_TESTS") + endif() + if(ARGN STREQUAL "AUTO") + # obtain sources used for building that executable + get_property(ARGN TARGET ${executable} PROPERTY SOURCES) + endif() + set(gtest_case_name_regex ".*\\( *([A-Za-z_0-9]+) *, *([A-Za-z_0-9]+) *\\).*") + set(gtest_test_type_regex "(TYPED_TEST|TEST_?[FP]?)") + foreach(source ${ARGN}) + file(READ "${source}" contents) + string(REGEX MATCHALL "${gtest_test_type_regex} *\\(([A-Za-z_0-9 ,]+)\\)" found_tests ${contents}) + foreach(hit ${found_tests}) + string(REGEX MATCH "${gtest_test_type_regex}" test_type ${hit}) + + # Parameterized tests have a different signature for the filter + if("x${test_type}" STREQUAL "xTEST_P") + string(REGEX REPLACE ${gtest_case_name_regex} "*/\\1.\\2/*" test_name ${hit}) + elseif("x${test_type}" STREQUAL "xTEST_F" OR "x${test_type}" STREQUAL "xTEST") + string(REGEX REPLACE ${gtest_case_name_regex} "\\1.\\2" test_name ${hit}) + elseif("x${test_type}" STREQUAL "xTYPED_TEST") + string(REGEX REPLACE ${gtest_case_name_regex} "\\1/*.\\2" test_name ${hit}) + else() + message(WARNING "Could not parse GTest ${hit} for adding to CTest.") + continue() + endif() + add_test(NAME ${test_name} COMMAND ${executable} --gtest_filter=${test_name} ${extra_args}) + endforeach() + endforeach() +endfunction() + +function(_append_debugs _endvar _library) + if(${_library} AND ${_library}_DEBUG) + set(_output optimized ${${_library}} debug ${${_library}_DEBUG}) + else() + set(_output ${${_library}}) + endif() + set(${_endvar} ${_output} PARENT_SCOPE) +endfunction() + +function(_gmock_find_library _name) + find_library(${_name} + NAMES ${ARGN} + HINTS + ENV GMOCK_ROOT + ${GMOCK_ROOT} + PATH_SUFFIXES ${_gmock_libpath_suffixes} + ) + mark_as_advanced(${_name}) +endfunction() + +function(_gtest_find_library _name) + find_library(${_name} + NAMES ${ARGN} + HINTS + ENV GTEST_ROOT + ${GTEST_ROOT} + PATH_SUFFIXES ${_gtest_libpath_suffixes} + ) + mark_as_advanced(${_name}) +endfunction() + +if(NOT DEFINED GMOCK_MSVC_SEARCH) + set(GMOCK_MSVC_SEARCH MD) +endif() + +set(_gmock_libpath_suffixes lib) +set(_gtest_libpath_suffixes lib) +if(MSVC) + if(GMOCK_MSVC_SEARCH STREQUAL "MD") + list(APPEND _gmock_libpath_suffixes + msvc/gmock-md/Debug + msvc/gmock-md/Release) + list(APPEND _gtest_libpath_suffixes + msvc/gtest-md/Debug + msvc/gtest-md/Release) + elseif(GMOCK_MSVC_SEARCH STREQUAL "MT") + list(APPEND _gmock_libpath_suffixes + msvc/gmock/Debug + msvc/gmock/Release) + list(APPEND _gtest_libpath_suffixes + msvc/gtest/Debug + msvc/gtest/Release) + endif() +endif() + +find_path(GMOCK_INCLUDE_DIR gmock/gmock.h + HINTS + $ENV{GMOCK_ROOT}/include + ${GMOCK_ROOT}/include + ) +mark_as_advanced(GMOCK_INCLUDE_DIR) + +find_path(GTEST_INCLUDE_DIR gtest/gtest.h + HINTS + $ENV{GTEST_ROOT}/include + ${GTEST_ROOT}/include + ) +mark_as_advanced(GTEST_INCLUDE_DIR) + +if(MSVC AND GMOCK_MSVC_SEARCH STREQUAL "MD") + # The provided /MD project files for Google Mock add -md suffixes to the + # library names. + _gmock_find_library(GMOCK_LIBRARY gmock-md gmock) + _gmock_find_library(GMOCK_LIBRARY_DEBUG gmock-mdd gmockd) + _gmock_find_library(GMOCK_MAIN_LIBRARY gmock_main-md gmock_main) + _gmock_find_library(GMOCK_MAIN_LIBRARY_DEBUG gmock_main-mdd gmock_maind) + + _gtest_find_library(GTEST_LIBRARY gtest-md gtest) + _gtest_find_library(GTEST_LIBRARY_DEBUG gtest-mdd gtestd) + _gtest_find_library(GTEST_MAIN_LIBRARY gtest_main-md gtest_main) + _gtest_find_library(GTEST_MAIN_LIBRARY_DEBUG gtest_main-mdd gtest_maind) +else() + _gmock_find_library(GMOCK_LIBRARY gmock) + _gmock_find_library(GMOCK_LIBRARY_DEBUG gmockd) + _gmock_find_library(GMOCK_MAIN_LIBRARY gmock_main) + _gmock_find_library(GMOCK_MAIN_LIBRARY_DEBUG gmock_maind) + + _gtest_find_library(GTEST_LIBRARY gtest) + _gtest_find_library(GTEST_LIBRARY_DEBUG gtestd) + _gtest_find_library(GTEST_MAIN_LIBRARY gtest_main) + _gtest_find_library(GTEST_MAIN_LIBRARY_DEBUG gtest_maind) +endif() + +if(NOT TARGET GTest::GTest) + add_library(GTest::GTest UNKNOWN IMPORTED) +endif() +if(NOT TARGET GTest::Main) + add_library(GTest::Main UNKNOWN IMPORTED) +endif() + +if(NOT TARGET GMock::GMock) + add_library(GMock::GMock UNKNOWN IMPORTED) +endif() + +if(NOT TARGET GMock::Main) + add_library(GMock::Main UNKNOWN IMPORTED) +endif() + +set(GMOCK_LIBRARY_EXISTS OFF) +set(GTEST_LIBRARY_EXISTS OFF) + +if(EXISTS "${GMOCK_LIBRARY}" OR EXISTS "${GMOCK_LIBRARY_DEBUG}" AND GMOCK_INCLUDE_DIR) + set(GMOCK_LIBRARY_EXISTS ON) +endif() + +if(EXISTS "${GTEST_LIBRARY}" OR EXISTS "${GTEST_LIBRARY_DEBUG}" AND GTEST_INCLUDE_DIR) + set(GTEST_LIBRARY_EXISTS ON) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GTest DEFAULT_MSG GTEST_LIBRARY GTEST_INCLUDE_DIR GTEST_MAIN_LIBRARY) +find_package_handle_standard_args(GMock DEFAULT_MSG GMOCK_LIBRARY GMOCK_INCLUDE_DIR GMOCK_MAIN_LIBRARY) + +include(CMakeFindDependencyMacro) +find_dependency(Threads) + +set_target_properties(GTest::GTest PROPERTIES + INTERFACE_LINK_LIBRARIES "Threads::Threads" + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" + IMPORTED_LOCATION "${GTEST_LIBRARY}" + ) + +if(GTEST_INCLUDE_DIR) + set_target_properties(GTest::GTest PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}" + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}" + ) +endif() + +set_target_properties(GTest::Main PROPERTIES + INTERFACE_LINK_LIBRARIES "GTest::GTest" + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" + IMPORTED_LOCATION "${GTEST_MAIN_LIBRARY}") + +set_target_properties(GMock::GMock PROPERTIES + INTERFACE_LINK_LIBRARIES "Threads::Threads" + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" + IMPORTED_LOCATION "${GMOCK_LIBRARY}") + +if(GMOCK_INCLUDE_DIR) + set_target_properties(GMock::GMock PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${GMOCK_INCLUDE_DIR}" + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${GMOCK_INCLUDE_DIR}" + ) + if(GMOCK_VER VERSION_LESS "1.7") + # GMock 1.6 still has GTest as an external link-time dependency, + # so just specify it on the link interface. + set_property(TARGET GMock::GMock APPEND PROPERTY + INTERFACE_LINK_LIBRARIES GTest::GTest) + elseif(GTEST_INCLUDE_DIR) + # GMock 1.7 and beyond doesn't have it as a link-time dependency anymore, + # so merge it's compile-time interface (include dirs) with ours. + set_property(TARGET GMock::GMock APPEND PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}") + set_property(TARGET GMock::GMock APPEND PROPERTY + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${GTEST_INCLUDE_DIR}") + endif() +endif() + +set_target_properties(GMock::Main PROPERTIES + INTERFACE_LINK_LIBRARIES "GMock::GMock" + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" + IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}") + +if(GTEST_FOUND) + set(GTEST_INCLUDE_DIRS ${GTEST_INCLUDE_DIR}) + set(GTEST_LIBRARIES GTest::GTest) + set(GTEST_MAIN_LIBRARIES GTest::Main) + set(GTEST_BOTH_LIBRARIES ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES}) + if(VERBOSE) + message(STATUS "GTest includes: ${GTEST_INCLUDE_DIRS}") + message(STATUS "GTest libs: ${GTEST_BOTH_LIBRARIES}") + endif() +endif() + +if(GMOCK_FOUND) + set(GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIR}) + set(GMOCK_LIBRARIES GMock::GMock) + set(GMOCK_MAIN_LIBRARIES GMock::Main) + set(GMOCK_BOTH_LIBRARIES ${GMOCK_LIBRARIES} ${GMOCK_MAIN_LIBRARIES}) + if(VERBOSE) + message(STATUS "GMock includes: ${GMOCK_INCLUDE_DIRS}") + message(STATUS "GMock libs: ${GMOCK_BOTH_LIBRARIES}") + endif() +endif() diff --git a/contrib/resource_pool/tests/error.cc b/contrib/resource_pool/tests/error.cc new file mode 100644 index 000000000..672eb0775 --- /dev/null +++ b/contrib/resource_pool/tests/error.cc @@ -0,0 +1,47 @@ +#include + +#include + +#include + +namespace { + +using namespace testing; +using namespace yamail::resource_pool; +using namespace yamail::resource_pool::error; + +using boost::system::error_code; + +struct error_test : Test {}; + +TEST(error_test, make_no_error_and_check_message) { + const error_code error = make_error_code(code(ok)); + EXPECT_EQ(error.message(), "no error"); +} + +TEST(error_test, make_get_resource_timeout_error_and_check_message) { + const error_code error = make_error_code(get_resource_timeout); + EXPECT_EQ(error.message(), "get resource timeout"); +} + +TEST(error_test, make_request_queue_overflow_error_and_check_message) { + const error_code error = make_error_code(request_queue_overflow); + EXPECT_EQ(error.message(), "request queue overflow"); +} + +TEST(error_test, make_disabled_error_and_check_message) { + const error_code error = make_error_code(disabled); + EXPECT_EQ(error.message(), "resource pool is disabled"); +} + +TEST(error_test, make_out_of_range_error_and_check_message) { + const error_code error = make_error_code(code(std::numeric_limits::max())); + EXPECT_THROW(error.message(), std::logic_error); +} + +TEST(error_test, make_no_error_and_category_name) { + const error_code error = make_error_code(code(ok)); + EXPECT_STREQ(error.category().name(), "yamail::resource_pool::error::detail::category"); +} + +} diff --git a/contrib/resource_pool/tests/handle.cc b/contrib/resource_pool/tests/handle.cc new file mode 100644 index 000000000..6da0d9caf --- /dev/null +++ b/contrib/resource_pool/tests/handle.cc @@ -0,0 +1,102 @@ +#include + +#include +#include + +#include + +#include + +namespace { + +using namespace testing; + +using yamail::resource_pool::time_traits; +using yamail::resource_pool::detail::pool_returns; + +struct handle_test : Test {}; + +struct resource { + int value = 0; + + resource(int value = 0) : value(value) {} + resource(const resource&) = delete; + resource(resource&&) = default; + resource& operator =(const resource &) = delete; + resource& operator =(resource &&) = default; +}; + +using list_iterator = yamail::resource_pool::detail::cell_iterator; + +struct pool_impl_mock : pool_returns { + MOCK_METHOD1(recycle, void (list_iterator)); + MOCK_METHOD1(waste, void (list_iterator)); +}; + +using idle = yamail::resource_pool::detail::idle; +using resource_handle = yamail::resource_pool::handle; + +TEST(handle_test, construct_usable_should_be_not_unusable) { + std::list resources; + resources.emplace_back(); + const auto pool_impl = std::make_shared>(); + const resource_handle handle(pool_impl, &resource_handle::waste, resources.begin()); + EXPECT_FALSE(handle.unusable()); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); +} + +TEST(handle_test, construct_usable_and_move_then_destination_should_contain_value) { + std::list resources; + resources.emplace_back(); + const auto pool_impl = std::make_shared>(); + resource_handle src(pool_impl, &resource_handle::waste, resources.begin()); + const resource_handle dst = std::move(src); + EXPECT_FALSE(dst.unusable()); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); +} + +TEST(handle_test, construct_usable_and_move_over_assign_then_destination_should_contain_value) { + std::list resources; + resources.emplace_back(); + const auto pool_impl = std::make_shared>(); + resource_handle src(pool_impl, &resource_handle::waste, resources.begin()); + resource_handle dst; + dst = std::move(src); + EXPECT_FALSE(dst.unusable()); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); +} + +TEST(handle_test, construct_usable_then_get_should_return_value) { + std::list resources; + resources.emplace_back(resource(42), time_traits::time_point(), time_traits::time_point()); + auto pool_impl = std::make_shared>(); + resource_handle handle(pool_impl, &resource_handle::waste, resources.begin()); + EXPECT_EQ(42, handle->value); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); +} + +TEST(handle_test, construct_usable_then_get_const_should_return_value) { + std::list resources; + resources.emplace_back(resource(42), time_traits::time_point(), time_traits::time_point()); + auto pool_impl = std::make_shared>(); + const resource_handle handle(pool_impl, &resource_handle::waste, resources.begin()); + EXPECT_EQ(42, handle->value); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); +} + +TEST(handle_test, move_to_usable_should_release_replaced_resource) { + std::list resources(2); + const auto pool_impl = std::make_shared>(); + const auto src_res = resources.begin(); + const auto dst_res = std::next(resources.begin()); + resource_handle src(pool_impl, &resource_handle::waste, src_res); + resource_handle dst(pool_impl, &resource_handle::waste, dst_res); + + EXPECT_CALL(*pool_impl, waste(dst_res)).WillOnce(Return()); + dst = std::move(src); + + EXPECT_FALSE(dst.unusable()); + EXPECT_CALL(*pool_impl, waste(src_res)).WillOnce(Return()); +} + +} diff --git a/contrib/resource_pool/tests/main.cc b/contrib/resource_pool/tests/main.cc new file mode 100644 index 000000000..ff1678257 --- /dev/null +++ b/contrib/resource_pool/tests/main.cc @@ -0,0 +1,6 @@ +#include + +int main(int argc, char *argv[]) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/contrib/resource_pool/tests/sync/pool.cc b/contrib/resource_pool/tests/sync/pool.cc new file mode 100644 index 000000000..b8bb75e4f --- /dev/null +++ b/contrib/resource_pool/tests/sync/pool.cc @@ -0,0 +1,231 @@ +#include + +#include +#include + +namespace { + +using namespace testing; +using namespace yamail::resource_pool; +using namespace yamail::resource_pool::sync; + +struct resource { + resource() = default; + resource(const resource&) = delete; + resource(resource&&) = default; + resource& operator =(const resource&) = delete; + resource& operator =(resource&&) = default; +}; + +using yamail::resource_pool::detail::pool_returns; + +struct mocked_pool_impl : pool_returns { + using value_type = resource; + using idle = yamail::resource_pool::detail::idle; + using list = std::list; + using list_iterator = list::iterator; + using get_result = std::pair; + + MOCK_CONST_METHOD0(capacity, std::size_t ()); + MOCK_CONST_METHOD0(size, std::size_t ()); + MOCK_CONST_METHOD0(available, std::size_t ()); + MOCK_CONST_METHOD0(used, std::size_t ()); + MOCK_CONST_METHOD0(stats, sync::stats ()); + MOCK_METHOD1(get, get_result (time_traits::duration)); + MOCK_METHOD1(recycle, void (list_iterator)); + MOCK_METHOD1(waste, void (list_iterator)); + MOCK_METHOD0(disable, void ()); +}; + +using resource_pool = pool>; + +struct sync_resource_pool : Test { + mocked_pool_impl::list resources; + mocked_pool_impl::list_iterator resource_iterator; + + sync_resource_pool() + : resources(1), resource_iterator(resources.begin()) {} +}; + +TEST_F(sync_resource_pool, create_without_mocks_should_succeed) { + pool(1); +} + +TEST_F(sync_resource_pool, call_capacity_should_call_impl_capacity) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, capacity()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.capacity(); +} + +TEST_F(sync_resource_pool, call_size_should_call_impl_size) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, size()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.size(); +} + +TEST_F(sync_resource_pool, call_available_should_call_impl_available) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, available()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.available(); +} + +TEST_F(sync_resource_pool, call_used_should_call_impl_used) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, used()).WillOnce(Return(0)); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.used(); +} + +TEST_F(sync_resource_pool, call_stats_should_call_impl_stats) { + const auto pool_impl = std::make_shared>(); + const resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, stats()).WillOnce(Return(sync::stats {0, 0, 0})); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + pool.stats(); +} + +TEST_F(sync_resource_pool, move_than_dtor_should_call_disable_only_for_destination) { + const auto pool_impl = std::make_shared>(); + resource_pool src(pool_impl); + + EXPECT_CALL(*pool_impl, disable()).Times(0); + + const auto dst = std::move(src); + + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); +} + +TEST_F(sync_resource_pool, get_auto_recylce_handle_should_call_recycle) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, get(_)).WillOnce(Return(mocked_pool_impl::get_result(boost::system::error_code(), resource_iterator))); + EXPECT_CALL(*pool_impl, recycle(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + auto res = pool.get_auto_recycle(); + auto& ec = res.first; + auto& handle = res.second; + + EXPECT_EQ(ec, boost::system::error_code()); + EXPECT_FALSE(handle.unusable()); + EXPECT_TRUE(handle.empty()); + EXPECT_NO_THROW(handle.reset(resource {})); + EXPECT_NO_THROW(handle.get()); +} + +TEST_F(sync_resource_pool, get_auto_waste_handle_should_call_waste) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, get(_)).WillOnce(Return(mocked_pool_impl::get_result(boost::system::error_code(), resource_iterator))); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + const auto res = pool.get_auto_waste(); + const auto& handle = res.second; + + EXPECT_FALSE(handle.unusable()); +} + +TEST_F(sync_resource_pool, get_auto_recylce_handle_and_recycle_should_call_recycle_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, get(_)).WillOnce(Return(mocked_pool_impl::get_result(boost::system::error_code(), resource_iterator))); + EXPECT_CALL(*pool_impl, recycle(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + auto res = pool.get_auto_recycle(); + auto& handle = res.second; + + handle.recycle(); + + EXPECT_TRUE(handle.unusable()); + EXPECT_THROW(handle.recycle(), error::unusable_handle); + EXPECT_TRUE(handle.empty()); + EXPECT_THROW(handle.get(), error::empty_handle); +} + +TEST_F(sync_resource_pool, get_auto_recylce_handle_and_waste_should_call_waste_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, get(_)).WillOnce(Return(mocked_pool_impl::get_result(boost::system::error_code(), resource_iterator))); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + auto res = pool.get_auto_recycle(); + auto& handle = res.second; + + handle.waste(); +} + +TEST_F(sync_resource_pool, get_auto_waste_handle_and_recycle_should_call_recycle_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, get(_)).WillOnce(Return(mocked_pool_impl::get_result(boost::system::error_code(), resource_iterator))); + EXPECT_CALL(*pool_impl, recycle(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + auto res = pool.get_auto_waste(); + auto& handle = res.second; + + handle.recycle(); +} + +TEST_F(sync_resource_pool, get_auto_waste_handle_and_waste_should_call_waste_once) { + const auto pool_impl = std::make_shared>(); + resource_pool pool(pool_impl); + + InSequence s; + + EXPECT_CALL(*pool_impl, get(_)).WillOnce(Return(mocked_pool_impl::get_result(boost::system::error_code(), resource_iterator))); + EXPECT_CALL(*pool_impl, waste(_)).WillOnce(Return()); + EXPECT_CALL(*pool_impl, disable()).WillOnce(Return()); + + auto res = pool.get_auto_waste(); + auto& handle = res.second; + + handle.waste(); +} + +} diff --git a/contrib/resource_pool/tests/sync/pool_impl.cc b/contrib/resource_pool/tests/sync/pool_impl.cc new file mode 100644 index 000000000..6d2ede362 --- /dev/null +++ b/contrib/resource_pool/tests/sync/pool_impl.cc @@ -0,0 +1,291 @@ +#include + +#include +#include + +namespace { + +using namespace testing; +using namespace yamail::resource_pool; +using namespace yamail::resource_pool::sync::detail; + +using std::make_shared; + +struct resource {}; + +struct mocked_condition_variable { + MOCK_CONST_METHOD0(notify_one, void ()); + MOCK_CONST_METHOD0(notify_all, void ()); + MOCK_CONST_METHOD2(wait_for, std::cv_status (std::unique_lock&, time_traits::duration)); +}; + +using resource_pool_impl = pool_impl; +using get_result = resource_pool_impl::get_result; +using resource_ptr_list_iterator = resource_pool_impl::list_iterator; + +struct sync_resource_pool_impl : Test {}; + +TEST(sync_resource_pool_impl, create_with_zero_capacity_should_throw_exception) { + EXPECT_THROW(resource_pool_impl(0, time_traits::duration::max(), time_traits::duration::max()), error::zero_pool_capacity); +} + +TEST(sync_resource_pool_impl, create_with_non_zero_capacity_then_check) { + const resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.capacity(), 1u); +} + +TEST(sync_resource_pool_impl, create_then_check_size_should_be_0) { + const resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.size(), 0u); +} + +TEST(sync_resource_pool_impl, create_then_check_available_should_be_0) { + const resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.available(), 0u); +} + +TEST(sync_resource_pool_impl, create_then_check_used_should_be_0) { + const resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_EQ(pool.used(), 0u); +} + +TEST(sync_resource_pool_impl, create_const_then_check_stats_should_be_0_0_0) { + const resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + const sync::stats expected {0, 0, 0}; + const auto actual = pool.stats(); + + EXPECT_EQ(actual.size, expected.size); + EXPECT_EQ(actual.available, expected.available); + EXPECT_EQ(actual.used, expected.used); +} + +TEST(sync_resource_pool_impl, get_one_should_succeed) { + resource_pool_impl pool_impl(1, time_traits::duration::max(), time_traits::duration::max()); + const get_result res = pool_impl.get(); + EXPECT_EQ(res.first, boost::system::error_code()); +} + +TEST(sync_resource_pool_impl, get_one_and_recycle_should_succeed) { + resource_pool_impl pool_impl(1, time_traits::duration::max(), time_traits::duration::max()); + const get_result res = pool_impl.get(); + EXPECT_EQ(res.first, boost::system::error_code()); + EXPECT_CALL(pool_impl.has_capacity(), notify_one()).WillOnce(Return()); + pool_impl.recycle(res.second); +} + +TEST(sync_resource_pool_impl, get_one_and_waste_should_succeed) { + resource_pool_impl pool_impl(1, time_traits::duration::max(), time_traits::duration::max()); + const get_result res = pool_impl.get(); + EXPECT_EQ(res.first, boost::system::error_code()); + EXPECT_CALL(pool_impl.has_capacity(), notify_one()).WillOnce(Return()); + pool_impl.waste(res.second); +} + +TEST(sync_resource_pool_impl, get_more_than_capacity_returns_error) { + resource_pool_impl pool_impl(1, time_traits::duration::max(), time_traits::duration::max()); + pool_impl.get(); + EXPECT_CALL(pool_impl.has_capacity(), wait_for(_, _)).WillOnce(Return(std::cv_status::timeout)); + EXPECT_EQ(pool_impl.get().first, make_error_code(error::get_resource_timeout)); +} + +TEST(sync_resource_pool_impl, get_after_disable_capacity_returns_error) { + resource_pool_impl pool_impl(1, time_traits::duration::max(), time_traits::duration::max()); + EXPECT_CALL(pool_impl.has_capacity(), notify_all()).WillOnce(Return()); + pool_impl.disable(); + EXPECT_EQ(pool_impl.get().first, make_error_code(error::disabled)); +} + +struct handle_resource { + using strategy_type = void (resource_pool_impl::*)(resource_ptr_list_iterator); + + resource_pool_impl& pool; + resource_ptr_list_iterator res_it; + strategy_type strategy; + + handle_resource(resource_pool_impl& pool, resource_ptr_list_iterator res_it, strategy_type strategy) + : pool(pool), res_it(res_it), strategy(strategy) {} + + std::cv_status operator ()(std::unique_lock& lock, time_traits::duration) const { + lock.unlock(); + (pool.*strategy)(res_it); + lock.lock(); + return std::cv_status::no_timeout; + } +}; + +struct recycle_resource : handle_resource { + recycle_resource(resource_pool_impl& pool, resource_ptr_list_iterator res_it) + : handle_resource(pool, res_it, &resource_pool_impl::recycle) {} +}; + +TEST(sync_resource_pool_impl, get_from_pool_and_wait_then_after_recycle_should_allocate) { + resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + const get_result& first_res = pool.get(); + + EXPECT_EQ(first_res.first, boost::system::error_code()); + first_res.second->value = resource {}; + first_res.second->reset_time = time_traits::now(); + + InSequence s; + + EXPECT_CALL(pool.has_capacity(), wait_for(_, _)).WillOnce(Invoke(recycle_resource(pool, first_res.second))); + EXPECT_CALL(pool.has_capacity(), notify_one()).WillOnce(Return()); + + const get_result& second_res = pool.get(); + + EXPECT_FALSE(second_res.first); + EXPECT_EQ(second_res.second, first_res.second); +} + +struct waste_resource : handle_resource { + waste_resource(resource_pool_impl& pool, resource_ptr_list_iterator res_it) + : handle_resource(pool, res_it, &resource_pool_impl::waste) {} +}; + +TEST(sync_resource_pool_impl, get_from_pool_and_wait_then_after_waste_should_reserve) { + resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + const get_result& first_res = pool.get(); + + EXPECT_EQ(first_res.first, boost::system::error_code()); + + InSequence s; + + EXPECT_CALL(pool.has_capacity(), wait_for(_, _)).WillOnce(Invoke(waste_resource(pool, first_res.second))); + EXPECT_CALL(pool.has_capacity(), notify_one()).WillOnce(Return()); + + const get_result& second_res = pool.get(); + + EXPECT_FALSE(second_res.first); +} + +struct disable_pool { + resource_pool_impl& pool; + + disable_pool(resource_pool_impl& pool) : pool(pool) {} + + std::cv_status operator ()(std::unique_lock& lock, time_traits::duration) const { + lock.unlock(); + pool.disable(); + lock.lock(); + return std::cv_status::no_timeout; + } +}; + +TEST(sync_resource_pool_impl, get_from_pool_with_zero_capacity_then_disable_should_return_error) { + resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + const get_result& first = pool.get(); + + EXPECT_EQ(first.first, boost::system::error_code()); + + InSequence s; + + EXPECT_CALL(pool.has_capacity(), wait_for(_, _)).WillOnce(Invoke(disable_pool(pool))); + EXPECT_CALL(pool.has_capacity(), notify_all()).WillOnce(Return()); + + const get_result& result = pool.get(); + + EXPECT_EQ(result.first, make_error_code(error::disabled)); +} + +TEST(sync_resource_pool_impl, get_one_set_and_recycle_with_zero_idle_timeout_then_get_should_return_empty) { + resource_pool_impl pool_impl(1, time_traits::duration(0), time_traits::duration::max()); + + EXPECT_CALL(pool_impl.has_capacity(), notify_one()).WillOnce(Return()); + + const get_result first_res = pool_impl.get(); + EXPECT_EQ(first_res.first, boost::system::error_code()); + first_res.second->value = resource {}; + first_res.second->reset_time = time_traits::now(); + EXPECT_TRUE(first_res.second->value); + pool_impl.recycle(first_res.second); + + EXPECT_EQ(pool_impl.available(), 1u); + + const get_result second_res = pool_impl.get(); + EXPECT_EQ(second_res.first, boost::system::error_code()); + EXPECT_FALSE(second_res.second->value); +} + +TEST(sync_resource_pool_impl, should_waste_resource_when_lifespan_ends) { + resource_pool_impl pool_impl(1, time_traits::duration::max(), time_traits::duration(0)); + + EXPECT_CALL(pool_impl.has_capacity(), notify_one()).WillOnce(Return()); + + const get_result first_res = pool_impl.get(); + EXPECT_EQ(first_res.first, boost::system::error_code()); + first_res.second->value = resource {}; + first_res.second->reset_time = time_traits::now(); + EXPECT_TRUE(first_res.second->value); + pool_impl.recycle(first_res.second); + + EXPECT_EQ(pool_impl.available(), 0u); +} + +TEST(sync_resource_pool_impl, should_waste_used_resource_after_invalidate) { + resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + + EXPECT_CALL(pool.has_capacity(), notify_one()).WillOnce(Return()); + + const get_result res = pool.get(); + EXPECT_EQ(res.first, boost::system::error_code()); + res.second->value = resource {}; + res.second->reset_time = time_traits::now(); + pool.invalidate(); + pool.recycle(res.second); + + EXPECT_EQ(pool.available(), 0u); +} + +TEST(sync_resource_pool_impl, should_waste_available_resource_after_invalidate) { + resource_pool_impl pool([]{ return resource{}; }, 1, time_traits::duration::max(), time_traits::duration::max()); + + pool.invalidate(); + + EXPECT_EQ(pool.available(), 0u); +} + +TEST(sync_resource_pool_impl, should_restore_wasted_cell) { + resource_pool_impl pool([]{ return resource{}; }, 1, time_traits::duration::max(), time_traits::duration::max()); + + pool.invalidate(); + + InSequence s; + + EXPECT_CALL(pool.has_capacity(), notify_one()).WillOnce(Return()); + EXPECT_CALL(pool.has_capacity(), notify_one()).WillOnce(Return()); + + const get_result first_res = pool.get(); + EXPECT_EQ(first_res.first, boost::system::error_code()); + first_res.second->value = resource {}; + first_res.second->reset_time = time_traits::now(); + pool.recycle(first_res.second); + + EXPECT_EQ(pool.available(), 1u); + + const get_result second_res = pool.get(); + EXPECT_EQ(second_res.first, boost::system::error_code()); + EXPECT_TRUE(second_res.second->value); + pool.recycle(second_res.second); +} + +TEST(sync_resource_pool_impl, should_waste_used_resource_after_invalidate_when_other_client_is_waiting) { + resource_pool_impl pool(1, time_traits::duration::max(), time_traits::duration::max()); + const get_result& first_res = pool.get(); + pool.invalidate(); + + EXPECT_EQ(first_res.first, boost::system::error_code()); + first_res.second->value = resource {}; + first_res.second->reset_time = time_traits::now(); + + InSequence s; + + EXPECT_CALL(pool.has_capacity(), wait_for(_, _)).WillOnce(Invoke(recycle_resource(pool, first_res.second))); + EXPECT_CALL(pool.has_capacity(), notify_one()).WillOnce(Return()); + + const get_result& second_res = pool.get(); + + EXPECT_FALSE(second_res.first); + EXPECT_EQ(second_res.second, first_res.second); +} + +} diff --git a/contrib/resource_pool/tests/time_traits.cc b/contrib/resource_pool/tests/time_traits.cc new file mode 100644 index 000000000..73ba5a398 --- /dev/null +++ b/contrib/resource_pool/tests/time_traits.cc @@ -0,0 +1,27 @@ +#include + +#include + +namespace { + +using namespace testing; +using namespace yamail::resource_pool; + +struct time_traits_test : Test {}; + +TEST(time_traits_test, add_more_than_max_should_return_max) { + time_traits::time_point result = time_traits::add(time_traits::time_point::max(), time_traits::duration::max()); + EXPECT_EQ(result, time_traits::time_point::max()); +} + +TEST(time_traits_test, add_to_less_than_epoch_should_return_min) { + time_traits::time_point result = time_traits::add(time_traits::time_point::min(), time_traits::duration::min()); + EXPECT_EQ(result, time_traits::time_point(time_traits::duration::min())); +} + +TEST(time_traits_test, add_epoch_should_return_increased) { + time_traits::time_point result = time_traits::add(time_traits::time_point(), time_traits::duration(1)); + EXPECT_EQ(result, time_traits::time_point(time_traits::duration(1))); +} + +} diff --git a/docker-compose.yml b/docker-compose.yml index c92d7ba17..969fce9bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: - ~/.ccache:/ccache - .:/code ozo_postgres: - image: postgres:alpine + image: postgres:16 environment: POSTGRES_DB: &POSTGRES_DB ozo_test_db POSTGRES_USER: &POSTGRES_USER ozo_test_user diff --git a/examples/connection_pool.cpp b/examples/connection_pool.cpp index 4a01879a4..bbb9c5963 100644 --- a/examples/connection_pool.cpp +++ b/examples/connection_pool.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include diff --git a/examples/request.cpp b/examples/request.cpp index 0663d4c07..c2a76a0c9 100644 --- a/examples/request.cpp +++ b/examples/request.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include diff --git a/examples/retry_request.cpp b/examples/retry_request.cpp index 5349c07e6..61d9e0958 100644 --- a/examples/retry_request.cpp +++ b/examples/retry_request.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include diff --git a/examples/role_based_request.cpp b/examples/role_based_request.cpp index 7f209b362..b9ef5ddac 100644 --- a/examples/role_based_request.cpp +++ b/examples/role_based_request.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include diff --git a/examples/transaction.cpp b/examples/transaction.cpp index 9dd7d1201..c9a4a6460 100644 --- a/examples/transaction.cpp +++ b/examples/transaction.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include #include diff --git a/include/ozo/asio.h b/include/ozo/asio.h index fd08fca19..8a44bd40e 100644 --- a/include/ozo/asio.h +++ b/include/ozo/asio.h @@ -1,35 +1,19 @@ #pragma once -#include -#include +#include #include #include #include +#include +#include + namespace ozo { namespace asio = boost::asio; using asio::async_completion; -using asio::io_context; - -#if BOOST_VERSION < 107000 - -template -inline decltype(auto) async_initiate(Initiation&& initiation, - CompletionToken& token, Args&&... args) { - async_completion completion(token); - - initiation(std::move(completion.completion_handler), std::forward(args)...); - - return completion.result.get(); -} - -#else - using asio::async_initiate; - -#endif +using asio::io_context; namespace detail { @@ -50,30 +34,35 @@ auto make_strand_executor(const Executor& ex) { return strand_executor::get(ex); } +template +inline constexpr auto is_asio_executor_v = + asio::is_executor>::value || + asio::execution::is_executor>::value; + +template +auto resolve_asio_executor(Executor ex) { + if constexpr (is_asio_executor_v) { + return ex; + } else if constexpr (requires { ex.get_executor(); }) { + return resolve_asio_executor(ex.get_executor()); + } else { + return ex; + } +} + template struct operation_timer { static_assert(std::is_same_v, "No operation_timer<> specialization found for specified type"); }; -#if BOOST_VERSION < 107000 -template <> -struct operation_timer { - using type = asio::steady_timer; - - template - static type get(const asio::io_context::executor_type& ex, TimeConstraint t) { - return type{ex.context(), t}; - } - - static type get(const asio::io_context::executor_type& ex) { - return type{ex.context()}; - } -}; -#else template <> struct operation_timer { - using type = asio::steady_timer; + using type = asio::basic_waitable_timer< + std::chrono::steady_clock, + asio::wait_traits, + asio::io_context::executor_type + >; template static type get(const asio::io_context::executor_type& ex, TimeConstraint t) { @@ -84,7 +73,6 @@ struct operation_timer { return type{ex}; } }; -#endif template inline auto get_operation_timer(const Executior& ex, TimeConstraint t) { @@ -105,14 +93,14 @@ struct connection_stream { template <> struct connection_stream { - using type = asio::posix::stream_descriptor; + using type = asio::posix::basic_stream_descriptor; static type get(const asio::io_context::executor_type& ex, type::native_handle_type fd) { - return type{ex.context(), fd}; + return type{ex, fd}; } static type get(const asio::io_context::executor_type& ex) { - return type{ex.context()}; + return type{ex}; } }; diff --git a/include/ozo/connection.h b/include/ozo/connection.h index 284207761..3e1f67d82 100644 --- a/include/ozo/connection.h +++ b/include/ozo/connection.h @@ -292,8 +292,8 @@ struct is_connection> : std::true_type {}; * `%Connection` concept represents a minimum set of attributes and functions that are required * by the library to execute operations on a database. `%Connection` should provide: * * the native PostgreSQL connection handle from `libpq`, -* * an executor to perform IO-related operation (according to the current version of Boost.Asio -* it should be `boost::asio::io_context::executor_type` object), +* * an executor to perform IO-related operation (default OZO connection models use +* `boost::asio::io_context::executor_type`), * * an additional error context to provide context-depended information for errors, * * IO functions that are necessary to perform operations. * @@ -314,7 +314,7 @@ struct is_connection> : std::true_type {}; * |

as_const(c).oid_map()
| `C::oid_map_type` | Should return a const reference on `OidMap` which is used by the library for custom types introspection for the connection IO. Shall not throw an exception. | * |
as_const(c).%get_error_context()
| `C::error_context_type` | Should return a const reference on an additional error context is related to at least the last error. In the current implementation, the type supported is `std::string`. Shall not throw an exception. | * |
c.set_error_context(error_context_type)[1]
%c.set_error_context()[2]
| | Should set[1] or reset[2] additional error context. | -* |
as_const(c).%get_executor()
| `C::executor_type` | Should provide an executor object that is useful for IO-related operations, like timer and so on. In the current implementation `boost::asio::io_context::executor_type` is only applicable. Shall not throw an exception. | +* |
as_const(c).%get_executor()
| `C::executor_type` | Should provide an executor object that is useful for IO-related operations, like timer and so on. Default OZO connection models expose `boost::asio::io_context::executor_type`. Shall not throw an exception. | * |
c.async_wait_write(WaitHandler)
| | Should asynchronously wait for write ready state of the connection socket. | * |
c.async_wait_read(WaitHandler)
| | Should asynchronously wait for read ready state of the connection socket. | * |
c.close()
| `error_code` | Should close connection socket and cancel all IO operation on the connection (like `async_wait_write`, `async_wait_read`). Shall not throw an exception. | @@ -591,7 +591,13 @@ struct forward_connection { static_assert(ozo::TimeConstraint, "should model TimeConstraint concept"); unwrap_connection(c).set_error_context(); auto ex = unwrap_connection(c).get_executor(); - asio::dispatch(ex, detail::bind(std::forward(h), error_code{}, std::forward(c))); + auto bound = detail::bind(std::forward(h), error_code{}, std::forward(c)); + auto bound_ex = detail::resolve_asio_executor(asio::get_associated_executor(bound)); + asio::dispatch(ex, [bound_ex, bound = std::move(bound)]() mutable { + asio::dispatch(bound_ex, [bound = std::move(bound)]() mutable { + bound(); + }); + }); } }; diff --git a/include/ozo/connection_pool.h b/include/ozo/connection_pool.h index ea0d65950..474dc1c74 100644 --- a/include/ozo/connection_pool.h +++ b/include/ozo/connection_pool.h @@ -105,8 +105,8 @@ class connection_rep { * the pool and be closed. The class object is non-copyable. * * @tparam Rep --- underlying connection pool representation for the real connection. - * @tparam Executor --- the type of the executor is used to perform IO; currently only - * `boost::asio::io_context::executor_type` is supported. + * @tparam Executor --- the type of the executor used to perform IO. The default model uses + * `boost::asio::io_context::executor_type`. * * @thread_safety{Safe,Unsafe} * @ingroup group-connection-types diff --git a/include/ozo/detail/post_handler.h b/include/ozo/detail/post_handler.h index d71eb64ac..4a8fbef2a 100644 --- a/include/ozo/detail/post_handler.h +++ b/include/ozo/detail/post_handler.h @@ -17,7 +17,13 @@ struct post_handler { template void operator() (error_code ec, Connection&& connection) { auto ex = get_executor(connection); - asio::post(ex, detail::bind(std::move(handler), std::move(ec), std::forward(connection))); + auto bound = detail::bind(std::move(handler), std::move(ec), std::forward(connection)); + auto bound_ex = detail::resolve_asio_executor(asio::get_associated_executor(bound)); + asio::post(ex, [bound_ex, bound = std::move(bound)]() mutable { + asio::dispatch(bound_ex, [bound = std::move(bound)]() mutable { + bound(); + }); + }); } }; diff --git a/include/ozo/detail/wrap_executor.h b/include/ozo/detail/wrap_executor.h index 701b37fe9..ba9f55537 100644 --- a/include/ozo/detail/wrap_executor.h +++ b/include/ozo/detail/wrap_executor.h @@ -21,7 +21,11 @@ struct wrap_executor { template void operator() (Args&& ...args) { - asio::dispatch(detail::bind(std::move(handler), std::forward(args)...)); + auto bound = detail::bind(std::move(handler), std::forward(args)...); + auto bound_ex = detail::resolve_asio_executor(asio::get_associated_executor(bound)); + asio::dispatch(bound_ex, [bound = std::move(bound)]() mutable { + bound(); + }); } using executor_type = Executor; diff --git a/include/ozo/impl/async_request.h b/include/ozo/impl/async_request.h index a77fb3324..03d505c19 100644 --- a/include/ozo/impl/async_request.h +++ b/include/ozo/impl/async_request.h @@ -240,6 +240,15 @@ struct async_get_result_op : boost::asio::coroutine { case PGRES_COPY_BOTH: case PGRES_NONFATAL_ERROR: break; + +#ifdef LIBPQ_HAS_PIPELINING + // Since currently there have not seen that ozo has supported pipeline + // inteoduced in PostgreSQL 14, the related results should be ignored. + case PGRES_PIPELINE_SYNC: + case PGRES_PIPELINE_ABORTED: + case PGRES_TUPLES_CHUNK: + break; +#endif } get_connection(ctx_).set_error_context(get_result_status_name(status)); diff --git a/include/ozo/impl/cancel.h b/include/ozo/impl/cancel.h index 5d5aec039..b9b40899f 100644 --- a/include/ozo/impl/cancel.h +++ b/include/ozo/impl/cancel.h @@ -124,7 +124,11 @@ struct cancel_op { void operator () () { auto [ec, msg] = dispatch_cancel(std::move(cancel_handle_)); - asio::dispatch(detail::bind(std::move(handler_), std::move(ec), std::move(msg))); + auto bound = detail::bind(std::move(handler_), std::move(ec), std::move(msg)); + auto bound_ex = detail::resolve_asio_executor(asio::get_associated_executor(bound)); + asio::dispatch(bound_ex, [bound = std::move(bound)]() mutable { + bound(); + }); } }; diff --git a/include/ozo/impl/connection_pool.h b/include/ozo/impl/connection_pool.h index 3a75449fa..750959649 100644 --- a/include/ozo/impl/connection_pool.h +++ b/include/ozo/impl/connection_pool.h @@ -14,12 +14,14 @@ auto create_pooled_connection(const Allocator& alloc, const Executor& ex, Rep&& return std::allocate_shared, Executor>>(alloc, ex, std::forward(rep)); } -template +template struct pooled_connection_wrapper { using connection_ptr = typename connection_pool::connection_type; using connection = typename connection_ptr::element_type; using handle_type = typename connection::rep_type; + using io_context_type = std::decay_t; + io_context_type* io_ = nullptr; typename connection::executor_type io_executor_; Source source_; detail::make_copyable_t handler_; @@ -70,7 +72,7 @@ struct pooled_connection_wrapper { return handler_(std::move(ec), std::move(conn)); } - source_(io_executor_.context(), time_constrain_, wrapper{std::move(handler_), std::move(handle)}); + source_(*io_, time_constrain_, wrapper{std::move(handler_), std::move(handle)}); } using executor_type = decltype(asio::get_associated_executor(handler_)); @@ -86,12 +88,12 @@ struct pooled_connection_wrapper { } }; -template -auto wrap_pooled_connection_handler(const Executor& ex, Source&& source, TimeConstraint t, Handler&& handler) { +template +auto wrap_pooled_connection_handler(IoContext& io, const Executor& ex, Source&& source, TimeConstraint t, Handler&& handler) { static_assert(ConnectionSource, "is not a ConnectionSource"); - return pooled_connection_wrapper, std::decay_t, TimeConstraint> { - ex, std::forward(source), std::forward(handler), t + return pooled_connection_wrapper, std::decay_t, TimeConstraint, std::decay_t> { + std::addressof(io), ex, std::forward(source), std::forward(handler), t }; } @@ -114,6 +116,7 @@ void connection_pool::operator ()(io_context& io, TimeCons impl_.get_auto_recycle( io, detail::wrap_pooled_connection_handler( + io, io.get_executor(), source_, t, @@ -125,7 +128,7 @@ void connection_pool::operator ()(io_context& io, TimeCons template pooled_connection::pooled_connection(const Executor& ex, Rep&& rep) -: rep_(std::move(rep)), ex_(ex), stream_(get_executor().context()) { +: rep_(std::move(rep)), ex_(ex), stream_(detail::get_connection_stream(ex_)) { if (auto fd = PQsocket(native_handle()); fd != -1) { stream_.assign(fd); } diff --git a/include/ozo/impl/result_status.h b/include/ozo/impl/result_status.h index 610465a36..9a5d159f7 100644 --- a/include/ozo/impl/result_status.h +++ b/include/ozo/impl/result_status.h @@ -17,6 +17,11 @@ inline const char* get_result_status_name(ExecStatusType status) noexcept { OZO_CASE_RETURN(PGRES_BAD_RESPONSE) OZO_CASE_RETURN(PGRES_EMPTY_QUERY) OZO_CASE_RETURN(PGRES_FATAL_ERROR) +#ifdef LIBPQ_HAS_PIPELINING + OZO_CASE_RETURN(PGRES_PIPELINE_SYNC) + OZO_CASE_RETURN(PGRES_PIPELINE_ABORTED) + OZO_CASE_RETURN(PGRES_TUPLES_CHUNK) +#endif } #undef OZO_CASE_RETURN return "unknown"; diff --git a/include/ozo/io/istream.h b/include/ozo/io/istream.h index d581e6b57..fc2ddfc08 100644 --- a/include/ozo/io/istream.h +++ b/include/ozo/io/istream.h @@ -14,18 +14,20 @@ namespace ozo { class istream { class istreambuf { const char* i_; - const char* last_; + std::size_t remaining_; public: constexpr istreambuf(const char* data, size_t len) noexcept - : i_(data), last_(data + len) {} + : i_(data), remaining_(len) {} std::streamsize read(char* buf, std::streamsize n) noexcept { - auto last = std::min(i_ + n, last_); - std::copy(i_, last, buf); - n = std::distance(i_, last); - i_ = last; - return n; + const auto nbytes = n > 0 ? std::min(remaining_, static_cast(n)) : 0; + if (nbytes > 0) { + std::copy_n(i_, nbytes, buf); + i_ += nbytes; + remaining_ -= nbytes; + } + return static_cast(nbytes); } }; public: @@ -44,7 +46,7 @@ class istream { } traits_type::int_type get() noexcept { - char retval; + char retval = 0; if (!read(&retval, 1)) { return traits_type::eof(); } diff --git a/include/ozo/query_builder.h b/include/ozo/query_builder.h index 0e69d7714..a7aeefc3c 100644 --- a/include/ozo/query_builder.h +++ b/include/ozo/query_builder.h @@ -183,9 +183,34 @@ struct get_query_params_impl> { namespace literals { -template -constexpr auto operator "" _SQL() { - return make_query_builder(hana::make_tuple(make_query_text(hana::string()))); +template +struct fixed_string { + char value[N]; + + constexpr fixed_string(const char (&str)[N]) { + for (std::size_t i = 0; i < N; ++i) { + value[i] = str[i]; + } + } + + static constexpr std::size_t size = N - 1; // 不算 '\0' +}; + +template +constexpr auto make_hana_string_from_fixed() { + return [](std::index_sequence) { + return hana::string_c; + }(std::make_index_sequence{}); +} + +template +constexpr auto operator ""_SQL() { + constexpr auto sql = make_hana_string_from_fixed(); + return make_query_builder( + hana::make_tuple( + make_query_text(sql) + ) + ); } } // namespace literals diff --git a/include/ozo/result.h b/include/ozo/result.h index 765e4880a..1aea680bc 100644 --- a/include/ozo/result.h +++ b/include/ozo/result.h @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -390,7 +391,11 @@ class basic_result { * @return native_handle_type --- native handle representation */ native_handle_type native_handle() const noexcept { - return std::addressof(*handle_); + if constexpr (std::is_pointer_v) { + return handle_; + } else { + return handle_.get(); + } } /** diff --git a/scripts/run_pg_tests_podman.sh b/scripts/run_pg_tests_podman.sh new file mode 100755 index 000000000..82b375255 --- /dev/null +++ b/scripts/run_pg_tests_podman.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +set -euo pipefail + +IMAGE="${OZO_PODMAN_POSTGRES_IMAGE:-docker.io/library/postgres:16}" +CONTAINER_NAME="${OZO_PODMAN_POSTGRES_CONTAINER:-ozo-pg-test}" +HOST="${OZO_PG_TEST_HOST:-127.0.0.1}" +PORT="${OZO_PG_TEST_PORT:-55432}" +DB="${POSTGRES_DB:-ozo_test_db}" +USER="${POSTGRES_USER:-ozo_test_user}" +PASSWORD="${POSTGRES_PASSWORD:-v4Xpkocl~5l6h219Ynk1lJbM61jIr!ca}" +BUILD_DIR="${OZO_PG_BUILD_DIR:-build-podman-pg}" +JOBS="${OZO_BUILD_JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu)}" + +cleanup() { + podman rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true +} + +trap cleanup EXIT + +cleanup + +podman run -d \ + --name "${CONTAINER_NAME}" \ + -e POSTGRES_DB="${DB}" \ + -e POSTGRES_USER="${USER}" \ + -e POSTGRES_PASSWORD="${PASSWORD}" \ + -p "${HOST}:${PORT}:5432" \ + "${IMAGE}" >/dev/null + +for _ in $(seq 1 60); do + if podman exec "${CONTAINER_NAME}" pg_isready -U "${USER}" -d "${DB}" >/dev/null 2>&1; then + break + fi + sleep 1 +done + +if ! podman exec "${CONTAINER_NAME}" pg_isready -U "${USER}" -d "${DB}" >/dev/null 2>&1; then + podman logs "${CONTAINER_NAME}" >&2 || true + echo "PostgreSQL in container ${CONTAINER_NAME} did not become ready in time." >&2 + exit 1 +fi + +CONNINFO="host=${HOST} port=${PORT} dbname=${DB} user=${USER} password=${PASSWORD}" + +cmake -S . -B "${BUILD_DIR}" \ + -DOZO_BUILD_TESTS=ON \ + -DOZO_BUILD_PG_TESTS=ON \ + -DOZO_PG_TEST_CONNINFO="${CONNINFO}" + +cmake --build "${BUILD_DIR}" -j"${JOBS}" --target ozo_tests +ctest --test-dir "${BUILD_DIR}" --output-on-failure -R ozo_tests diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 170916bb3..aeb9c089e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,7 @@ include(ExternalProject) ExternalProject_Add( GoogleTest GIT_REPOSITORY "https://github.com/google/googletest.git" - GIT_TAG release-1.10.0 + GIT_TAG release-1.12.1 CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR} UPDATE_COMMAND "" LOG_DOWNLOAD ON @@ -15,6 +15,7 @@ ExternalProject_Get_Property(GoogleTest source_dir) include_directories(SYSTEM "${source_dir}/include") include_directories(SYSTEM "${CMAKE_CURRENT_BINARY_DIR}/include") link_directories("${CMAKE_CURRENT_BINARY_DIR}/lib") +link_directories("${CMAKE_CURRENT_BINARY_DIR}/lib64") include_directories("${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/tests/connection_pool.cpp b/tests/connection_pool.cpp index a77b87d7c..c86fe0325 100644 --- a/tests/connection_pool.cpp +++ b/tests/connection_pool.cpp @@ -252,6 +252,7 @@ struct pooled_connection_wrapper : Test { auto wrap_pooled_connection_handler() { return ozo::detail::wrap_pooled_connection_handler( + io, io.get_executor(), connection_source{&provider_mock}, ozo::none, @@ -269,6 +270,7 @@ using ozo::error_code; TEST_F(pooled_connection_wrapper, should_be_copyable_with_non_copyable_handler_for_resource_pool_compatibility) { auto h = ozo::detail::wrap_pooled_connection_handler( + io, io.get_executor(), connection_source{&provider_mock}, ozo::none, diff --git a/tests/failover/role_based.cpp b/tests/failover/role_based.cpp index a83138da5..7e5a80821 100644 --- a/tests/failover/role_based.cpp +++ b/tests/failover/role_based.cpp @@ -108,7 +108,7 @@ TEST(role_based_connection_provider__is_supported, should_return_false_for_conne TEST(role_based_connection_provider__rebind, should_call_source_rebind_and_return_new_provider_for_role) { role_based_connection_source_mock source; - boost::asio::io_service io; + boost::asio::io_context io; auto provider = ozo::failover::role_based_connection_provider{ role_based_connection_source {std::addressof(source)}, io @@ -125,7 +125,7 @@ TEST(role_based_connection_provider__rebind, should_call_source_rebind_and_retur TEST(role_based_connection_provider__rebind, should_move_source_call_source_rebind_and_return_new_provider_for_role) { role_based_connection_source_mock source; - boost::asio::io_service io; + boost::asio::io_context io; auto provider = ozo::failover::role_based_connection_provider{ role_based_connection_source {std::addressof(source)}, io @@ -146,7 +146,7 @@ using namespace std::literals; struct role_based_try__initiate_next_try : Test { StrictMock conn; role_based_connection_source_mock source; - boost::asio::io_service io; + boost::asio::io_context io; auto provider() { return ozo::failover::role_based_connection_provider{ @@ -328,7 +328,7 @@ TEST_F(role_based_try__initiate_next_try, should_not_close_connection_on_no_retr struct role_based_try__get_context : Test { role_based_connection_source_mock source; - boost::asio::io_service io; + boost::asio::io_context io; auto provider() { return ozo::failover::role_based_connection_provider{ diff --git a/tests/impl/async_connect.cpp b/tests/impl/async_connect.cpp index 8ff5df253..48feb03fc 100644 --- a/tests/impl/async_connect.cpp +++ b/tests/impl/async_connect.cpp @@ -40,7 +40,7 @@ struct fixture { fixture() { EXPECT_CALL(io.strand_service_, get_executor()).WillOnce(ReturnRef(strand)); - auto ex = boost::asio::executor(ozo::detail::make_strand_executor(io.get_executor())); + auto ex = ozo::detail::make_strand_executor(io.get_executor()); EXPECT_CALL(callback, get_executor()).WillRepeatedly(Return(ex)); } }; diff --git a/tests/integration/cancel_integration.cpp b/tests/integration/cancel_integration.cpp index bfb37e06d..75e1c157d 100644 --- a/tests/integration/cancel_integration.cpp +++ b/tests/integration/cancel_integration.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include @@ -25,30 +25,36 @@ TEST(cancel, should_cancel_operation) { using namespace hana::literals; ozo::io_context io; - boost::asio::steady_timer timer(io); - - boost::asio::spawn(io, [&io, &timer](auto yield){ - const ozo::connection_info conn_info(OZO_PG_TEST_CONNINFO); - ozo::error_code ec; - auto conn = ozo::get_connection(conn_info[io], yield[ec]); - EXPECT_FALSE(ec); - boost::asio::spawn(yield, [&io, &timer, handle = get_cancel_handle(conn)](auto yield) mutable { - timer.expires_after(1s); - ozo::error_code ec; - timer.async_wait(yield[ec]); + auto timer = ozo::detail::get_operation_timer(io.get_executor()); + auto result = std::make_shared>(); + auto future = result->get_future(); + const ozo::connection_info conn_info(OZO_PG_TEST_CONNINFO); + + ozo::get_connection(conn_info[io], [&io, &timer, result](ozo::error_code ec, auto conn) mutable { + if (ec) { + result->set_value(ec); + return; + } + + timer.expires_after(1s); + timer.async_wait([&io, handle = get_cancel_handle(conn)](ozo::error_code ec) mutable { if (!ec) { // Guard is needed since cancel will be served with external // system executor, so we need to preserve our io_context from // stop until all the operation processed properly auto guard = boost::asio::make_work_guard(io); - ozo::cancel(std::move(handle), io, 5s, yield[ec]); + ozo::cancel(std::move(handle), io, 5s, + [guard = std::move(guard)](ozo::error_code, std::string) mutable {}); } }); - ozo::execute(conn, "SELECT pg_sleep(1000000)"_SQL, yield[ec]); - EXPECT_EQ(ec, ozo::sqlstate::query_canceled); + ozo::execute(conn, "SELECT pg_sleep(1000000)"_SQL, + [result](ozo::error_code ec, auto) mutable { + result->set_value(ec); + }); }); io.run(); + EXPECT_EQ(future.get(), ozo::sqlstate::query_canceled); } TEST(cancel, should_stop_cancel_operation_on_zero_timeout) { @@ -58,31 +64,45 @@ TEST(cancel, should_stop_cancel_operation_on_zero_timeout) { ozo::io_context io; ozo::io_context dummy_io; - boost::asio::steady_timer timer(io); - - boost::asio::spawn(io, [&io, &timer, &dummy_io](auto yield){ - const ozo::connection_info conn_info(OZO_PG_TEST_CONNINFO); - ozo::error_code ec; - auto conn = ozo::get_connection(conn_info[io], yield[ec]); - EXPECT_FALSE(ec); - boost::asio::spawn(yield, [&io, &timer, handle = get_cancel_handle(conn, dummy_io.get_executor())](auto yield) mutable { - timer.expires_after(1s); - ozo::error_code ec; - timer.async_wait(yield[ec]); - if (!ec) { - // Guard is needed since cancel will be served with external - // system executor, so we need to preserve our io_context from - // stop until all the operation processed properly - auto guard = boost::asio::make_work_guard(io); - ozo::cancel(std::move(handle), io, 0s, yield[ec]); - EXPECT_EQ(ec, boost::asio::error::timed_out); + auto timer = ozo::detail::get_operation_timer(io.get_executor()); + const ozo::connection_info conn_info(OZO_PG_TEST_CONNINFO); + auto execute_result = std::make_shared>(); + auto execute_future = execute_result->get_future(); + auto cancel_result = std::make_shared>(); + auto cancel_future = cancel_result->get_future(); + + ozo::get_connection(conn_info[io], [&io, &timer, &dummy_io, execute_result, cancel_result] + (ozo::error_code ec, auto conn) mutable { + if (ec) { + execute_result->set_value(ec); + cancel_result->set_value(ec); + return; + } + + timer.expires_after(1s); + timer.async_wait([&io, cancel_result, handle = get_cancel_handle(conn, dummy_io.get_executor())](ozo::error_code ec) mutable { + if (ec) { + cancel_result->set_value(ec); + return; } + // Guard is needed since cancel will be served with external + // system executor, so we need to preserve our io_context from + // stop until all the operation processed properly + auto guard = boost::asio::make_work_guard(io); + ozo::cancel(std::move(handle), io, 0s, + [cancel_result, guard = std::move(guard)](ozo::error_code ec, std::string) mutable { + cancel_result->set_value(ec); + }); }); - ozo::execute(conn, "SELECT pg_sleep(1000000)"_SQL, 2s, yield[ec]); - EXPECT_EQ(ec, boost::asio::error::timed_out); + ozo::execute(conn, "SELECT pg_sleep(1000000)"_SQL, 2s, + [execute_result](ozo::error_code ec, auto) mutable { + execute_result->set_value(ec); + }); }); io.run(); + EXPECT_EQ(execute_future.get(), boost::asio::error::timed_out); + EXPECT_EQ(cancel_future.get(), boost::asio::error::timed_out); } } // namespace diff --git a/tests/test_asio.h b/tests/test_asio.h index 7e4469879..cae6aad5f 100644 --- a/tests/test_asio.h +++ b/tests/test_asio.h @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -201,7 +200,7 @@ struct stream_descriptor { void assign(int fd) { mock_->assign(fd);} - using executor_type = boost::asio::executor; + using executor_type = ozo::tests::executor; executor_type get_executor() const { return io_->get_executor(); @@ -251,7 +250,7 @@ struct callback_gmock; template struct callback_gmock { - using executor_type = boost::asio::executor; + using executor_type = ozo::tests::executor; MOCK_CONST_METHOD3_T(call, void(ozo::error_code, Arg1, Arg2)); MOCK_CONST_METHOD0_T(get_executor, executor_type ()); @@ -259,7 +258,7 @@ struct callback_gmock { template struct callback_gmock { - using executor_type = boost::asio::executor; + using executor_type = ozo::tests::executor; MOCK_CONST_METHOD2_T(call, void(ozo::error_code, Arg)); MOCK_CONST_METHOD0_T(get_executor, executor_type ()); @@ -267,7 +266,7 @@ struct callback_gmock { template <> struct callback_gmock<> { - using executor_type = boost::asio::executor; + using executor_type = ozo::tests::executor; MOCK_CONST_METHOD1_T(call, void(ozo::error_code)); MOCK_CONST_METHOD0_T(get_executor, executor_type ());