mirror of
https://github.com/latentPrion/libspinscale.git
synced 2026-08-12 23:48:22 +00:00
Compare commits
59
Commits
e98aae7e7e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6d9bfe30c | ||
|
|
fd2440e4ee | ||
|
|
21fa125d52 | ||
|
|
3156652257 | ||
|
|
88d6367491 | ||
|
|
5e6108d396 | ||
|
|
3d23ed21be | ||
|
|
15ebf375ef | ||
|
|
7c9bec7b9c | ||
|
|
01efbd8c94 | ||
|
|
3d81ee92aa | ||
|
|
2f31e9a034 | ||
|
|
a29c779f6e | ||
|
|
1763685c0e | ||
|
|
016b2d26de | ||
|
|
ffe86369e2 | ||
|
|
00be517f30 | ||
|
|
ebf0fa2921 | ||
|
|
d33e70f14a | ||
|
|
656aae37c8 | ||
|
|
5689ac3914 | ||
|
|
565e339a8b | ||
|
|
b04b0db155 | ||
|
|
44894299b4 | ||
|
|
edde8f4a64 | ||
|
|
8a7d4272bd | ||
|
|
c60854845d | ||
|
|
a53e0ca325 | ||
|
|
42076d6c78 | ||
|
|
2749d77d65 | ||
|
|
3ea1475757 | ||
|
|
6df9407e65 | ||
|
|
0afa3e16b8 | ||
|
|
4dbc066aac | ||
|
|
ca2cccaa9c | ||
|
|
a14d622eaf | ||
|
|
16e0350245 | ||
|
|
5f265567d1 | ||
|
|
e7707dacdf | ||
|
|
5d139abff2 | ||
|
|
e29bee52cf | ||
|
|
daad2a8c95 | ||
|
|
abdb857e55 | ||
|
|
525530b567 | ||
|
|
3f91cbf104 | ||
|
|
6396cce7e0 | ||
|
|
15295ac05e | ||
|
|
dc58e5d521 | ||
|
|
1db3494d26 | ||
|
|
e94aaf9323 | ||
|
|
83ad680c68 | ||
|
|
1d1cb099db | ||
|
|
0dcfa754b6 | ||
|
|
ad4ea3ccac | ||
|
|
b6eb502e56 | ||
|
|
596ad367e2 | ||
|
|
e4332323f9 | ||
|
|
7eff7a6a9c | ||
|
|
85ac715772 |
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "googletest"]
|
||||||
|
path = googletest
|
||||||
|
url = https://github.com/google/googletest.git
|
||||||
+75
-21
@@ -1,6 +1,8 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(libspinscale VERSION 0.1.0 LANGUAGES CXX)
|
project(libspinscale VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
# Set C++ standard
|
# Set C++ standard
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -18,6 +20,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
|
|||||||
option(ENABLE_DEBUG_LOCKS "Enable debug features for locking system" OFF)
|
option(ENABLE_DEBUG_LOCKS "Enable debug features for locking system" OFF)
|
||||||
option(ENABLE_DEBUG_TRACE_CALLABLES
|
option(ENABLE_DEBUG_TRACE_CALLABLES
|
||||||
"Enable callable tracing for debugging boost::asio post operations" OFF)
|
"Enable callable tracing for debugging boost::asio post operations" OFF)
|
||||||
|
option(ENABLE_DEBUG_CO "Enable coroutine-type debug logging" OFF)
|
||||||
|
|
||||||
# Qutex deadlock detection configuration
|
# Qutex deadlock detection configuration
|
||||||
if(NOT DEFINED DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS)
|
if(NOT DEFINED DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS)
|
||||||
@@ -43,6 +46,10 @@ if(ENABLE_DEBUG_LOCKS)
|
|||||||
set(CONFIG_ENABLE_DEBUG_LOCKS TRUE)
|
set(CONFIG_ENABLE_DEBUG_LOCKS TRUE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_DEBUG_CO)
|
||||||
|
set(CONFIG_LIBSSCL_DEBUG_CO TRUE)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(ENABLE_DEBUG_TRACE_CALLABLES)
|
if(ENABLE_DEBUG_TRACE_CALLABLES)
|
||||||
set(CONFIG_DEBUG_TRACE_CALLABLES TRUE)
|
set(CONFIG_DEBUG_TRACE_CALLABLES TRUE)
|
||||||
# Suppress frame-address warnings when using __builtin_return_address()
|
# Suppress frame-address warnings when using __builtin_return_address()
|
||||||
@@ -59,26 +66,33 @@ configure_file(
|
|||||||
@ONLY
|
@ONLY
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find dependencies
|
# Find dependencies (Boost.System optional on 1.89+; see BoostSharedDeps.cmake)
|
||||||
# Tell CMake we're linking against the shared library (not header-only)
|
if(EXISTS ${CMAKE_SOURCE_DIR}/cmake/BoostSharedDeps.cmake
|
||||||
set(Boost_USE_STATIC_LIBS OFF)
|
AND NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
set(Boost_USE_HEADER_ONLY OFF)
|
include(${CMAKE_SOURCE_DIR}/cmake/BoostSharedDeps.cmake)
|
||||||
find_package(Boost REQUIRED COMPONENTS system log)
|
else()
|
||||||
# Define BOOST_ALL_DYN_LINK project-wide to ensure all Boost libraries use dynamic linking
|
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/BoostSharedDeps.cmake)
|
||||||
add_compile_definitions(BOOST_ALL_DYN_LINK)
|
endif()
|
||||||
|
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
# Create the library
|
# Create the library
|
||||||
add_library(spinscale SHARED
|
add_library(spinscale SHARED
|
||||||
|
src/boostAsioLinkageFix.cpp
|
||||||
src/qutex.cpp
|
src/qutex.cpp
|
||||||
src/lockerAndInvokerBase.cpp
|
|
||||||
src/componentThread.cpp
|
src/componentThread.cpp
|
||||||
src/component.cpp
|
src/component.cpp
|
||||||
|
src/envKvStore.cpp
|
||||||
src/puppeteerComponent.cpp
|
src/puppeteerComponent.cpp
|
||||||
src/puppetApplication.cpp
|
src/puppetApplication.cpp
|
||||||
src/runtime.cpp
|
src/runtime.cpp
|
||||||
src/callableTracer.cpp
|
src/callableTracer.cpp
|
||||||
|
src/multiOperationResultSet.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(spinscale PROPERTIES
|
||||||
|
VERSION ${PROJECT_VERSION}
|
||||||
|
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Conditionally add qutexAcquisitionHistoryTracker.cpp only when debug locks
|
# Conditionally add qutexAcquisitionHistoryTracker.cpp only when debug locks
|
||||||
@@ -97,12 +111,12 @@ target_include_directories(spinscale PUBLIC
|
|||||||
$<INSTALL_INTERFACE:include>
|
$<INSTALL_INTERFACE:include>
|
||||||
)
|
)
|
||||||
|
|
||||||
# Link against required dependencies for shared library
|
# Link against required dependencies for shared library.
|
||||||
# Boost::system is PUBLIC because componentThread.h exposes Boost.Asio types
|
# BOOST_SHARED_DEP_TARGETS is PUBLIC because componentThread.h exposes Boost.Asio
|
||||||
|
# types (and Boost::system when a compiled stub still exists).
|
||||||
target_link_libraries(spinscale PUBLIC
|
target_link_libraries(spinscale PUBLIC
|
||||||
Threads::Threads
|
Threads::Threads
|
||||||
Boost::system
|
${BOOST_SHARED_DEP_TARGETS}
|
||||||
Boost::log
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify Boost dynamic dependencies after build
|
# Verify Boost dynamic dependencies after build
|
||||||
@@ -127,29 +141,69 @@ endif()
|
|||||||
# Install rules
|
# Install rules
|
||||||
install(TARGETS spinscale
|
install(TARGETS spinscale
|
||||||
EXPORT spinscaleTargets
|
EXPORT spinscaleTargets
|
||||||
LIBRARY DESTINATION lib
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
|
||||||
ARCHIVE DESTINATION lib
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
RUNTIME DESTINATION bin
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
install(DIRECTORY include/spinscale
|
install(DIRECTORY include/spinscale
|
||||||
DESTINATION include
|
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
FILES_MATCHING PATTERN "*.h"
|
FILES_MATCHING PATTERN "*.h"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
set(_libspinscaleTestsDefault ON)
|
||||||
|
else()
|
||||||
|
set(_libspinscaleTestsDefault OFF)
|
||||||
|
if(DEFINED ENABLE_TESTS AND ENABLE_TESTS)
|
||||||
|
set(_libspinscaleTestsDefault ON)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
option(LIBSPINSCALE_BUILD_TESTS "Build libspinscale unit tests"
|
||||||
|
${_libspinscaleTestsDefault})
|
||||||
|
|
||||||
|
option(LIBSPINSCALE_BUILD_PROBE_SUPPORT
|
||||||
|
"Build spinscale probe component-thread harness (tools and tests)"
|
||||||
|
OFF)
|
||||||
|
|
||||||
|
# Tests always need the probe harness; tools may request it via cache/root.
|
||||||
|
if(LIBSPINSCALE_BUILD_TESTS)
|
||||||
|
set(LIBSPINSCALE_BUILD_PROBE_SUPPORT ON CACHE BOOL
|
||||||
|
"Build spinscale probe component-thread harness (tools and tests)"
|
||||||
|
FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(LIBSPINSCALE_BUILD_PROBE_SUPPORT)
|
||||||
|
add_subdirectory(probe)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(LIBSPINSCALE_BUILD_TESTS)
|
||||||
|
if(NOT TARGET gtest AND NOT TARGET gtest_main)
|
||||||
|
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||||
|
add_subdirectory(
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/googletest
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/googletest
|
||||||
|
EXCLUDE_FROM_ALL)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
endif()
|
||||||
|
|
||||||
install(FILES include/boostAsioLinkageFix.h
|
install(FILES include/boostAsioLinkageFix.h
|
||||||
DESTINATION include
|
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/config.h
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/config.h
|
||||||
DESTINATION include
|
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Install CMake config files for find_package() support
|
# Install CMake config files for find_package() support
|
||||||
install(EXPORT spinscaleTargets
|
install(EXPORT spinscaleTargets
|
||||||
FILE spinscaleTargets.cmake
|
FILE spinscaleTargets.cmake
|
||||||
NAMESPACE spinscale::
|
NAMESPACE spinscale::
|
||||||
DESTINATION lib/cmake/spinscale
|
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/spinscale
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create config file for find_package()
|
# Create config file for find_package()
|
||||||
@@ -158,7 +212,7 @@ include(CMakePackageConfigHelpers)
|
|||||||
configure_package_config_file(
|
configure_package_config_file(
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/spinscaleConfig.cmake.in
|
${CMAKE_CURRENT_SOURCE_DIR}/cmake/spinscaleConfig.cmake.in
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/spinscaleConfig.cmake
|
${CMAKE_CURRENT_BINARY_DIR}/spinscaleConfig.cmake
|
||||||
INSTALL_DESTINATION lib/cmake/spinscale
|
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/spinscale
|
||||||
)
|
)
|
||||||
|
|
||||||
write_basic_package_version_file(
|
write_basic_package_version_file(
|
||||||
@@ -170,5 +224,5 @@ write_basic_package_version_file(
|
|||||||
install(FILES
|
install(FILES
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/spinscaleConfig.cmake
|
${CMAKE_CURRENT_BINARY_DIR}/spinscaleConfig.cmake
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/spinscaleConfigVersion.cmake
|
${CMAKE_CURRENT_BINARY_DIR}/spinscaleConfigVersion.cmake
|
||||||
DESTINATION lib/cmake/spinscale
|
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/spinscale
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# libspinscale
|
||||||
|
|
||||||
|
libspinscale is a C++ coroutine and asynchronous component runtime for
|
||||||
|
thread-affine systems: applications where work is not just asynchronous, but
|
||||||
|
must run on specific long-lived component threads with explicit lifecycle,
|
||||||
|
posting, cancellation, and orchestration rules.
|
||||||
|
|
||||||
|
It is built around a simple premise: in some systems, the important question is
|
||||||
|
not only "when does this async operation complete?", but also "which component
|
||||||
|
thread owns this work, where does it resume, what locks does the coroutine
|
||||||
|
chain hold, and how does a whole subsystem start or stop as one unit?"
|
||||||
|
|
||||||
|
libspinscale targets that class of problems directly.
|
||||||
|
|
||||||
|
## What It Is For
|
||||||
|
|
||||||
|
libspinscale is meant for applications that look more like a small distributed
|
||||||
|
runtime inside one process than a collection of independent async tasks. It is a
|
||||||
|
fit when the program has:
|
||||||
|
|
||||||
|
- Dedicated component threads with their own `boost::asio::io_context`.
|
||||||
|
- Thread-affine components that must run operations on their owning thread.
|
||||||
|
- Startup, pause, resume, and shutdown protocols across many threads.
|
||||||
|
- Coroutine orchestration that needs structured fan-out and settlement scanning.
|
||||||
|
- Cross-thread post-to and post-back behavior that must be explicit and
|
||||||
|
predictable.
|
||||||
|
- Coroutine-aware locking where deadlock detection must understand the caller
|
||||||
|
coroutine chain.
|
||||||
|
|
||||||
|
Typical examples include simulation runtimes, test harnesses, embedded control
|
||||||
|
planes, game/server subsystems, robotics-style component graphs, multi-thread
|
||||||
|
workflow engines, and other applications where "run this on the right thread" is
|
||||||
|
part of the correctness contract.
|
||||||
|
|
||||||
|
## What Makes It Different
|
||||||
|
|
||||||
|
Most C++ coroutine libraries focus on awaitable primitives, task types, event
|
||||||
|
loops, generators, or composable async algorithms. libspinscale focuses on
|
||||||
|
thread-affine component orchestration. We'll compare libspinscale to contemporaneous C++ coro libraries below to differentiate it:
|
||||||
|
|
||||||
|
Boost.Asio gives excellent networking and executor primitives, but it does not
|
||||||
|
define an application-level component model. libspinscale uses Asio
|
||||||
|
`io_context`s as posting threads and layers a stricter coroutine model on top:
|
||||||
|
component threads, post-to/post-back promises, lifecycle invokers, and
|
||||||
|
structured group settlement.
|
||||||
|
|
||||||
|
Folly coroutines provide production-grade task abstractions and executor
|
||||||
|
integration, especially for large service stacks. libspinscale is narrower: it
|
||||||
|
optimizes for explicit component ownership and deterministic thread handoff
|
||||||
|
rather than general-purpose async service composition.
|
||||||
|
|
||||||
|
cppcoro provides foundational coroutine types such as tasks, generators, async
|
||||||
|
manual reset events, and related primitives. libspinscale is less of a primitive
|
||||||
|
toolkit and more of a runtime pattern for systems with named threads, component
|
||||||
|
lifetimes, and cross-thread orchestration.
|
||||||
|
|
||||||
|
In short: libspinscale is not trying to replace Boost.Asio, Folly, or cppcoro.
|
||||||
|
It is trying to solve the coordination problem that appears above them when an
|
||||||
|
application has a fixed topology of cooperating component threads.
|
||||||
|
|
||||||
|
## Core Ideas
|
||||||
|
|
||||||
|
### Component Threads
|
||||||
|
|
||||||
|
`ComponentThread` owns a `boost::asio::io_context` and represents a named
|
||||||
|
execution thread. `PuppeteerThread` and `PuppetThread` build a lifecycle model
|
||||||
|
on top of that thread:
|
||||||
|
|
||||||
|
- A puppeteer coordinates the application.
|
||||||
|
- Puppet threads host component work.
|
||||||
|
- Lifecycle operations such as JOLT, start, pause, resume, and exit are exposed
|
||||||
|
as awaitable operations.
|
||||||
|
|
||||||
|
`PuppetApplication` orchestrates a set of puppet threads and exposes lifecycle
|
||||||
|
batch operations:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
co_await app.joltAllPuppetThreadsCReq();
|
||||||
|
co_await app.startAllPuppetThreadsCReq();
|
||||||
|
co_await app.exitAllPuppetThreadsCReq();
|
||||||
|
```
|
||||||
|
|
||||||
|
These operations are coroutine-native. Callers that are already in a coroutine
|
||||||
|
can `co_await` them directly.
|
||||||
|
|
||||||
|
### Posting Promises
|
||||||
|
|
||||||
|
Posting coroutines are tied to a target thread. A tagged posting promise posts the
|
||||||
|
callee coroutine to `ThreadTag::io_context()` at initial suspend and posts back
|
||||||
|
to the caller's `io_context` at completion.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct BodyThreadTag
|
||||||
|
{
|
||||||
|
static boost::asio::io_context &io_context();
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
using BodyPostingPromise =
|
||||||
|
sscl::co::TaggedPostingPromise<T, BodyThreadTag>;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
using BodyInvoker =
|
||||||
|
sscl::co::ViralPostingInvoker<BodyPostingPromise, T>;
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes thread ownership visible in the coroutine return type. A component
|
||||||
|
operation can say, at the type level, that it runs on the body thread and resumes
|
||||||
|
its caller correctly when done.
|
||||||
|
|
||||||
|
### Viral And Non-Viral Invokers
|
||||||
|
|
||||||
|
libspinscale distinguishes coroutine-to-coroutine orchestration from
|
||||||
|
non-coroutine entry points.
|
||||||
|
|
||||||
|
Viral invokers are awaitable and are used inside coroutine call chains:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
BodyInvoker<void> BodyComponent::initializeCReq()
|
||||||
|
{
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await body.initializeCReq();
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-viral invokers are for top-level boundaries where ordinary code starts a
|
||||||
|
coroutine and supplies a completion callback:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto invoker = component.initializeFromHookCReq(exceptionPtr, [] {
|
||||||
|
// completion callback
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This distinction is intentional. Coroutine orchestration should use `co_await`.
|
||||||
|
Callback-style completion should stay at the outer boundary.
|
||||||
|
|
||||||
|
### Dynamic Post Targets
|
||||||
|
|
||||||
|
Most posting coroutines use a compile-time `ThreadTag`. When the target thread is
|
||||||
|
known only at runtime, `DynamicViralPostingInvoker<T>` and
|
||||||
|
`ExplicitPostTarget` allow the caller to supply the destination `io_context`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
sscl::co::DynamicViralPostingInvoker<void>
|
||||||
|
runOnSelectedThread(sscl::co::ExplicitPostTarget target)
|
||||||
|
{
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await runOnSelectedThread(sscl::co::ExplicitPostTarget{thread.getIoContext()});
|
||||||
|
```
|
||||||
|
|
||||||
|
The post-back side still returns to the caller's thread.
|
||||||
|
|
||||||
|
### Group Settlement
|
||||||
|
|
||||||
|
`co::Group` provides structured fan-out over heterogeneous invokers. Members can
|
||||||
|
be added, awaited as a group, and inspected by settlement status:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
sscl::co::Group group;
|
||||||
|
|
||||||
|
auto bodyInit = body.initializeCReq();
|
||||||
|
auto worldInit = world.initializeCReq();
|
||||||
|
auto legInit = leg.initializeCReq();
|
||||||
|
|
||||||
|
group.add(bodyInit);
|
||||||
|
group.add(worldInit);
|
||||||
|
group.add(legInit);
|
||||||
|
|
||||||
|
co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
group.checkForAndReThrowGroupExceptions();
|
||||||
|
```
|
||||||
|
|
||||||
|
Settlements record whether a member completed or threw. The original invoker can
|
||||||
|
be recovered from a descriptor when a caller needs typed return values.
|
||||||
|
|
||||||
|
### Non-Viral Task Nursery
|
||||||
|
|
||||||
|
`co::NonViralTaskNursery` is the structured-concurrency owner for non-viral
|
||||||
|
invokers at non-coroutine boundaries. Unlike `co::Group`, it is for callback-style
|
||||||
|
entry from ordinary code (HTTP handlers, timers, shutdown sequences), not for
|
||||||
|
`co_await` orchestration inside coroutines.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
sscl::co::NonViralTaskNursery nursery;
|
||||||
|
nursery.openAdmission();
|
||||||
|
|
||||||
|
nursery.launch(
|
||||||
|
[](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||||
|
{
|
||||||
|
return component.someNonViralCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
lease.getSyncCanceler());
|
||||||
|
},
|
||||||
|
[](std::exception_ptr &exceptionPtr)
|
||||||
|
{
|
||||||
|
sscl::co::NonViralCompletion nvc(exceptionPtr);
|
||||||
|
nvc.checkAndRethrowException();
|
||||||
|
});
|
||||||
|
|
||||||
|
nursery.closeAdmission();
|
||||||
|
nursery.requestCancelOnAll();
|
||||||
|
nursery.syncAwaitAllSettlements(
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext());
|
||||||
|
```
|
||||||
|
|
||||||
|
Each slot owns a `SyncCancelerForAsyncWork`. `requestCancelOnAll()` only signals
|
||||||
|
cooperative stop; it does not destroy invokers. Invokers are retired when their
|
||||||
|
completion callbacks run. Call `closeAdmission()` before `requestCancelOnAll()`
|
||||||
|
so no new work can be admitted after cancel begins, and call `closeAdmission()`
|
||||||
|
explicitly before `asyncAwaitAllSettlements()` or `syncAwaitAllSettlements()`;
|
||||||
|
those APIs wait until all slots have retired naturally and throw if admission is
|
||||||
|
still open.
|
||||||
|
|
||||||
|
`syncAwaitAllSettlements()` runs a nested `io_context` loop on the **calling
|
||||||
|
thread** (it blocks in `run_one()` until every slot has retired). Pass the
|
||||||
|
caller thread's `io_context` — usually `ComponentThread::getSelf()->getIoContext()`
|
||||||
|
— not some other thread's context. While the caller is blocked pumping another
|
||||||
|
thread's queue, handlers posted to the caller's own `io_context` are abandoned
|
||||||
|
and the drain can deadlock even when in-flight work has already completed on a
|
||||||
|
different thread.
|
||||||
|
|
||||||
|
`launch(factory, onSettledHook)` registers a non-null hook before `fillSlot()`.
|
||||||
|
Omit the hook (default `nullptr`) when no settlement callback is needed.
|
||||||
|
`Slot::Lease` is commit-required: an uncommitted lease removes its
|
||||||
|
reservation on destruction. `fillSlot()` takes an invoker factory (deferred
|
||||||
|
construction) because non-viral coroutines may complete synchronously during
|
||||||
|
invoker construction. The factory may capture `lease` by reference;
|
||||||
|
`setOnSettledHook()` and the hook passed to `launch()` may not capture `lease`
|
||||||
|
itself. The nursery passes the slot's `exceptionPtr` into the hook at retirement.
|
||||||
|
`Slot::Handle` is an opaque slot pointer valid only while the slot remains in the
|
||||||
|
nursery.
|
||||||
|
|
||||||
|
Slot metadata (`exceptionPtr`, lease/settlement status, canceler) lives on `Slot`.
|
||||||
|
`MemberInvokerBase` is invoker type-erasure only.
|
||||||
|
|
||||||
|
### Coroutine-Aware Locking
|
||||||
|
|
||||||
|
`co::CoQutex` is a coroutine-aware mutual exclusion primitive. It tracks
|
||||||
|
acquired locks through the coroutine promise chain, which lets it detect
|
||||||
|
dangerous re-acquisition patterns across nested coroutine calls instead of only
|
||||||
|
within one stack frame.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto releaseHandle = co_await qutex.getAcquireInvocationAndSuspensionPolicy();
|
||||||
|
```
|
||||||
|
|
||||||
|
When a coroutine cannot acquire the qutex, it suspends and is resumed through
|
||||||
|
the correct caller `io_context`.
|
||||||
|
|
||||||
|
## Design Biases
|
||||||
|
|
||||||
|
libspinscale intentionally favors:
|
||||||
|
|
||||||
|
- Explicit thread ownership over invisible executor selection.
|
||||||
|
- Member coroutine APIs over free-function workaround layers.
|
||||||
|
- `co_await` orchestration inside coroutine code.
|
||||||
|
- Callback completion only at non-coroutine boundaries.
|
||||||
|
- Structured group settlement over ad hoc counters and flags.
|
||||||
|
- Type-visible posting behavior over ambient global schedulers.
|
||||||
|
|
||||||
|
These choices make the library opinionated. That is the point. It is designed
|
||||||
|
for systems where implicit scheduling is a source of bugs.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
libspinscale is not a general replacement for:
|
||||||
|
|
||||||
|
- Boost.Asio networking and executor facilities.
|
||||||
|
- Folly's broad async service infrastructure.
|
||||||
|
- cppcoro's foundational coroutine primitive set.
|
||||||
|
- Standard library coroutine machinery.
|
||||||
|
|
||||||
|
It also is not trying to hide C++ coroutine mechanics. Promise types, invokers,
|
||||||
|
and suspension behavior are part of the public design because the target
|
||||||
|
applications need control over where work runs and where completion resumes.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
The API is still evolving. The current direction is centered on coroutine-native
|
||||||
|
component orchestration:
|
||||||
|
|
||||||
|
- `boost::asio::io_context` is the thread event-loop primitive.
|
||||||
|
- Posting coroutines use `TaggedPostingPromise<T, ThreadTag>`.
|
||||||
|
- Runtime-selected posting uses `DynamicViralPostingInvoker<T>`.
|
||||||
|
- Component lifecycle batches are viral non-posting coroutines.
|
||||||
|
- `co::Group` is the primary structured fan-out/fan-in primitive.
|
||||||
|
- `co::NonViralTaskNursery` owns non-viral invoker lifetimes at outer boundaries.
|
||||||
|
|
||||||
|
Expect breaking changes when they simplify the ownership, lifecycle, or
|
||||||
|
post-to/post-back model.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# EXPLANATION:
|
||||||
|
# Shared Boost deps for standalone or nested libspinscale builds.
|
||||||
|
# Always require Boost.Log as a shared library. Require Boost.System only on
|
||||||
|
# versions that still ship a compiled stub (Ubuntu dropped it at Boost 1.89).
|
||||||
|
#
|
||||||
|
# Sets BOOST_SHARED_DEP_TARGETS for target_link_libraries(... ${BOOST_SHARED_DEP_TARGETS}).
|
||||||
|
# Uses Boost:: imported targets (not a project INTERFACE lib) so export sets stay valid.
|
||||||
|
|
||||||
|
set(BOOST_COMPILED_SYSTEM_REMOVED_VERSION "1.89.0")
|
||||||
|
set(BOOST_SHARED_DEPS_MIN_VERSION "1.69")
|
||||||
|
|
||||||
|
function(boostSharedDepsComponentsForVersion _boostVersion _outVar)
|
||||||
|
set(_components log)
|
||||||
|
if(_boostVersion VERSION_LESS "${BOOST_COMPILED_SYSTEM_REMOVED_VERSION}")
|
||||||
|
list(APPEND _components system)
|
||||||
|
endif()
|
||||||
|
set(${_outVar} ${_components} PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(boostSharedDepsLinkTargets _outVar)
|
||||||
|
set(_targets Boost::log)
|
||||||
|
if(TARGET Boost::system)
|
||||||
|
list(APPEND _targets Boost::system)
|
||||||
|
endif()
|
||||||
|
set(${_outVar} ${_targets} PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(boostSharedDepsReportStatus)
|
||||||
|
if(TARGET Boost::system)
|
||||||
|
message(STATUS
|
||||||
|
"Boost ${Boost_VERSION}: linking Boost::system "
|
||||||
|
"(compiled stub still present)")
|
||||||
|
else()
|
||||||
|
message(STATUS
|
||||||
|
"Boost ${Boost_VERSION}: omitting Boost::system "
|
||||||
|
"(header-only; compiled stub removed at "
|
||||||
|
"${BOOST_COMPILED_SYSTEM_REMOVED_VERSION})")
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
if(NOT BOOST_SHARED_DEPS_RESOLVED)
|
||||||
|
# Prefer shared Boost libs where a compiled component still exists.
|
||||||
|
set(Boost_USE_STATIC_LIBS OFF)
|
||||||
|
set(Boost_USE_HEADER_ONLY OFF)
|
||||||
|
|
||||||
|
# Resolve version before requesting components so we can skip system on
|
||||||
|
# Boost 1.89+, where Ubuntu no longer packages libboost_system.
|
||||||
|
find_package(Boost ${BOOST_SHARED_DEPS_MIN_VERSION} REQUIRED)
|
||||||
|
boostSharedDepsComponentsForVersion("${Boost_VERSION}" _boostSharedDepsComponents)
|
||||||
|
find_package(Boost ${BOOST_SHARED_DEPS_MIN_VERSION} REQUIRED
|
||||||
|
COMPONENTS ${_boostSharedDepsComponents})
|
||||||
|
|
||||||
|
boostSharedDepsLinkTargets(BOOST_SHARED_DEP_TARGETS)
|
||||||
|
set(BOOST_SHARED_DEPS_RESOLVED TRUE)
|
||||||
|
boostSharedDepsReportStatus()
|
||||||
|
|
||||||
|
# Ensure remaining Boost libs (e.g. Log) use dynamic linking.
|
||||||
|
add_compile_definitions(BOOST_ALL_DYN_LINK)
|
||||||
|
endif()
|
||||||
Submodule
+1
Submodule googletest added at 7140cd416c
@@ -8,4 +8,7 @@
|
|||||||
/* Debug callable tracing configuration */
|
/* Debug callable tracing configuration */
|
||||||
#cmakedefine CONFIG_DEBUG_TRACE_CALLABLES
|
#cmakedefine CONFIG_DEBUG_TRACE_CALLABLES
|
||||||
|
|
||||||
|
/* Debug coroutine-type logging configuration */
|
||||||
|
#cmakedefine CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
|
||||||
#endif /* _CONFIG_H */
|
#endif /* _CONFIG_H */
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
#ifndef ASYNCHRONOUS_BRIDGE_H
|
|
||||||
#define ASYNCHRONOUS_BRIDGE_H
|
|
||||||
|
|
||||||
#include <boostAsioLinkageFix.h>
|
|
||||||
#include <atomic>
|
|
||||||
#include <boost/asio/io_service.hpp>
|
|
||||||
|
|
||||||
namespace sscl {
|
|
||||||
|
|
||||||
class AsynchronousBridge
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
AsynchronousBridge(boost::asio::io_service &io_service)
|
|
||||||
: isAsyncOperationComplete(false), io_service(io_service)
|
|
||||||
{}
|
|
||||||
|
|
||||||
void setAsyncOperationComplete(void)
|
|
||||||
{
|
|
||||||
/** EXPLANATION:
|
|
||||||
* This empty post()ed message is necessary to ensure that the thread
|
|
||||||
* that's waiting on the io_service is signaled to wake up and check
|
|
||||||
* the io_service's queue.
|
|
||||||
*/
|
|
||||||
isAsyncOperationComplete.store(true);
|
|
||||||
io_service.post([]{});
|
|
||||||
}
|
|
||||||
|
|
||||||
void waitForAsyncOperationCompleteOrIoServiceStopped(void)
|
|
||||||
{
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
io_service.run_one();
|
|
||||||
if (isAsyncOperationComplete.load() || io_service.stopped())
|
|
||||||
{ break; }
|
|
||||||
|
|
||||||
/** EXPLANATION:
|
|
||||||
* In the puppeteer and mind thread loops we call checkException()
|
|
||||||
* after run() returns, but we don't have to do that here because
|
|
||||||
* setException() calls stop().
|
|
||||||
*
|
|
||||||
* So if an exception is set on our thread, we'll break out of this
|
|
||||||
* loop due to the check for stopped() above, and that'll take us
|
|
||||||
* back out to the main loop, where we'll catch the exception.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool exitedBecauseIoServiceStopped(void) const
|
|
||||||
{ return io_service.stopped(); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::atomic<bool> isAsyncOperationComplete;
|
|
||||||
boost::asio::io_service &io_service;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sscl
|
|
||||||
|
|
||||||
#endif // ASYNCHRONOUS_BRIDGE_H
|
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
#ifndef CO_CONDITION_VARIABLE_H
|
||||||
|
#define CO_CONDITION_VARIABLE_H
|
||||||
|
|
||||||
|
#include <config.h>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <deque>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
#include <spinscale/spinLock.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/** Coroutine-friendly handoff: wait until `signal()` before running a completion
|
||||||
|
* step. Standalone primitive (posting promises use `PostBackStatus` instead).
|
||||||
|
*
|
||||||
|
* `clear()` only clears `isSignaled`; it does not wake or drain waiters. If
|
||||||
|
* `clear()` runs while coroutines are still waiting, they stay queued until a
|
||||||
|
* later `signal()` posts them.
|
||||||
|
*/
|
||||||
|
class CoConditionVariable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/** Waiter queued under the CV spin lock; `signal()` drains and calls `post()`. */
|
||||||
|
class WaitingCoroutineBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit WaitingCoroutineBase(
|
||||||
|
boost::asio::io_context &callerIoContextIn) noexcept
|
||||||
|
: callerIoContext(callerIoContextIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual ~WaitingCoroutineBase() = default;
|
||||||
|
|
||||||
|
virtual void post() noexcept = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
boost::asio::io_context &callerIoContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Promise>
|
||||||
|
class TypedWaitingCoroutine
|
||||||
|
: public WaitingCoroutineBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TypedWaitingCoroutine(
|
||||||
|
boost::asio::io_context &callerIoContextIn,
|
||||||
|
std::coroutine_handle<Promise> callerSchedHandleIn) noexcept
|
||||||
|
: WaitingCoroutineBase(callerIoContextIn),
|
||||||
|
callerSchedHandle(callerSchedHandleIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void post() noexcept override
|
||||||
|
{
|
||||||
|
boost::asio::post(callerIoContext, callerSchedHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::coroutine_handle<Promise> callerSchedHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OperationInvoker
|
||||||
|
{
|
||||||
|
explicit OperationInvoker(CoConditionVariable &parentCvIn) noexcept
|
||||||
|
: parentCv(parentCvIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
CoConditionVariable &parentCv;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WaitForInvoker
|
||||||
|
: public OperationInvoker
|
||||||
|
{
|
||||||
|
using OperationInvoker::OperationInvoker;
|
||||||
|
|
||||||
|
bool await_ready() const noexcept { return false; }
|
||||||
|
|
||||||
|
template <typename Promise>
|
||||||
|
bool await_suspend(std::coroutine_handle<Promise> cvCallerSchedHandle) noexcept
|
||||||
|
{
|
||||||
|
boost::asio::io_context &cvCallerIoContext =
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext();
|
||||||
|
|
||||||
|
sscl::SpinLock::Guard guard(parentCv.spinLock);
|
||||||
|
if (parentCv.isSignaled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " CV not signaled: Enqueuing waiter coroutine.\n";
|
||||||
|
#endif
|
||||||
|
parentCv.enqueueWaitingCoroutine(
|
||||||
|
cvCallerSchedHandle, cvCallerIoContext);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void await_resume() const noexcept {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Manual await-style API only (lowerCamelCase); not a coroutine awaiter.
|
||||||
|
* `FinalSuspendPostingInvoker` calls `awaitSuspend` explicitly.
|
||||||
|
*/
|
||||||
|
struct DecisionEnablingDerivableWaitForInvoker
|
||||||
|
: public OperationInvoker
|
||||||
|
{
|
||||||
|
struct DecisionFactors
|
||||||
|
{
|
||||||
|
sscl::SpinLock &cvInternalSpinLock;
|
||||||
|
bool wasAlreadySignaled;
|
||||||
|
|
||||||
|
DecisionFactors(sscl::SpinLock &cvLockIn, bool signaledIn) noexcept
|
||||||
|
: cvInternalSpinLock(cvLockIn),
|
||||||
|
wasAlreadySignaled(signaledIn)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
using OperationInvoker::OperationInvoker;
|
||||||
|
|
||||||
|
void operator co_await() const = delete;
|
||||||
|
|
||||||
|
bool awaitReady() const noexcept { return false; }
|
||||||
|
|
||||||
|
template <typename Promise>
|
||||||
|
DecisionFactors awaitSuspend(std::coroutine_handle<Promise> cvCallerSchedHandle) noexcept
|
||||||
|
{
|
||||||
|
boost::asio::io_context &cvCallerIoContext =
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext();
|
||||||
|
|
||||||
|
parentCv.spinLock.acquire();
|
||||||
|
if (parentCv.isSignaled)
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " CV already signaled: returning already-signaled DecisionFactors.\n";
|
||||||
|
#endif
|
||||||
|
return DecisionFactors(parentCv.spinLock, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " CV not signaled: returning not-signaled DecisionFactors.\n";
|
||||||
|
#endif
|
||||||
|
parentCv.enqueueWaitingCoroutine(
|
||||||
|
cvCallerSchedHandle, cvCallerIoContext);
|
||||||
|
|
||||||
|
return DecisionFactors(parentCv.spinLock, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void awaitResume() const noexcept {}
|
||||||
|
};
|
||||||
|
|
||||||
|
CoConditionVariable() noexcept = default;
|
||||||
|
CoConditionVariable(const CoConditionVariable &) = delete;
|
||||||
|
CoConditionVariable &operator=(const CoConditionVariable &) = delete;
|
||||||
|
CoConditionVariable(CoConditionVariable &&) noexcept = delete;
|
||||||
|
CoConditionVariable &operator=(CoConditionVariable &&) noexcept = delete;
|
||||||
|
~CoConditionVariable() noexcept = default;
|
||||||
|
|
||||||
|
WaitForInvoker getWaitForInvoker() noexcept
|
||||||
|
{ return WaitForInvoker(*this); }
|
||||||
|
|
||||||
|
DecisionEnablingDerivableWaitForInvoker
|
||||||
|
getDecisionEnablingDerivableWaitForInvoker() noexcept
|
||||||
|
{
|
||||||
|
return DecisionEnablingDerivableWaitForInvoker(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void signal() noexcept
|
||||||
|
{
|
||||||
|
std::deque<std::unique_ptr<WaitingCoroutineBase>> drained;
|
||||||
|
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(spinLock);
|
||||||
|
isSignaled = true;
|
||||||
|
drained.swap(waitingCoroutines);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::unique_ptr<WaitingCoroutineBase> &waiter : drained) {
|
||||||
|
waiter->post();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only clears the signaled flag; waiters (if any) remain in the deque. */
|
||||||
|
void clear() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(spinLock);
|
||||||
|
isSignaled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Promise>
|
||||||
|
void enqueueWaitingCoroutine(
|
||||||
|
std::coroutine_handle<Promise> handle,
|
||||||
|
boost::asio::io_context &ctx) noexcept
|
||||||
|
{
|
||||||
|
waitingCoroutines.push_back(
|
||||||
|
std::make_unique<TypedWaitingCoroutine<Promise>>(ctx, handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
sscl::SpinLock spinLock;
|
||||||
|
bool isSignaled = false;
|
||||||
|
std::deque<std::unique_ptr<WaitingCoroutineBase>> waitingCoroutines;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // CO_CONDITION_VARIABLE_H
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#ifndef CO_QUTEX_H
|
||||||
|
#define CO_QUTEX_H
|
||||||
|
|
||||||
|
#include <config.h>
|
||||||
|
#include <cassert>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <deque>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
#include <spinscale/co/promiseChainWalker.h>
|
||||||
|
#include <spinscale/spinLock.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
class CoQutex
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
class ReleaseHandle;
|
||||||
|
|
||||||
|
CoQutex() noexcept = default;
|
||||||
|
|
||||||
|
CoQutex([[maybe_unused]] const std::string &_name) noexcept
|
||||||
|
:
|
||||||
|
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
|
||||||
|
name(_name),
|
||||||
|
#endif
|
||||||
|
isOwned(false)
|
||||||
|
{}
|
||||||
|
|
||||||
|
CoQutex(const CoQutex &) = delete;
|
||||||
|
CoQutex(CoQutex &&) noexcept = delete;
|
||||||
|
CoQutex &operator=(const CoQutex &) = delete;
|
||||||
|
CoQutex &operator=(CoQutex &&) noexcept = delete;
|
||||||
|
~CoQutex() = default;
|
||||||
|
|
||||||
|
struct AcquireInvocationAndSuspensionPolicy
|
||||||
|
{
|
||||||
|
AcquireInvocationAndSuspensionPolicy(CoQutex &_coQutex) noexcept
|
||||||
|
: coQutex(_coQutex)
|
||||||
|
{}
|
||||||
|
|
||||||
|
~AcquireInvocationAndSuspensionPolicy() noexcept = default;
|
||||||
|
|
||||||
|
struct WaitingCoroutine
|
||||||
|
{
|
||||||
|
WaitingCoroutine(
|
||||||
|
std::coroutine_handle<void> _callerSchedHandle,
|
||||||
|
boost::asio::io_context &_callerIoContext,
|
||||||
|
PromiseChainLink &_waitingPromise) noexcept
|
||||||
|
: callerSchedHandle(_callerSchedHandle),
|
||||||
|
callerIoContext(_callerIoContext),
|
||||||
|
waitingPromise(_waitingPromise)
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::coroutine_handle<void> callerSchedHandle;
|
||||||
|
boost::asio::io_context &callerIoContext;
|
||||||
|
PromiseChainLink &waitingPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool await_ready() noexcept { return false; }
|
||||||
|
|
||||||
|
template <typename Promise>
|
||||||
|
bool await_suspend(std::coroutine_handle<Promise> callerSchedHandle)
|
||||||
|
{
|
||||||
|
static_assert(
|
||||||
|
std::is_base_of_v<PromiseChainLink, Promise>,
|
||||||
|
"CoQutex acquire requires a promise type derived from PromiseChainLink");
|
||||||
|
|
||||||
|
acquirerChainLink = &callerSchedHandle.promise();
|
||||||
|
|
||||||
|
walkCallerPromiseChainFrom(
|
||||||
|
static_cast<const PromiseChainLink &>(callerSchedHandle.promise()),
|
||||||
|
[this](const PromiseChainLink &link)
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Walking caller promise chain.\n";
|
||||||
|
#endif
|
||||||
|
if (link.holdsAcquiredLock(coQutex)) {
|
||||||
|
std::string message =
|
||||||
|
"Deadlock detected: CoQutex re-acquire on caller promise chain";
|
||||||
|
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
|
||||||
|
message += " (" + coQutex.name + ")";
|
||||||
|
#endif
|
||||||
|
message += ".";
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sscl::SpinLock::Guard guard(coQutex.spinLock);
|
||||||
|
if (!coQutex.isOwned) {
|
||||||
|
coQutex.isOwned = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
coQutex.waitingCoroutines.emplace_back(
|
||||||
|
std::coroutine_handle<void>::from_address(callerSchedHandle.address()),
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
*acquirerChainLink);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseHandle
|
||||||
|
// [[nodiscard("store co_await result; lock is held until ReleaseHandle is released")]]
|
||||||
|
await_resume() noexcept;
|
||||||
|
|
||||||
|
CoQutex &coQutex;
|
||||||
|
|
||||||
|
private:
|
||||||
|
PromiseChainLink *acquirerChainLink = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
AcquireInvocationAndSuspensionPolicy
|
||||||
|
// [[nodiscard("store co_await result; lock is held until ReleaseHandle is released")]]
|
||||||
|
getAcquireInvocationAndSuspensionPolicy() noexcept
|
||||||
|
{
|
||||||
|
return AcquireInvocationAndSuspensionPolicy(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class ReleaseHandle;
|
||||||
|
|
||||||
|
void release() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(spinLock);
|
||||||
|
|
||||||
|
assert(isOwned);
|
||||||
|
if (waitingCoroutines.empty()) {
|
||||||
|
isOwned = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &frontWaitingCoroutine = waitingCoroutines.front();
|
||||||
|
boost::asio::post(
|
||||||
|
frontWaitingCoroutine.callerIoContext,
|
||||||
|
frontWaitingCoroutine.callerSchedHandle);
|
||||||
|
waitingCoroutines.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
|
||||||
|
std::string name;
|
||||||
|
#endif
|
||||||
|
sscl::SpinLock spinLock;
|
||||||
|
bool isOwned = false;
|
||||||
|
std::deque<AcquireInvocationAndSuspensionPolicy::WaitingCoroutine> waitingCoroutines;
|
||||||
|
};
|
||||||
|
|
||||||
|
//[[nodiscard("store co_await result; lock is held until ReleaseHandle is released")]]
|
||||||
|
class CoQutex::ReleaseHandle
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ReleaseHandle(PromiseChainLink &promiseChainLinkIn, CoQutex &coQutexIn) noexcept
|
||||||
|
: promiseChainLink(promiseChainLinkIn),
|
||||||
|
coQutex(coQutexIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
ReleaseHandle(const ReleaseHandle &) = delete;
|
||||||
|
ReleaseHandle &operator=(const ReleaseHandle &) = delete;
|
||||||
|
|
||||||
|
ReleaseHandle(ReleaseHandle &&other) noexcept
|
||||||
|
: promiseChainLink(other.promiseChainLink),
|
||||||
|
coQutex(other.coQutex),
|
||||||
|
armed(other.armed)
|
||||||
|
{
|
||||||
|
other.armed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseHandle &operator=(ReleaseHandle &&other) noexcept = delete;
|
||||||
|
|
||||||
|
~ReleaseHandle() noexcept
|
||||||
|
{
|
||||||
|
if (armed)
|
||||||
|
{ release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void release() noexcept
|
||||||
|
{
|
||||||
|
if (!armed)
|
||||||
|
{ return; }
|
||||||
|
|
||||||
|
armed = false;
|
||||||
|
promiseChainLink.removeAcquiredLock(coQutex);
|
||||||
|
coQutex.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()() noexcept
|
||||||
|
{
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
PromiseChainLink &promiseChainLink;
|
||||||
|
CoQutex &coQutex;
|
||||||
|
bool armed = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline CoQutex::ReleaseHandle
|
||||||
|
// [[nodiscard("store co_await result; lock is held until ReleaseHandle is released")]]
|
||||||
|
CoQutex::AcquireInvocationAndSuspensionPolicy::await_resume() noexcept
|
||||||
|
{
|
||||||
|
assert(acquirerChainLink != nullptr);
|
||||||
|
acquirerChainLink->addAcquiredLock(coQutex);
|
||||||
|
return CoQutex::ReleaseHandle(*acquirerChainLink, coQutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // CO_QUTEX_H
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#ifndef DYNAMIC_POSTING_INVOKER_H
|
||||||
|
#define DYNAMIC_POSTING_INVOKER_H
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
|
#include <spinscale/co/postingPromise.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/** Fallback ThreadTag for DynamicViralPostingInvoker when ExplicitPostTarget is
|
||||||
|
* omitted. Callers must always pass ExplicitPostTarget in production paths.
|
||||||
|
*/
|
||||||
|
struct DynamicPostTargetThreadTag
|
||||||
|
{
|
||||||
|
static boost::asio::io_context &io_context()
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__)
|
||||||
|
+ ": ExplicitPostTarget required for DynamicViralPostingInvoker");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
using DynamicPostingPromise =
|
||||||
|
TaggedPostingPromise<T, DynamicPostTargetThreadTag>;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
using DynamicViralPostingInvoker =
|
||||||
|
ViralPostingInvoker<DynamicPostingPromise, T>;
|
||||||
|
|
||||||
|
using DynamicNonViralPostingInvoker =
|
||||||
|
NonViralPostingInvoker<DynamicPostingPromise>;
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // DYNAMIC_POSTING_INVOKER_H
|
||||||
@@ -0,0 +1,652 @@
|
|||||||
|
#ifndef GROUP_H
|
||||||
|
#define GROUP_H
|
||||||
|
|
||||||
|
#include <any>
|
||||||
|
#include <cassert>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <iostream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
#include <spinscale/co/promiseChainLink.h>
|
||||||
|
#include <spinscale/sharedResourceGroup.h>
|
||||||
|
#include <spinscale/spinLock.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
template <typename T, typename H>
|
||||||
|
concept await_suspend_returns_void = requires(T &t, H h) {
|
||||||
|
{ t.await_suspend(h) } -> std::same_as<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename H>
|
||||||
|
concept await_suspend_returns_bool = requires(T &t, H h) {
|
||||||
|
{ t.await_suspend(h) } -> std::convertible_to<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename H>
|
||||||
|
concept await_suspend_returns_handle = requires(T &t, H h) {
|
||||||
|
{ t.await_suspend(h) } -> std::convertible_to<std::coroutine_handle<>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename H>
|
||||||
|
concept await_suspend_ok = await_suspend_returns_void<T, H>
|
||||||
|
|| await_suspend_returns_bool<T, H>
|
||||||
|
|| await_suspend_returns_handle<T, H>;
|
||||||
|
|
||||||
|
template <typename T, typename H = std::coroutine_handle<>>
|
||||||
|
concept AwaiterIface = requires(T &t, H h) {
|
||||||
|
{ t.await_ready() } -> std::convertible_to<bool>;
|
||||||
|
{ t.await_resume() };
|
||||||
|
} && await_suspend_ok<T, H>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto get_operator_co_await(T &t) -> decltype(operator co_await(t));
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept AwaitableIface = requires(T &t) {
|
||||||
|
{ get_operator_co_await(t) };
|
||||||
|
} && AwaiterIface<decltype(get_operator_co_await(std::declval<T &>()))>;
|
||||||
|
|
||||||
|
template<AwaiterIface T>
|
||||||
|
T &asAwaiter(T &t) noexcept
|
||||||
|
{
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<AwaitableIface T>
|
||||||
|
auto asAwaiter(T &t) noexcept(noexcept(get_operator_co_await(t)))
|
||||||
|
-> decltype(get_operator_co_await(t))
|
||||||
|
{
|
||||||
|
return get_operator_co_await(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool endsWithLineBreak(const std::string &message)
|
||||||
|
{
|
||||||
|
return !message.empty()
|
||||||
|
&& (message.back() == '\n' || message.back() == '\r');
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void appendGroupAdapterExceptionLine(
|
||||||
|
std::ostringstream &ostream, std::exception_ptr exceptionPtr)
|
||||||
|
{
|
||||||
|
ostream << "Exc thrown in Group Adapter: ";
|
||||||
|
try {
|
||||||
|
std::rethrow_exception(exceptionPtr);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
const std::string message = e.what();
|
||||||
|
ostream << message;
|
||||||
|
if (!endsWithLineBreak(message)) {
|
||||||
|
ostream << "\n";
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
ostream << "<unknown exception type>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept AwaitableIface = detail::AwaitableIface<T>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept AwaiterIface = detail::AwaiterIface<T>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept AwaitableOrAwaiterIface = AwaiterIface<T> || AwaitableIface<T>;
|
||||||
|
|
||||||
|
/** Typical usage — parallel members, then gather:
|
||||||
|
*
|
||||||
|
* co::Group group;
|
||||||
|
*
|
||||||
|
* auto bodyInit = body.initializeCReq(exceptionPtr, noopCallback);
|
||||||
|
* auto legInit = leg.initializeCReq(exceptionPtr, noopCallback);
|
||||||
|
* ViralNonPostingInvoker<void> batch = app.joltAllPuppetThreadsCReq(...);
|
||||||
|
*
|
||||||
|
* group.add(bodyInit);
|
||||||
|
* group.add(legInit);
|
||||||
|
* group.add(batch);
|
||||||
|
*
|
||||||
|
* co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
* group.checkForAndReThrowGroupExceptions();
|
||||||
|
*
|
||||||
|
* (void)bodyInit.completedReturnValues();
|
||||||
|
*
|
||||||
|
* // When walking settlement slots by index:
|
||||||
|
* settlements[i].invokerAs<BodyViralPostingInvoker<void>>()
|
||||||
|
* .completedReturnValues();
|
||||||
|
*/
|
||||||
|
struct Group
|
||||||
|
{
|
||||||
|
enum class AwaitingCondition {
|
||||||
|
NONE, FIRST_SETTLED, ALL_SETTLED
|
||||||
|
};
|
||||||
|
|
||||||
|
class SettlementDescriptor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum class TypeE {
|
||||||
|
/* We track EXCEPTIION_THROWN but we don't provide an
|
||||||
|
* awaitInvoker for exception events. The caller can
|
||||||
|
* wait for settlements and then scan the result set
|
||||||
|
* to manually deal with exceptions.
|
||||||
|
*/
|
||||||
|
UNSETTLED, COMPLETED, EXCEPTION_THROWN
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename Member>
|
||||||
|
void bindMemberRef(Member &member)
|
||||||
|
{
|
||||||
|
memberInvokerRef = std::ref(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Member>
|
||||||
|
Member &invokerAs() const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return std::any_cast<std::reference_wrapper<Member>>(
|
||||||
|
memberInvokerRef).get();
|
||||||
|
} catch (const std::bad_any_cast &) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Group settlement invoker type mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSettlementStatus() noexcept
|
||||||
|
{
|
||||||
|
assert(type == TypeE::UNSETTLED);
|
||||||
|
|
||||||
|
if (calleeException) {
|
||||||
|
type = TypeE::EXCEPTION_THROWN;
|
||||||
|
} else {
|
||||||
|
type = TypeE::COMPLETED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeE type = TypeE::UNSETTLED;
|
||||||
|
std::exception_ptr calleeException = nullptr;
|
||||||
|
std::exception_ptr adapterException = nullptr;
|
||||||
|
std::any memberInvokerRef;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SettlementAwaitingInvoker;
|
||||||
|
struct AwaitFirstSettlementInvoker;
|
||||||
|
struct AwaitAllSettlementsInvoker;
|
||||||
|
|
||||||
|
// getAwaitNextSettlementInvoker();
|
||||||
|
AwaitFirstSettlementInvoker getAwaitFirstSettlementInvoker()
|
||||||
|
{ return AwaitFirstSettlementInvoker(*this); }
|
||||||
|
|
||||||
|
AwaitAllSettlementsInvoker getAwaitAllSettlementsInvoker()
|
||||||
|
{ return AwaitAllSettlementsInvoker(*this); }
|
||||||
|
|
||||||
|
bool verifyAllInvokersSettled() const
|
||||||
|
{
|
||||||
|
for (auto &desc : s.rsrc.settlements) {
|
||||||
|
if (desc.type == SettlementDescriptor::TypeE::UNSETTLED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool firstInvokerSettled() const
|
||||||
|
{ return s.rsrc.firstSettledInvokerIdx >= 0; }
|
||||||
|
|
||||||
|
bool allInvokersSettled() const
|
||||||
|
{
|
||||||
|
const std::size_t nInvokersAdded = s.rsrc.settlements.size();
|
||||||
|
assert(s.rsrc.nInvokersSettled <= nInvokersAdded);
|
||||||
|
return s.rsrc.nInvokersSettled == nInvokersAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Caller must hold s.lock. */
|
||||||
|
void throwIfNoMemberInvokersForCoAwaitUnderLock() const
|
||||||
|
{
|
||||||
|
if (s.rsrc.settlements.empty()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"co_await: Group has no member invokers; call add() before awaiting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettlementAwaitingInvoker
|
||||||
|
{
|
||||||
|
explicit SettlementAwaitingInvoker(Group &_group)
|
||||||
|
: parentGroup(_group)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool await_ready() const { return false; }
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* This exists for if we ever need to re-make the adapter coro
|
||||||
|
* throw exceptions. But we decided to make it noexcept in order
|
||||||
|
* to avoid this complication.
|
||||||
|
*/
|
||||||
|
void checkForAndReThrowAdapterExceptions() const
|
||||||
|
{
|
||||||
|
std::ostringstream ostream;
|
||||||
|
bool doThrow = false;
|
||||||
|
|
||||||
|
for (auto &item : parentGroup.s.rsrc.settlements)
|
||||||
|
{
|
||||||
|
if (!item.adapterException) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
doThrow = true;
|
||||||
|
detail::appendGroupAdapterExceptionLine(
|
||||||
|
ostream, item.adapterException);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doThrow) {
|
||||||
|
throw std::runtime_error(ostream.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Group &parentGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* AwaitingCondition and the group-awaiter coroutine_handle are set only
|
||||||
|
* in await_suspend when this co_await actually suspends. Constructing
|
||||||
|
* several AwaitFirstSettlementInvoker / AwaitAllSettlementsInvoker
|
||||||
|
* objects without co_awaiting them is harmless.
|
||||||
|
*
|
||||||
|
* You may co_await await-all and later co_await await-first (in either
|
||||||
|
* construction order). After a suspending wait completes, the adapter
|
||||||
|
* clears handle state in updateSettlementsStateAndAwakenCallerIfConditionMet,
|
||||||
|
* so a later co_await on another handle (or a second co_await on the same
|
||||||
|
* handle, after the first finished) is legal.
|
||||||
|
*
|
||||||
|
* Only one group co_await may be suspended with a registered handle at a
|
||||||
|
* time; a second concurrent co_await trips assert(!callerHasSetSchedHandle)
|
||||||
|
* in debug builds.
|
||||||
|
*
|
||||||
|
* firstSettledInvokerIdx and calleeWasReadyToNotifyOfFirstSettlement are
|
||||||
|
* sticky for the Group lifetime (first member ever to settle), not per wave.
|
||||||
|
*/
|
||||||
|
struct AwaitFirstSettlementInvoker
|
||||||
|
: public SettlementAwaitingInvoker
|
||||||
|
{
|
||||||
|
using SettlementAwaitingInvoker::SettlementAwaitingInvoker;
|
||||||
|
|
||||||
|
bool await_suspend(std::coroutine_handle<> groupAwaiterSchedHandle)
|
||||||
|
{
|
||||||
|
/* No other group co_await may be suspended with a registered handle.
|
||||||
|
* Sequential co_await on the same object is allowed after the prior
|
||||||
|
* wait finished and clearCallerSchedHandleState() ran on wake.
|
||||||
|
*/
|
||||||
|
assert(!this->parentGroup.s.rsrc.callerHasSetSchedHandle);
|
||||||
|
|
||||||
|
sscl::SpinLock::Guard guard(this->parentGroup.s.lock);
|
||||||
|
|
||||||
|
this->parentGroup.throwIfNoMemberInvokersForCoAwaitUnderLock();
|
||||||
|
|
||||||
|
if (this->parentGroup.s.rsrc.calleeWasReadyToNotifyOfFirstSettlement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We store away the coro_handle of the
|
||||||
|
* group awaiter, and suspend that group awaiter.
|
||||||
|
*/
|
||||||
|
this->parentGroup.s.rsrc.setCallerSchedHandleAndCondition(
|
||||||
|
groupAwaiterSchedHandle, AwaitingCondition::FIRST_SETTLED);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<SettlementDescriptor &, std::vector<SettlementDescriptor> &>
|
||||||
|
await_resume()
|
||||||
|
{
|
||||||
|
assert(this->parentGroup.firstInvokerSettled());
|
||||||
|
return {
|
||||||
|
this->parentGroup.s.rsrc.settlements[
|
||||||
|
this->parentGroup.s.rsrc.firstSettledInvokerIdx],
|
||||||
|
this->parentGroup.s.rsrc.settlements
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Same awaiting rules as AwaitFirstSettlementInvoker (see above).
|
||||||
|
*
|
||||||
|
* It is illegal to add() new members while a group co_await is suspended
|
||||||
|
* (groupAwaiterSchedHandle is registered). You may add() after co_await
|
||||||
|
* returns, including starting a new settlement wave before the next
|
||||||
|
* co_await.
|
||||||
|
*/
|
||||||
|
struct AwaitAllSettlementsInvoker
|
||||||
|
: public SettlementAwaitingInvoker
|
||||||
|
{
|
||||||
|
using SettlementAwaitingInvoker::SettlementAwaitingInvoker;
|
||||||
|
|
||||||
|
bool await_suspend(std::coroutine_handle<> groupAwaiterSchedHandle)
|
||||||
|
{
|
||||||
|
/* See AwaitFirstSettlementInvoker::await_suspend. Handle state is
|
||||||
|
* cleared when the adapter wakes a suspended group co_awaiter, not
|
||||||
|
* in await_resume.
|
||||||
|
*/
|
||||||
|
assert(!this->parentGroup.s.rsrc.callerHasSetSchedHandle);
|
||||||
|
|
||||||
|
sscl::SpinLock::Guard guard(this->parentGroup.s.lock);
|
||||||
|
|
||||||
|
this->parentGroup.throwIfNoMemberInvokersForCoAwaitUnderLock();
|
||||||
|
|
||||||
|
if (this->parentGroup.allInvokersSettled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->parentGroup.s.rsrc.setCallerSchedHandleAndCondition(
|
||||||
|
groupAwaiterSchedHandle, AwaitingCondition::ALL_SETTLED);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SettlementDescriptor> &await_resume()
|
||||||
|
{
|
||||||
|
assert(this->parentGroup.allInvokersSettled());
|
||||||
|
return this->parentGroup.s.rsrc.settlements;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NonAwaitableNonPostingAdapterCoro
|
||||||
|
{
|
||||||
|
struct promise_type
|
||||||
|
: public PromiseChainLink
|
||||||
|
{
|
||||||
|
NonAwaitableNonPostingAdapterCoro get_return_object() noexcept
|
||||||
|
{ return {}; }
|
||||||
|
|
||||||
|
void removeAcquiredLock(CoQutex &) noexcept override
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::suspend_never initial_suspend() noexcept { return {}; }
|
||||||
|
/** EXPLANATION:
|
||||||
|
* final_suspend must return suspend_never here so that
|
||||||
|
* this fire-and-forget adapter coro will be self-destroying.
|
||||||
|
*/
|
||||||
|
std::suspend_never final_suspend() noexcept { return {}; }
|
||||||
|
void return_void() noexcept { return; }
|
||||||
|
void unhandled_exception() noexcept
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
auto eptr = std::current_exception();
|
||||||
|
if (eptr) {
|
||||||
|
std::rethrow_exception(eptr);
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
std::cerr << "Unhandled exception in Group adapter coroutine:\n"
|
||||||
|
<< e.what() << "\n";
|
||||||
|
} catch (...) {
|
||||||
|
std::cerr << "Unhandled non-std exception in Group adapter coroutine\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::terminate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NonAwaitableNonPostingAdapterCoro() noexcept = default;
|
||||||
|
NonAwaitableNonPostingAdapterCoro operator co_await() const = delete;
|
||||||
|
bool await_ready() const { std::terminate(); return false; }
|
||||||
|
void await_suspend() const { std::terminate(); }
|
||||||
|
void await_resume() const { std::terminate(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::pair<bool, bool>
|
||||||
|
updateSettlementsStateAndAwakenCallerIfConditionMet(
|
||||||
|
std::size_t settlementIndex) noexcept
|
||||||
|
{
|
||||||
|
bool isFirstSettlement = false;
|
||||||
|
bool isLastSettlement = false;
|
||||||
|
std::coroutine_handle<> groupAwaiterSchedHandleToWake = nullptr;
|
||||||
|
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|
||||||
|
/* If we can be certain that the AllSettled condition won't
|
||||||
|
* be triggered repeatedly, then we can get rid of
|
||||||
|
* calleeWasReadyToNotifyOfLastSettlementForCurrentSet.
|
||||||
|
*/
|
||||||
|
assert(s.rsrc.nInvokersSettled < s.rsrc.settlements.size());
|
||||||
|
assert(settlementIndex < s.rsrc.settlements.size());
|
||||||
|
s.rsrc.nInvokersSettled++;
|
||||||
|
|
||||||
|
if (!firstInvokerSettled())
|
||||||
|
{
|
||||||
|
isFirstSettlement = true;
|
||||||
|
s.rsrc.firstSettledInvokerIdx = static_cast<int>(settlementIndex);
|
||||||
|
|
||||||
|
/* This should be set-once & sticky throughout the lifetime
|
||||||
|
* of the Group object. The first invoker only gets
|
||||||
|
* settled once, irrespective of how many
|
||||||
|
* AwaitFirstSettlementInvoker instances we create.
|
||||||
|
*/
|
||||||
|
s.rsrc.calleeWasReadyToNotifyOfFirstSettlement = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allInvokersSettled())
|
||||||
|
{
|
||||||
|
assert(s.rsrc.nInvokersSettled == s.rsrc.settlements.size());
|
||||||
|
assert(verifyAllInvokersSettled());
|
||||||
|
isLastSettlement = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If no group co_awaiter registered a handle (did not suspend, or
|
||||||
|
* already woke and clearCallerSchedHandleState ran), there is
|
||||||
|
* nothing to post back to.
|
||||||
|
*/
|
||||||
|
if (!s.rsrc.callerHasSetSchedHandle) {
|
||||||
|
return {isFirstSettlement, isLastSettlement};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we're here, then callerHasSetSchedHandle must be true.
|
||||||
|
* I.e: an invoker has been created and co_awaited for one of the
|
||||||
|
* conditions.
|
||||||
|
* Therefore currentAwaitingCondition must also have been set,
|
||||||
|
* since currentAwaitingCondition is set in the invokers' ctors.
|
||||||
|
*/
|
||||||
|
assert(s.rsrc.currentAwaitingCondition != AwaitingCondition::NONE);
|
||||||
|
|
||||||
|
if ((isFirstSettlement
|
||||||
|
&& s.rsrc.currentAwaitingCondition == AwaitingCondition::FIRST_SETTLED)
|
||||||
|
|| (isLastSettlement
|
||||||
|
&& s.rsrc.currentAwaitingCondition == AwaitingCondition::ALL_SETTLED))
|
||||||
|
{
|
||||||
|
groupAwaiterSchedHandleToWake = s.rsrc.groupAwaiterSchedHandle;
|
||||||
|
|
||||||
|
/** We only clear here and not in await_resume, because if
|
||||||
|
* the caller hasn't already set it schedHandle by the time we're
|
||||||
|
* called, then when it eventually does call await_suspend, it
|
||||||
|
* won't set it then either.
|
||||||
|
*
|
||||||
|
* I.e: callerSchedHandle only needs to be cleared it if gets set
|
||||||
|
* in the first place;
|
||||||
|
* And it only gets set if we need to invoke the schedHandle from
|
||||||
|
* here.
|
||||||
|
* If the group co_awaiter is able to call await_resume, then it
|
||||||
|
* simply doesn't set its schedHandle at all.
|
||||||
|
*/
|
||||||
|
s.rsrc.clearCallerSchedHandleState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupAwaiterSchedHandleToWake)
|
||||||
|
{
|
||||||
|
/* We should be able to just directly resume() the group awaiter's handle
|
||||||
|
* here because that would invoke await_resume, which may destroy the
|
||||||
|
* callee's promise.
|
||||||
|
* And who is the callee? Is it not this coro here? And this coro
|
||||||
|
* hasn't been suspended. So we'd be destroying ourself while we're
|
||||||
|
* not suspended.
|
||||||
|
*
|
||||||
|
* But all of that only applies __IFF__ we actually do try to destroy
|
||||||
|
* the callee within the caller's Invoker. If we don't, then the callee
|
||||||
|
* should persist just fine. There's no implicit mechanism that
|
||||||
|
* will always destroy the callee coro state before the invoker
|
||||||
|
* is destroyed.
|
||||||
|
* If that was in fact the way it worked, then fire-and-forget coros
|
||||||
|
* would be impossible.
|
||||||
|
*
|
||||||
|
* So we should be able to call resume() directly here without
|
||||||
|
* post()ing to ComponentThread::getSelf()->getIoContext().
|
||||||
|
*
|
||||||
|
* EXPLANATION:
|
||||||
|
* However, in order to ensure that we keep this adapter coro
|
||||||
|
* method exception-free, we are forced to post() rather than
|
||||||
|
* directly calling the handle.
|
||||||
|
*/
|
||||||
|
boost::asio::post(
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
groupAwaiterSchedHandleToWake);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {isFirstSettlement, isLastSettlement};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* This coro is a coro which has a promise, and does __not__ expose an awaitable
|
||||||
|
* iface and in fact should not be capable of being awaited, ultimately.
|
||||||
|
*
|
||||||
|
* Its purpose is to be an adapter that enables the Group class to invoke the
|
||||||
|
* invokers that are added to it, without having to co_await those invokers.
|
||||||
|
* Rather, the Group class simply invokes this function on them, and then this
|
||||||
|
* function both co_awaits the invoker on behalf of the Group class, and also
|
||||||
|
* performs the normal function of an invoker, which is both to invoke the
|
||||||
|
* target async fn, and also to convey its results back to the Group class.
|
||||||
|
* It's effectively a go-between coro that provides the outcomes that Invokers
|
||||||
|
* normally provide, without needing, itself, to be co_awaited.
|
||||||
|
*
|
||||||
|
* settlementIndex is captured by value (not a vector iterator) so adapter
|
||||||
|
* coros remain valid if settlements reallocate during concurrent add().
|
||||||
|
*/
|
||||||
|
template<AwaitableOrAwaiterIface Member>
|
||||||
|
NonAwaitableNonPostingAdapterCoro memberAdapterCoro(
|
||||||
|
Member &memberInvoker,
|
||||||
|
std::size_t settlementIndex) noexcept
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
co_await detail::asAwaiter(memberInvoker);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
s.rsrc.settlements[settlementIndex].calleeException =
|
||||||
|
std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* From here onwards, we mustn't throw(). Unhandled exceptions
|
||||||
|
* generated by the adapter coro itself will result in
|
||||||
|
* std::terminate().
|
||||||
|
*/
|
||||||
|
s.rsrc.settlements[settlementIndex].setSettlementStatus();
|
||||||
|
updateSettlementsStateAndAwakenCallerIfConditionMet(settlementIndex);
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<AwaitableOrAwaiterIface Member>
|
||||||
|
void add(Member &memberInvoker)
|
||||||
|
{
|
||||||
|
std::size_t settlementIndex = 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|
||||||
|
if (s.rsrc.groupAwaiterSchedHandle)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
"add: New member invokers mustn't be added "
|
||||||
|
"while co_awaiting a given set");
|
||||||
|
}
|
||||||
|
|
||||||
|
settlementIndex = s.rsrc.settlements.size();
|
||||||
|
s.rsrc.settlements.emplace_back();
|
||||||
|
s.rsrc.settlements[settlementIndex].bindMemberRef(memberInvoker);
|
||||||
|
}
|
||||||
|
|
||||||
|
memberAdapterCoro(memberInvoker, settlementIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::exception_ptr captureAggregatedGroupExceptions() const
|
||||||
|
{
|
||||||
|
std::ostringstream ostream;
|
||||||
|
bool hasFailures = false;
|
||||||
|
|
||||||
|
for (auto &item : s.rsrc.settlements)
|
||||||
|
{
|
||||||
|
if (item.type != SettlementDescriptor::TypeE::EXCEPTION_THROWN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(item.calleeException);
|
||||||
|
|
||||||
|
hasFailures = true;
|
||||||
|
detail::appendGroupAdapterExceptionLine(
|
||||||
|
ostream, item.calleeException);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFailures) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_exception_ptr(std::runtime_error(ostream.str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkForAndReThrowGroupExceptions() const
|
||||||
|
{
|
||||||
|
std::exception_ptr aggregatedException =
|
||||||
|
captureAggregatedGroupExceptions();
|
||||||
|
if (aggregatedException) {
|
||||||
|
std::rethrow_exception(aggregatedException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State
|
||||||
|
{
|
||||||
|
void clearCallerSchedHandleState() noexcept
|
||||||
|
{
|
||||||
|
groupAwaiterSchedHandle = nullptr;
|
||||||
|
callerHasSetSchedHandle = false;
|
||||||
|
currentAwaitingCondition = AwaitingCondition::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCallerSchedHandleAndCondition(
|
||||||
|
std::coroutine_handle<> groupAwaiterSchedHandleIn,
|
||||||
|
AwaitingCondition awaitingCondition) noexcept
|
||||||
|
{
|
||||||
|
groupAwaiterSchedHandle = groupAwaiterSchedHandleIn;
|
||||||
|
callerHasSetSchedHandle = true;
|
||||||
|
currentAwaitingCondition = awaitingCondition;
|
||||||
|
}
|
||||||
|
|
||||||
|
int firstSettledInvokerIdx = -1;
|
||||||
|
std::size_t nInvokersSettled = 0;
|
||||||
|
std::coroutine_handle<> groupAwaiterSchedHandle = nullptr;
|
||||||
|
bool callerHasSetSchedHandle = false;
|
||||||
|
/* calleWasReady*First* is an indelible record of what
|
||||||
|
* occured during the first settlement's adapter's update.
|
||||||
|
*/
|
||||||
|
bool calleeWasReadyToNotifyOfFirstSettlement = false;
|
||||||
|
std::vector<SettlementDescriptor> settlements;
|
||||||
|
AwaitingCondition currentAwaitingCondition = AwaitingCondition::NONE;
|
||||||
|
};
|
||||||
|
|
||||||
|
sscl::SharedResourceGroup<sscl::SpinLock, State> s;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // GROUP_H
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#ifndef INVOKER_BASE_H
|
||||||
|
#define INVOKER_BASE_H
|
||||||
|
|
||||||
|
#include <config.h>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <spinscale/co/promiseChainLink.h>
|
||||||
|
#include <spinscale/co/returnValues.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/** Shared callee-frame owner and awaiter for posting and non-posting promises.
|
||||||
|
* Posting vs non-posting completion is implemented in each promise's PostBackStatus
|
||||||
|
* and final_suspend; this type only wires caller handles and reads return values.
|
||||||
|
*/
|
||||||
|
template <typename PromiseType, typename T>
|
||||||
|
class Invoker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit Invoker(PromiseType &_calleePromise) noexcept
|
||||||
|
: calleePromise(_calleePromise)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Invoker(const Invoker &) = delete;
|
||||||
|
Invoker &operator=(const Invoker &) = delete;
|
||||||
|
|
||||||
|
Invoker(Invoker &&other) noexcept
|
||||||
|
: calleePromise(other.calleePromise),
|
||||||
|
ownsFrameDestroy_(std::exchange(other.ownsFrameDestroy_, false))
|
||||||
|
{}
|
||||||
|
|
||||||
|
Invoker &operator=(Invoker &&other) = delete;
|
||||||
|
|
||||||
|
~Invoker() noexcept
|
||||||
|
{
|
||||||
|
if (!ownsFrameDestroy_) { return; }
|
||||||
|
|
||||||
|
std::coroutine_handle<> handle = calleePromise.selfSchedHandle;
|
||||||
|
if (handle) {
|
||||||
|
handle.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename CallerPromise>
|
||||||
|
bool setCallerSchedHandle(
|
||||||
|
std::coroutine_handle<CallerPromise> callerSchedHandle) noexcept
|
||||||
|
{
|
||||||
|
static_assert(
|
||||||
|
std::is_base_of_v<PromiseChainLink, CallerPromise>,
|
||||||
|
"Invoker caller promise must derive from PromiseChainLink");
|
||||||
|
|
||||||
|
calleePromise.callerSchedHandle = callerSchedHandle;
|
||||||
|
calleePromise.setCallerPromiseChainLink(&callerSchedHandle.promise());
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " Done setting callerSchedHandle; running CallerFlowExecutor.\n";
|
||||||
|
#endif
|
||||||
|
return calleePromise.postBackStatus.getCallerFlowExecutor()();
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnValues<T> &completedReturnValues() noexcept
|
||||||
|
{ return calleePromise.returnValues; }
|
||||||
|
|
||||||
|
const ReturnValues<T> &completedReturnValues() const noexcept
|
||||||
|
{ return calleePromise.returnValues; }
|
||||||
|
|
||||||
|
auto await_resume()
|
||||||
|
{
|
||||||
|
calleePromise.postBackStatus.reset();
|
||||||
|
|
||||||
|
ReturnValues<T> &returnValues = calleePromise.returnValues;
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " About to check for and rethrow any exception.\n";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (returnValues.myExceptionPtr)
|
||||||
|
{
|
||||||
|
std::exception_ptr const captured = returnValues.myExceptionPtr;
|
||||||
|
std::rethrow_exception(captured);
|
||||||
|
}
|
||||||
|
if constexpr (!std::is_void_v<T>)
|
||||||
|
{
|
||||||
|
T result = std::move(returnValues.myReturnValue);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
PromiseType &calleePromise;
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Every live invoker owns destruction of its callee coroutine frame in
|
||||||
|
* ~Invoker (via calleePromise.selfSchedHandle).
|
||||||
|
*
|
||||||
|
* The only time frame destruction is skipped is for a moved-from invoker
|
||||||
|
* after move construction, so we do not double-destroy the same handle
|
||||||
|
* when get_return_object() returns the invoker by value.
|
||||||
|
*
|
||||||
|
* This is not an opt-out for viral vs non-viral callers or for "callee
|
||||||
|
* still running"; callers must keep the invoker alive until the callee
|
||||||
|
* frame is no longer needed.
|
||||||
|
*/
|
||||||
|
bool ownsFrameDestroy_ = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename PromiseType, typename T>
|
||||||
|
using PostingInvoker = Invoker<PromiseType, T>;
|
||||||
|
|
||||||
|
template <typename PromiseType, typename T>
|
||||||
|
using NonPostingInvoker = Invoker<PromiseType, T>;
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // INVOKER_BASE_H
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
#ifndef INVOKERS_H
|
||||||
|
#define INVOKERS_H
|
||||||
|
|
||||||
|
#include <config.h>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <spinscale/co/invokerBase.h>
|
||||||
|
#include <spinscale/co/nonPostingPromise.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/** Non-viral coroutine entry that must not be co_awaited: promise is always
|
||||||
|
* PostingPromiseTemplate<void> (no return-value path to a caller).
|
||||||
|
*
|
||||||
|
* The invoker must outlive the callee frame: do not discard the return object
|
||||||
|
* from get_return_object(). ~Invoker destroys the callee frame.
|
||||||
|
*/
|
||||||
|
template <template <typename> class PostingPromiseTemplate>
|
||||||
|
struct NonViralPostingInvoker
|
||||||
|
: public PostingInvoker<PostingPromiseTemplate<void>, void>
|
||||||
|
{
|
||||||
|
struct promise_type
|
||||||
|
: public PostingPromiseTemplate<void>
|
||||||
|
{
|
||||||
|
using PostingPromiseTemplate<void>::PostingPromiseTemplate;
|
||||||
|
|
||||||
|
NonViralPostingInvoker<PostingPromiseTemplate> get_return_object()
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Returning NonViralPostingInvoker.\n";
|
||||||
|
#endif
|
||||||
|
if (!this->callerLambda)
|
||||||
|
{
|
||||||
|
/** EXPLANATION:
|
||||||
|
* We require a completion lambda to be provided to the
|
||||||
|
* non-viral coroutines, because that's how we internally
|
||||||
|
* distinguish between non-viral and viral coroutines.
|
||||||
|
*
|
||||||
|
* Additionally, non-viral coroutines almost never have a
|
||||||
|
* good reason to not have a completion lambda.
|
||||||
|
*/
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::this_thread::get_id()
|
||||||
|
<< ": Missing completion lambda: non-viral coroutines require a completion lambda."
|
||||||
|
<< " Promise type=" << typeid(*this).name()
|
||||||
|
<< ". This usually means promise construction did not bind the"
|
||||||
|
<< " (exception_ptr&, function<void()>, ...) constructor.";
|
||||||
|
throw std::runtime_error(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
this->setSelfSchedHandle(
|
||||||
|
std::coroutine_handle<promise_type>::from_promise(*this));
|
||||||
|
|
||||||
|
return NonViralPostingInvoker<PostingPromiseTemplate>(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using PostingInvoker<PostingPromiseTemplate<void>, void>::PostingInvoker;
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{ std::terminate(); }
|
||||||
|
|
||||||
|
void await_suspend(std::coroutine_handle<NonViralPostingInvoker<PostingPromiseTemplate>>) noexcept
|
||||||
|
{ std::terminate(); }
|
||||||
|
|
||||||
|
void await_resume() noexcept
|
||||||
|
{ std::terminate(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Viral awaitable: promise_type inherits PostingPromiseTemplate<T> (posting
|
||||||
|
* target chosen by the posting-promise alias, e.g. BodyPostingPromise<int>).
|
||||||
|
*
|
||||||
|
* The invoker must outlive the callee frame until results are read.
|
||||||
|
* ~Invoker destroys the callee frame (not await_resume).
|
||||||
|
*/
|
||||||
|
template <template <typename> class PostingPromiseTemplate, typename T>
|
||||||
|
struct ViralPostingInvoker
|
||||||
|
: public PostingInvoker<PostingPromiseTemplate<T>, T>
|
||||||
|
{
|
||||||
|
struct promise_type
|
||||||
|
: public PostingPromiseTemplate<T>
|
||||||
|
{
|
||||||
|
using PostingPromiseTemplate<T>::PostingPromiseTemplate;
|
||||||
|
|
||||||
|
ViralPostingInvoker<PostingPromiseTemplate, T> get_return_object() noexcept
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Returning ViralPostingInvoker.\n";
|
||||||
|
#endif
|
||||||
|
this->setSelfSchedHandle(
|
||||||
|
std::coroutine_handle<promise_type>::from_promise(*this));
|
||||||
|
|
||||||
|
return ViralPostingInvoker<PostingPromiseTemplate, T>(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using PostingInvoker<PostingPromiseTemplate<T>, T>::PostingInvoker;
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Returning false.\n";
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename CallerPromise>
|
||||||
|
bool await_suspend(std::coroutine_handle<CallerPromise> callerSchedHandle) noexcept
|
||||||
|
{
|
||||||
|
static_assert(
|
||||||
|
std::is_base_of_v<PromiseChainLink, CallerPromise>,
|
||||||
|
"ViralPostingInvoker caller promise must derive from PromiseChainLink");
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Setting callerSchedHandle.\n";
|
||||||
|
#endif
|
||||||
|
const bool suspendCaller = this->setCallerSchedHandle(callerSchedHandle);
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " CallerFlowExecutor returned suspend=" << suspendCaller << ".\n";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* If the callee was ready to post-back, then we don't need to
|
||||||
|
* suspend the caller -- so return either false or
|
||||||
|
* a symmetric transfer handle to the `callerSchedHandle` we were
|
||||||
|
* passed as an argument.
|
||||||
|
*
|
||||||
|
* If the callee is not ready to post-back, then we need to suspend
|
||||||
|
* the caller so that the caller can suspend until the callee posts
|
||||||
|
* the callerSchedHandle to the callerIoContext -- so return true
|
||||||
|
* or std::noop_coroutine().
|
||||||
|
*/
|
||||||
|
return suspendCaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
T await_resume()
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Resumed on caller thread, hopefully.\n";
|
||||||
|
#endif
|
||||||
|
return PostingInvoker<PostingPromiseTemplate<T>, T>::await_resume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Non-viral coroutine entry that must not be co_awaited: runs on the caller
|
||||||
|
* thread (initial_suspend is never) and invokes the completion lambda directly
|
||||||
|
* from final_suspend (no cross-thread posting).
|
||||||
|
*
|
||||||
|
* The invoker must outlive the callee frame: do not discard the return object
|
||||||
|
* from get_return_object(). ~Invoker destroys the callee frame.
|
||||||
|
*/
|
||||||
|
struct NonViralNonPostingInvoker
|
||||||
|
: public NonPostingInvoker<NonPostingPromise<void>, void>
|
||||||
|
{
|
||||||
|
struct promise_type
|
||||||
|
: public NonPostingPromise<void>
|
||||||
|
{
|
||||||
|
using NonPostingPromise<void>::NonPostingPromise;
|
||||||
|
|
||||||
|
NonViralNonPostingInvoker get_return_object()
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Returning NonViralNonPostingInvoker.\n";
|
||||||
|
#endif
|
||||||
|
if (!this->callerLambda)
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::this_thread::get_id()
|
||||||
|
<< ": Missing completion lambda: non-viral coroutines require a completion lambda."
|
||||||
|
<< " Promise type=" << typeid(*this).name()
|
||||||
|
<< ". This usually means promise construction did not bind the"
|
||||||
|
<< " (exception_ptr&, function<void()>, ...) constructor.";
|
||||||
|
throw std::runtime_error(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
this->setSelfSchedHandle(
|
||||||
|
std::coroutine_handle<promise_type>::from_promise(*this));
|
||||||
|
|
||||||
|
return NonViralNonPostingInvoker(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using NonPostingInvoker<NonPostingPromise<void>, void>::NonPostingInvoker;
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{ std::terminate(); }
|
||||||
|
|
||||||
|
void await_suspend(std::coroutine_handle<NonViralNonPostingInvoker>) noexcept
|
||||||
|
{ std::terminate(); }
|
||||||
|
|
||||||
|
void await_resume() noexcept
|
||||||
|
{ std::terminate(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Viral awaitable non-posting coroutine: runs eagerly on the caller thread
|
||||||
|
* (initial_suspend is never). Caller resume uses symmetric transfer when the
|
||||||
|
* caller has registered before callee completion; otherwise PostBackStatus
|
||||||
|
* fast-paths await_resume on co_await.
|
||||||
|
*/
|
||||||
|
template <typename T = void>
|
||||||
|
struct ViralNonPostingInvoker
|
||||||
|
: public NonPostingInvoker<NonPostingPromise<T>, T>
|
||||||
|
{
|
||||||
|
struct promise_type
|
||||||
|
: public NonPostingPromise<T>
|
||||||
|
{
|
||||||
|
using NonPostingPromise<T>::NonPostingPromise;
|
||||||
|
|
||||||
|
ViralNonPostingInvoker<T> get_return_object() noexcept
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " Returning ViralNonPostingInvoker.\n";
|
||||||
|
#endif
|
||||||
|
this->setSelfSchedHandle(
|
||||||
|
std::coroutine_handle<promise_type>::from_promise(*this));
|
||||||
|
|
||||||
|
return ViralNonPostingInvoker<T>(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using NonPostingInvoker<NonPostingPromise<T>, T>::NonPostingInvoker;
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{ return false; }
|
||||||
|
|
||||||
|
template <typename CallerPromise>
|
||||||
|
bool await_suspend(
|
||||||
|
std::coroutine_handle<CallerPromise> callerSchedHandle) noexcept
|
||||||
|
{
|
||||||
|
static_assert(
|
||||||
|
std::is_base_of_v<PromiseChainLink, CallerPromise>,
|
||||||
|
"ViralNonPostingInvoker caller promise must derive from "
|
||||||
|
"PromiseChainLink");
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " Setting callerSchedHandle.\n";
|
||||||
|
#endif
|
||||||
|
const bool suspendCaller =
|
||||||
|
this->setCallerSchedHandle(callerSchedHandle);
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " CallerFlowExecutor returned suspend=" << suspendCaller
|
||||||
|
<< ".\n";
|
||||||
|
#endif
|
||||||
|
return suspendCaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto await_resume()
|
||||||
|
{
|
||||||
|
return NonPostingInvoker<NonPostingPromise<T>, T>::await_resume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // INVOKERS_H
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
#ifndef NON_POSTING_PROMISE_H
|
||||||
|
#define NON_POSTING_PROMISE_H
|
||||||
|
|
||||||
|
#include <config.h>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <spinscale/spinLock.h>
|
||||||
|
#include <spinscale/co/coQutex.h>
|
||||||
|
#include <spinscale/co/nonViralCompletion.h>
|
||||||
|
#include <spinscale/co/promiseChainLink.h>
|
||||||
|
#include <spinscale/co/promiseReturnOps.h>
|
||||||
|
#include <spinscale/co/returnValues.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct NonPostingPromise
|
||||||
|
: public PromiseChainLink,
|
||||||
|
public PromiseReturnOps<NonPostingPromise<T>, T>
|
||||||
|
{
|
||||||
|
struct PostBackStatus
|
||||||
|
{
|
||||||
|
struct CalleeFlowExecutor;
|
||||||
|
struct CallerFlowExecutor;
|
||||||
|
friend struct CalleeFlowExecutor;
|
||||||
|
friend struct CallerFlowExecutor;
|
||||||
|
|
||||||
|
explicit PostBackStatus(NonPostingPromise &calleePromiseIn) noexcept
|
||||||
|
: calleePromise(calleePromiseIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void reset() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(lock);
|
||||||
|
callerHasSetCallerSchedHandle = false;
|
||||||
|
calleeIsReadyToPostBack = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FlowExecutor
|
||||||
|
{
|
||||||
|
explicit FlowExecutor(PostBackStatus &parentIn) noexcept
|
||||||
|
: parent(parentIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
PostBackStatus &parent;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CalleeFlowExecutor
|
||||||
|
: public FlowExecutor
|
||||||
|
{
|
||||||
|
explicit CalleeFlowExecutor(PostBackStatus &parentIn) noexcept
|
||||||
|
: FlowExecutor(parentIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool operator()() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(this->parent.lock);
|
||||||
|
this->parent.calleeIsReadyToPostBack = true;
|
||||||
|
if (this->parent.callerHasSetCallerSchedHandle) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CallerFlowExecutor
|
||||||
|
: public FlowExecutor
|
||||||
|
{
|
||||||
|
explicit CallerFlowExecutor(PostBackStatus &parentIn) noexcept
|
||||||
|
: FlowExecutor(parentIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool operator()() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(this->parent.lock);
|
||||||
|
this->parent.callerHasSetCallerSchedHandle = true;
|
||||||
|
if (this->parent.calleeIsReadyToPostBack) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CalleeFlowExecutor getCalleeFlowExecutor() noexcept
|
||||||
|
{
|
||||||
|
return CalleeFlowExecutor(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerFlowExecutor getCallerFlowExecutor() noexcept
|
||||||
|
{
|
||||||
|
return CallerFlowExecutor(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
NonPostingPromise &calleePromise;
|
||||||
|
|
||||||
|
private:
|
||||||
|
sscl::SpinLock lock;
|
||||||
|
bool callerHasSetCallerSchedHandle = false;
|
||||||
|
bool calleeIsReadyToPostBack = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Completion work must run from this awaiter's await_suspend, not
|
||||||
|
* synchronously inside promise.final_suspend() before it returns: the
|
||||||
|
* hidden coroutine segment index in the coroutine state is only advanced
|
||||||
|
* after final_suspend exits. See docs/prompts/post-to-and-back-in-invokables.md.
|
||||||
|
*/
|
||||||
|
struct FinalSuspendNonPostingInvoker
|
||||||
|
: public std::suspend_always
|
||||||
|
{
|
||||||
|
explicit FinalSuspendNonPostingInvoker(
|
||||||
|
NonPostingPromise &calleePromiseIn) noexcept
|
||||||
|
: calleePromise(calleePromiseIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::coroutine_handle<> await_suspend(
|
||||||
|
std::coroutine_handle<> const) noexcept
|
||||||
|
{
|
||||||
|
if (calleePromise.callerLambda)
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << "final_suspend" << ": "
|
||||||
|
<< std::this_thread::get_id()
|
||||||
|
<< " Non-viral non-posting: invoking callerLambda directly.\n";
|
||||||
|
#endif
|
||||||
|
auto callerLambda = std::move(calleePromise.callerLambda);
|
||||||
|
callerLambda();
|
||||||
|
return std::noop_coroutine();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << "final_suspend" << ": " << std::this_thread::get_id()
|
||||||
|
<< " Viral non-posting: running CalleeFlowExecutor.\n";
|
||||||
|
#endif
|
||||||
|
const bool symmetricTransferToCaller =
|
||||||
|
calleePromise.postBackStatus.getCalleeFlowExecutor()();
|
||||||
|
|
||||||
|
if (symmetricTransferToCaller && calleePromise.callerSchedHandle) {
|
||||||
|
return calleePromise.callerSchedHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::noop_coroutine();
|
||||||
|
}
|
||||||
|
|
||||||
|
NonPostingPromise &calleePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
NonPostingPromise() noexcept
|
||||||
|
: returnValues(),
|
||||||
|
postBackStatus(*this)
|
||||||
|
{}
|
||||||
|
|
||||||
|
template <typename... TailArgs>
|
||||||
|
NonPostingPromise(
|
||||||
|
std::exception_ptr &callerExceptionPtr,
|
||||||
|
std::function<void()> callerLambdaIn,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: returnValues(callerExceptionPtr),
|
||||||
|
callerLambda(std::move(callerLambdaIn)),
|
||||||
|
postBackStatus(*this)
|
||||||
|
{}
|
||||||
|
|
||||||
|
template <typename ObjectArg, typename... TailArgs>
|
||||||
|
requires (!std::same_as<std::remove_cvref_t<ObjectArg>, std::exception_ptr>)
|
||||||
|
NonPostingPromise(
|
||||||
|
ObjectArg &&,
|
||||||
|
std::exception_ptr &callerExceptionPtr,
|
||||||
|
std::function<void()> callerLambdaIn,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: NonPostingPromise(
|
||||||
|
callerExceptionPtr,
|
||||||
|
std::move(callerLambdaIn))
|
||||||
|
{}
|
||||||
|
|
||||||
|
~NonPostingPromise() noexcept
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " Destructing.\n";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::suspend_never initial_suspend() noexcept
|
||||||
|
{ return {}; }
|
||||||
|
|
||||||
|
auto final_suspend() noexcept
|
||||||
|
{ return FinalSuspendNonPostingInvoker(*this); }
|
||||||
|
|
||||||
|
void unhandled_exception() noexcept
|
||||||
|
{
|
||||||
|
returnValues.myExceptionPtr = std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAcquiredLock(CoQutex &coQutex) noexcept override
|
||||||
|
{
|
||||||
|
eraseFirstMatchingAcquiredLock(coQutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PromiseChainLink *callerPromiseChainLink() const noexcept override
|
||||||
|
{ return callerChainLink; }
|
||||||
|
|
||||||
|
PromiseChainLink *callerPromiseChainLink() noexcept override
|
||||||
|
{ return callerChainLink; }
|
||||||
|
|
||||||
|
void setSelfSchedHandle(std::coroutine_handle<> schedHandle) noexcept
|
||||||
|
{ selfSchedHandle = schedHandle; }
|
||||||
|
|
||||||
|
void setCallerPromiseChainLink(PromiseChainLink *chainLink) noexcept
|
||||||
|
{ callerChainLink = chainLink; }
|
||||||
|
|
||||||
|
ReturnValues<T> returnValues;
|
||||||
|
std::function<void()> callerLambda;
|
||||||
|
PostBackStatus postBackStatus;
|
||||||
|
std::coroutine_handle<> selfSchedHandle;
|
||||||
|
std::coroutine_handle<> callerSchedHandle;
|
||||||
|
PromiseChainLink *callerChainLink = nullptr;
|
||||||
|
|
||||||
|
template <typename, typename>
|
||||||
|
friend class Invoker;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // NON_POSTING_PROMISE_H
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef NON_VIRAL_COMPLETION_H
|
||||||
|
#define NON_VIRAL_COMPLETION_H
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
class NonViralCompletion
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NonViralCompletion(std::exception_ptr &exceptionPtr)
|
||||||
|
: exceptionPtr(exceptionPtr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool hasException() const noexcept
|
||||||
|
{
|
||||||
|
return exceptionPtr != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkAndRethrowException() const
|
||||||
|
{
|
||||||
|
if (exceptionPtr)
|
||||||
|
{
|
||||||
|
std::rethrow_exception(exceptionPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::exception_ptr releaseException() noexcept
|
||||||
|
{
|
||||||
|
return std::exchange(exceptionPtr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::exception_ptr &exceptionPtr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // NON_VIRAL_COMPLETION_H
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
#ifndef NON_VIRAL_TASK_NURSERY_H
|
||||||
|
#define NON_VIRAL_TASK_NURSERY_H
|
||||||
|
|
||||||
|
#include <boostAsioLinkageFix.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <list>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/cps/asynchronousBridge.h>
|
||||||
|
#include <spinscale/sharedResourceGroup.h>
|
||||||
|
#include <spinscale/spinLock.h>
|
||||||
|
#include <spinscale/syncCancelerForAsyncWork.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
struct MemberInvokerBase
|
||||||
|
{
|
||||||
|
virtual ~MemberInvokerBase() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class Invoker>
|
||||||
|
struct MemberInvoker : MemberInvokerBase
|
||||||
|
{
|
||||||
|
explicit MemberInvoker(Invoker &&invokerIn)
|
||||||
|
: invoker(std::move(invokerIn))
|
||||||
|
{}
|
||||||
|
|
||||||
|
Invoker invoker;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
/** Structured-concurrency owner for non-viral invokers at non-coroutine boundaries.
|
||||||
|
*
|
||||||
|
* The nursery owns invoker lifetimes until natural completion, wraps completion
|
||||||
|
* callbacks, tracks unsettled slots, fans out cooperative cancel via per-slot
|
||||||
|
* SyncCancelerForAsyncWork, and provides drain APIs.
|
||||||
|
*
|
||||||
|
* Each nursery member must be one complete, self-contained non-viral async
|
||||||
|
* flow. For an external HTTP request, that means one coroutine should shepherd
|
||||||
|
* the whole request from framework callback through all sscl component awaits
|
||||||
|
* to final response/commit/error handling. Do not use the nursery as a place
|
||||||
|
* to reserve a slot, perform partial setup elsewhere, and later return to
|
||||||
|
* fill the slot. Do not add each individual awaited operation as a separate
|
||||||
|
* nursery member. The external submitter should add the complete flow to the
|
||||||
|
* nursery and then return; the nursery owns that flow until the flow settles.
|
||||||
|
*
|
||||||
|
* syncAwaitAllSettlements() runs a nested io_context loop on the calling
|
||||||
|
* thread (AsynchronousBridge). Pass the calling thread's io_context —
|
||||||
|
* typically
|
||||||
|
* ComponentThread::getSelf()->getIoContext() — not another thread's
|
||||||
|
* io_context. If the caller pumps a different thread's queue while blocked,
|
||||||
|
* completions posted back to the caller's own io_context are never executed
|
||||||
|
* and the drain can deadlock even after cooperative cancel.
|
||||||
|
*/
|
||||||
|
class NonViralTaskNursery
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum class SlotLeaseStatus {
|
||||||
|
RESERVED, ACTIVE_UNSETTLED, RETIRED
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SlotSettlementStatus {
|
||||||
|
UNSETTLED, COMPLETED, EXCEPTION_THROWN
|
||||||
|
};
|
||||||
|
|
||||||
|
class Slot
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/** Opaque handle to a nursery slot. Valid while the slot remains in storage. */
|
||||||
|
class Handle
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool operator==(const Handle &_other) const noexcept
|
||||||
|
{ return &slot == &_other.slot; }
|
||||||
|
|
||||||
|
bool operator!=(const Handle &_other) const noexcept
|
||||||
|
{ return &slot != &_other.slot; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class NonViralTaskNursery;
|
||||||
|
friend class Lease;
|
||||||
|
|
||||||
|
explicit Handle(Slot &_slot) noexcept
|
||||||
|
: slot(_slot)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Slot &slot;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lease
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Lease(Lease &&_other) noexcept
|
||||||
|
: nursery(_other.nursery), slot(_other.slot),
|
||||||
|
slotCommittedSoLeaseShouldntDestroy(
|
||||||
|
std::exchange(
|
||||||
|
_other.slotCommittedSoLeaseShouldntDestroy,
|
||||||
|
true))
|
||||||
|
{}
|
||||||
|
|
||||||
|
Lease(const Lease &) = delete;
|
||||||
|
Lease &operator=(const Lease &) = delete;
|
||||||
|
Lease &operator=(Lease &&) = delete;
|
||||||
|
|
||||||
|
~Lease()
|
||||||
|
{
|
||||||
|
if (!slotCommittedSoLeaseShouldntDestroy) {
|
||||||
|
nursery.releaseUncommittedSlot(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::exception_ptr &getExceptionStorage()
|
||||||
|
{ return slot.exceptionPtr; }
|
||||||
|
|
||||||
|
std::function<void()> getCallerLambda()
|
||||||
|
{ return nursery.buildCallerLambdaForSlot(slot); }
|
||||||
|
|
||||||
|
sscl::SyncCancelerForAsyncWork &getSyncCanceler()
|
||||||
|
{ return slot.syncCanceler; }
|
||||||
|
|
||||||
|
void setOnSettledHook(
|
||||||
|
std::function<void(std::exception_ptr &exceptionPtr)> hook)
|
||||||
|
{
|
||||||
|
if (slot.leaseStatus != SlotLeaseStatus::RESERVED)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__)
|
||||||
|
+ ": must be called before fillSlot()");
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.onSettledHook = std::move(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Factory must create the invoker. Deferred construction is
|
||||||
|
* required because non-viral coroutines may complete synchronously
|
||||||
|
* during invoker construction, before fillSlot() can store the
|
||||||
|
* record.
|
||||||
|
*/
|
||||||
|
template <class InvokerFactory>
|
||||||
|
void fillSlot(InvokerFactory &&invokerFactory)
|
||||||
|
{
|
||||||
|
Slot &reservedSlot = slot;
|
||||||
|
|
||||||
|
if (reservedSlot.memberInvoker)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": slot already filled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reservedSlot.leaseStatus != SlotLeaseStatus::RESERVED)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": slot is not reserved");
|
||||||
|
}
|
||||||
|
|
||||||
|
reservedSlot.leaseStatus = SlotLeaseStatus::ACTIVE_UNSETTLED;
|
||||||
|
auto invoker = invokerFactory();
|
||||||
|
|
||||||
|
if (reservedSlot.leaseStatus == SlotLeaseStatus::RETIRED)
|
||||||
|
{
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Non-viral coroutines may complete synchronously inside
|
||||||
|
* the factory. Retirement already ran; the local invoker
|
||||||
|
* must be allowed to destroy the callee frame on return.
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reservedSlot.memberInvoker = std::make_unique<
|
||||||
|
detail::MemberInvoker<std::decay_t<decltype(invoker)>>>(
|
||||||
|
std::move(invoker));
|
||||||
|
}
|
||||||
|
|
||||||
|
void commit()
|
||||||
|
{
|
||||||
|
if (slotCommittedSoLeaseShouldntDestroy)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": lease already committed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Slot &reservedSlot = slot;
|
||||||
|
|
||||||
|
if (reservedSlot.leaseStatus == SlotLeaseStatus::RESERVED)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__)
|
||||||
|
+ ": fillSlot() required before commit()");
|
||||||
|
}
|
||||||
|
|
||||||
|
slotCommittedSoLeaseShouldntDestroy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle handle() const
|
||||||
|
{
|
||||||
|
return Handle(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class NonViralTaskNursery;
|
||||||
|
|
||||||
|
Lease(NonViralTaskNursery &_nursery, Slot &_slot) noexcept
|
||||||
|
: nursery(_nursery), slot(_slot)
|
||||||
|
{}
|
||||||
|
|
||||||
|
NonViralTaskNursery &nursery;
|
||||||
|
Slot &slot;
|
||||||
|
bool slotCommittedSoLeaseShouldntDestroy = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class NonViralTaskNursery;
|
||||||
|
friend class Lease;
|
||||||
|
|
||||||
|
SlotLeaseStatus leaseStatus = SlotLeaseStatus::RESERVED;
|
||||||
|
SlotSettlementStatus settlementStatus = SlotSettlementStatus::UNSETTLED;
|
||||||
|
std::exception_ptr exceptionPtr = nullptr;
|
||||||
|
sscl::SyncCancelerForAsyncWork syncCanceler;
|
||||||
|
std::unique_ptr<detail::MemberInvokerBase> memberInvoker;
|
||||||
|
std::function<void(std::exception_ptr &exceptionPtr)> onSettledHook;
|
||||||
|
};
|
||||||
|
|
||||||
|
void openAdmission()
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
s.rsrc.admissionOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Stopping a nursery: always closeAdmission() before
|
||||||
|
* requestCancelOnAll(). requestCancelOnAll() only marks currently
|
||||||
|
* ACTIVE_UNSETTLED slots; it does not refuse new leases. If cancel
|
||||||
|
* runs while admission is still open, a concurrent submitter can still
|
||||||
|
* getNewSlotLease() / launch() after cancel has fanned out, and that
|
||||||
|
* newly admitted work will not have been cancelled — it races past the
|
||||||
|
* stop wave and keeps the drain from reaching "all settled" until it
|
||||||
|
* finishes on its own (or a later cancel). Closing admission first
|
||||||
|
* seals the nursery so cancel applies to a fixed membership set, then
|
||||||
|
* drain with asyncAwaitAllSettlements() / syncAwaitAllSettlements()
|
||||||
|
* (those APIs also require admission already closed).
|
||||||
|
*
|
||||||
|
* Preferred stop stack for a daemon/service that enqueues request
|
||||||
|
* coroutines into the nursery: keep protocol "stop listening /
|
||||||
|
* disconnect / refuse new connections and requests" separate from
|
||||||
|
* protocol state destruction. Stop accepting at the protocol level
|
||||||
|
* first, then nursery closeAdmission(), then requestCancelOnAll(),
|
||||||
|
* then cancel any awaited I/O owned outside the cancelers, then drain,
|
||||||
|
* then destroy protocol state. That ordering stops new work at the
|
||||||
|
* source before admission is sealed.
|
||||||
|
*
|
||||||
|
* If the daemon/service cannot disconnect/stop listening separately
|
||||||
|
* from destruction, the spinscale-using embedding project must handle
|
||||||
|
* closed-admission failures when it tries to enqueue. For example,
|
||||||
|
* catch the "admission closed" throw around nursery.launch() (or in
|
||||||
|
* the factory that calls it) and emit a protocol-specific failure such
|
||||||
|
* as "connection failed" or "request timed out" instead of letting the
|
||||||
|
* exception escape the accept/request path unbounded.
|
||||||
|
*/
|
||||||
|
void closeAdmission()
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
s.rsrc.admissionOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool admissionIsOpen() const
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
return s.rsrc.admissionOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allSettled() const
|
||||||
|
{ return unsettledCount() == 0; }
|
||||||
|
|
||||||
|
std::size_t unsettledCount() const
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
return countUnsettledSlotsUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
Slot::Lease getNewSlotLease()
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|
||||||
|
if (!s.rsrc.admissionOpen)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": admission closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneRetiredSlotsUnlocked();
|
||||||
|
s.rsrc.slots.emplace_back();
|
||||||
|
Slot &slot = s.rsrc.slots.back();
|
||||||
|
return Slot::Lease(*this, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestCancelOnAll()
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|
||||||
|
for (auto &slot : s.rsrc.slots)
|
||||||
|
{
|
||||||
|
if (slot.leaseStatus != SlotLeaseStatus::ACTIVE_UNSETTLED) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.syncCanceler.requestStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void asyncAwaitAllSettlements(std::function<void()> callback)
|
||||||
|
{
|
||||||
|
std::function<void()> waiterToInvoke;
|
||||||
|
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|
||||||
|
if (s.rsrc.admissionOpen)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__)
|
||||||
|
+ ": admission must be closed before awaiting drain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countUnsettledSlotsUnlocked() == 0) {
|
||||||
|
waiterToInvoke = std::move(callback);
|
||||||
|
}
|
||||||
|
else if (s.rsrc.drainWaiter)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__)
|
||||||
|
+ ": drain waiter already registered");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.rsrc.drainWaiter = std::move(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waiterToInvoke) {
|
||||||
|
waiterToInvoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nested drain: blocks the calling thread in run_one() on @p ioContext until
|
||||||
|
* all slots retire. @p ioContext must be the caller thread's io_context.
|
||||||
|
*/
|
||||||
|
void syncAwaitAllSettlements(boost::asio::io_context &ioContext)
|
||||||
|
{
|
||||||
|
if (admissionIsOpen())
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__)
|
||||||
|
+ ": admission must be closed before awaiting drain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allSettled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ioContext.stopped())
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": provided io_context is stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Drain may run on the thread that processes a completion callback,
|
||||||
|
* not necessarily the thread blocked in waitForAsyncOperationComplete.
|
||||||
|
* Keep the bridge off the waiter thread's stack.
|
||||||
|
*/
|
||||||
|
auto bridge = std::make_shared<sscl::cps::AsynchronousBridge>(
|
||||||
|
ioContext);
|
||||||
|
asyncAwaitAllSettlements(
|
||||||
|
[bridge]()
|
||||||
|
{
|
||||||
|
bridge->setAsyncOperationComplete();
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge->waitForAsyncOperationCompleteOrIoContextStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class InvokerFactory>
|
||||||
|
Slot::Handle launch(
|
||||||
|
InvokerFactory &&factory,
|
||||||
|
std::function<void(std::exception_ptr &exceptionPtr)> onSettledHook =
|
||||||
|
nullptr)
|
||||||
|
{
|
||||||
|
auto lease = getNewSlotLease();
|
||||||
|
lease.getSyncCanceler().startAcceptingWork();
|
||||||
|
if (onSettledHook) {
|
||||||
|
lease.setOnSettledHook(std::move(onSettledHook));
|
||||||
|
}
|
||||||
|
lease.fillSlot(
|
||||||
|
[&factory, &lease]()
|
||||||
|
{
|
||||||
|
return std::forward<InvokerFactory>(factory)(lease);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
return lease.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class Slot::Lease;
|
||||||
|
|
||||||
|
struct State
|
||||||
|
{
|
||||||
|
bool admissionOpen = false;
|
||||||
|
std::list<Slot> slots;
|
||||||
|
std::function<void()> drainWaiter;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::size_t countUnsettledSlotsUnlocked() const
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
|
||||||
|
for (const auto &slot : s.rsrc.slots)
|
||||||
|
{
|
||||||
|
if (slot.leaseStatus == SlotLeaseStatus::ACTIVE_UNSETTLED) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseUncommittedSlot(Slot &slot)
|
||||||
|
{
|
||||||
|
std::function<void()> waiterToInvoke;
|
||||||
|
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|
||||||
|
if (slot.leaseStatus != SlotLeaseStatus::RESERVED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.leaseStatus = SlotLeaseStatus::RETIRED;
|
||||||
|
slot.memberInvoker.reset();
|
||||||
|
waiterToInvoke = takeDrainWaiterIfDrainedUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waiterToInvoke) { waiterToInvoke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
std::function<void()> buildCallerLambdaForSlot(Slot &slot)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[this, slot = std::ref(slot)]()
|
||||||
|
{
|
||||||
|
retireSlot(slot.get());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void retireSlot(Slot &slot)
|
||||||
|
{
|
||||||
|
std::function<void()> waiterToInvoke;
|
||||||
|
std::function<void(std::exception_ptr &exceptionPtr)> onSettledHook;
|
||||||
|
std::exception_ptr settledExceptionPtr;
|
||||||
|
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|
||||||
|
if (slot.leaseStatus != SlotLeaseStatus::ACTIVE_UNSETTLED)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": slot is not active and "
|
||||||
|
"unsettled");
|
||||||
|
}
|
||||||
|
if (slot.settlementStatus != SlotSettlementStatus::UNSETTLED) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": slot is not unsettled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifySlotIsManagedUnlocked(slot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settledExceptionPtr = slot.exceptionPtr;
|
||||||
|
if (settledExceptionPtr) {
|
||||||
|
slot.settlementStatus = SlotSettlementStatus::EXCEPTION_THROWN;
|
||||||
|
} else {
|
||||||
|
slot.settlementStatus = SlotSettlementStatus::COMPLETED;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSettledHook = std::move(slot.onSettledHook);
|
||||||
|
slot.leaseStatus = SlotLeaseStatus::RETIRED;
|
||||||
|
slot.memberInvoker.reset();
|
||||||
|
waiterToInvoke = takeDrainWaiterIfDrainedUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onSettledHook) {
|
||||||
|
onSettledHook(settledExceptionPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waiterToInvoke) { waiterToInvoke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Caller must hold s.lock. */
|
||||||
|
bool verifySlotIsManagedUnlocked(const Slot &slot) const
|
||||||
|
{
|
||||||
|
for (const auto &trackedSlot : s.rsrc.slots)
|
||||||
|
{
|
||||||
|
if (&trackedSlot == &slot) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Caller must hold s.lock. */
|
||||||
|
void pruneRetiredSlotsUnlocked()
|
||||||
|
{
|
||||||
|
for (auto it = s.rsrc.slots.begin(); it != s.rsrc.slots.end();)
|
||||||
|
{
|
||||||
|
if (it->leaseStatus == SlotLeaseStatus::RETIRED) {
|
||||||
|
it = s.rsrc.slots.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Caller must hold s.lock. */
|
||||||
|
std::function<void()> takeDrainWaiterIfDrainedUnlocked()
|
||||||
|
{
|
||||||
|
if (s.rsrc.admissionOpen) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countUnsettledSlotsUnlocked() != 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::exchange(s.rsrc.drainWaiter, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
mutable sscl::SharedResourceGroup<sscl::SpinLock, State> s;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // NON_VIRAL_TASK_NURSERY_H
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef POST_TARGET_H
|
||||||
|
#define POST_TARGET_H
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/** Opt-in dynamic post-TO target for TaggedPostingPromise coroutines.
|
||||||
|
* When omitted, initial_suspend posts to ThreadTag::io_context().
|
||||||
|
* Post-back still uses callerIoContext (getSelf() at co_await site).
|
||||||
|
*/
|
||||||
|
struct ExplicitPostTarget
|
||||||
|
{
|
||||||
|
boost::asio::io_context& ioContext;
|
||||||
|
|
||||||
|
explicit ExplicitPostTarget(boost::asio::io_context& ctx) noexcept
|
||||||
|
: ioContext(ctx)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
inline constexpr bool is_explicit_post_target_v =
|
||||||
|
std::same_as<std::remove_cvref_t<T>, ExplicitPostTarget>;
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // POST_TARGET_H
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
#ifndef POSTING_PROMISE_H
|
||||||
|
#define POSTING_PROMISE_H
|
||||||
|
|
||||||
|
#include <config.h>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
|
#include <typeinfo>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
#include <spinscale/co/coQutex.h>
|
||||||
|
#include <spinscale/co/nonViralCompletion.h>
|
||||||
|
#include <spinscale/co/postTarget.h>
|
||||||
|
#include <spinscale/co/promiseChainLink.h>
|
||||||
|
#include <spinscale/co/promiseReturnOps.h>
|
||||||
|
#include <spinscale/co/returnValues.h>
|
||||||
|
#include <spinscale/spinLock.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
template <typename PromiseType, typename T>
|
||||||
|
class Invoker;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct PostingPromise
|
||||||
|
: public PromiseChainLink
|
||||||
|
{
|
||||||
|
struct PostBackStatus
|
||||||
|
{
|
||||||
|
struct CalleeFlowExecutor;
|
||||||
|
struct CallerFlowExecutor;
|
||||||
|
friend struct CalleeFlowExecutor;
|
||||||
|
friend struct CallerFlowExecutor;
|
||||||
|
|
||||||
|
explicit PostBackStatus(PostingPromise &calleePromiseIn) noexcept
|
||||||
|
: calleePromise(calleePromiseIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void reset() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(lock);
|
||||||
|
callerHasSetCallerSchedHandle = false;
|
||||||
|
calleeIsReadyToPostBack = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FlowExecutor
|
||||||
|
{
|
||||||
|
explicit FlowExecutor(PostBackStatus &parentIn) noexcept
|
||||||
|
: parent(parentIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
PostBackStatus &parent;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CalleeFlowExecutor
|
||||||
|
: public FlowExecutor
|
||||||
|
{
|
||||||
|
explicit CalleeFlowExecutor(PostBackStatus &parentIn) noexcept
|
||||||
|
: FlowExecutor(parentIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void operator()() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(this->parent.lock);
|
||||||
|
this->parent.calleeIsReadyToPostBack = true;
|
||||||
|
if (this->parent.callerHasSetCallerSchedHandle)
|
||||||
|
{
|
||||||
|
boost::asio::post(
|
||||||
|
this->parent.calleePromise.callerIoContext,
|
||||||
|
this->parent.calleePromise.callerSchedHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CallerFlowExecutor
|
||||||
|
: public FlowExecutor
|
||||||
|
{
|
||||||
|
explicit CallerFlowExecutor(PostBackStatus &parentIn) noexcept
|
||||||
|
: FlowExecutor(parentIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool operator()() noexcept
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(this->parent.lock);
|
||||||
|
this->parent.callerHasSetCallerSchedHandle = true;
|
||||||
|
if (this->parent.calleeIsReadyToPostBack) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CalleeFlowExecutor getCalleeFlowExecutor() noexcept
|
||||||
|
{
|
||||||
|
return CalleeFlowExecutor(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerFlowExecutor getCallerFlowExecutor() noexcept
|
||||||
|
{
|
||||||
|
return CallerFlowExecutor(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
PostingPromise &calleePromise;
|
||||||
|
|
||||||
|
private:
|
||||||
|
sscl::SpinLock lock;
|
||||||
|
bool callerHasSetCallerSchedHandle = false;
|
||||||
|
bool calleeIsReadyToPostBack = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Post-to must run from this awaiter's await_suspend, not synchronously inside
|
||||||
|
* promise.initial_suspend() before it returns: the implementation's hidden coroutine
|
||||||
|
* state (async segment / suspend index used on the next resume()) is only updated
|
||||||
|
* after initial_suspend has finished returning its awaiter. Posting the handle too
|
||||||
|
* early lets the callee resume before that update and re-enter initial_suspend from
|
||||||
|
* the start, duplicating the post. See docs/prompts/post-to-and-back-in-invokables.md.
|
||||||
|
*/
|
||||||
|
struct InitialSuspendPostingInvoker
|
||||||
|
: public std::suspend_always
|
||||||
|
{
|
||||||
|
InitialSuspendPostingInvoker(
|
||||||
|
boost::asio::io_context &targetIoContextIn,
|
||||||
|
std::coroutine_handle<> targetSchedHandleIn) noexcept
|
||||||
|
: targetIoContext(targetIoContextIn),
|
||||||
|
targetSchedHandle(targetSchedHandleIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool await_suspend(std::coroutine_handle<> const) noexcept
|
||||||
|
{
|
||||||
|
boost::asio::post(targetIoContext, targetSchedHandle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::io_context &targetIoContext;
|
||||||
|
std::coroutine_handle<> targetSchedHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Post-back (non-viral completion post; viral CalleeFlowExecutor) must run from this
|
||||||
|
* awaiter's await_suspend, not synchronously inside promise.final_suspend() before it
|
||||||
|
* returns: the hidden coroutine segment index in the coroutine state is only advanced
|
||||||
|
* after final_suspend exits. Doing that work inside final_suspend's body risks the same
|
||||||
|
* kind of ordering bug as initial_suspend—resume observing the wrong segment. See
|
||||||
|
* docs/prompts/post-to-and-back-in-invokables.md.
|
||||||
|
*/
|
||||||
|
struct FinalSuspendPostingInvoker
|
||||||
|
: public std::suspend_always
|
||||||
|
{
|
||||||
|
explicit FinalSuspendPostingInvoker(PostingPromise &calleePromiseIn) noexcept
|
||||||
|
: calleePromise(calleePromiseIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool await_suspend(std::coroutine_handle<> const) noexcept
|
||||||
|
{
|
||||||
|
if (calleePromise.callerLambda)
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << "final_suspend" << ": " << std::this_thread::get_id()
|
||||||
|
<< " Non-viral: posting callerLambda completion to callerIoContext.\n";
|
||||||
|
#endif
|
||||||
|
auto callerLambda = std::move(calleePromise.callerLambda);
|
||||||
|
boost::asio::post(
|
||||||
|
calleePromise.callerIoContext,
|
||||||
|
[callerLambda = std::move(callerLambda)]() mutable
|
||||||
|
{
|
||||||
|
callerLambda();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << "final_suspend" << ": " << std::this_thread::get_id()
|
||||||
|
<< " Viral: running CalleeFlowExecutor.\n";
|
||||||
|
#endif
|
||||||
|
calleePromise.postBackStatus.getCalleeFlowExecutor()();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PostingPromise &calleePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
PostingPromise() noexcept
|
||||||
|
: returnValues(), postBackStatus(*this)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Non-viral entry: post-TO uses ThreadTag default (via TaggedPostingPromise). */
|
||||||
|
template <typename... TailArgs>
|
||||||
|
PostingPromise(
|
||||||
|
std::exception_ptr &_callerExceptionPtr,
|
||||||
|
std::function<void()> _callerLambda,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: returnValues(_callerExceptionPtr),
|
||||||
|
callerLambda(std::move(_callerLambda)),
|
||||||
|
postBackStatus(*this)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Non-viral entry with explicit post-TO target. */
|
||||||
|
template <typename... TailArgs>
|
||||||
|
PostingPromise(
|
||||||
|
ExplicitPostTarget _calleePostTarget,
|
||||||
|
std::exception_ptr &_callerExceptionPtr,
|
||||||
|
std::function<void()> _callerLambda,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: returnValues(_callerExceptionPtr),
|
||||||
|
callerLambda(std::move(_callerLambda)),
|
||||||
|
calleePostTarget(std::move(_calleePostTarget)),
|
||||||
|
postBackStatus(*this)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Viral / free-function entry with explicit post-TO target. */
|
||||||
|
template <typename... TailArgs>
|
||||||
|
PostingPromise(
|
||||||
|
ExplicitPostTarget _calleePostTarget,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: returnValues(),
|
||||||
|
calleePostTarget(std::move(_calleePostTarget)),
|
||||||
|
postBackStatus(*this)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Viral / free-function entry: post-TO uses ThreadTag default. */
|
||||||
|
template <typename FirstArg, typename... TailArgs>
|
||||||
|
requires (!is_explicit_post_target_v<std::remove_cvref_t<FirstArg>>)
|
||||||
|
PostingPromise(FirstArg &&, TailArgs &&...) noexcept
|
||||||
|
: PostingPromise()
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Member non-viral: peel implicit object parameter. */
|
||||||
|
template <typename ObjectArg, typename... TailArgs>
|
||||||
|
requires (
|
||||||
|
!std::same_as<std::remove_cvref_t<ObjectArg>, std::exception_ptr>
|
||||||
|
&& !is_explicit_post_target_v<std::remove_cvref_t<ObjectArg>>)
|
||||||
|
PostingPromise(
|
||||||
|
ObjectArg &&,
|
||||||
|
std::exception_ptr &_callerExceptionPtr,
|
||||||
|
std::function<void()> _callerLambda,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: PostingPromise(
|
||||||
|
_callerExceptionPtr,
|
||||||
|
std::move(_callerLambda))
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Member non-viral with explicit post-TO target. */
|
||||||
|
template <typename ObjectArg, typename... TailArgs>
|
||||||
|
requires (
|
||||||
|
!std::same_as<std::remove_cvref_t<ObjectArg>, std::exception_ptr>
|
||||||
|
&& !is_explicit_post_target_v<std::remove_cvref_t<ObjectArg>>)
|
||||||
|
PostingPromise(
|
||||||
|
ObjectArg &&,
|
||||||
|
ExplicitPostTarget _calleePostTarget,
|
||||||
|
std::exception_ptr &_callerExceptionPtr,
|
||||||
|
std::function<void()> _callerLambda,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: PostingPromise(
|
||||||
|
std::move(_calleePostTarget),
|
||||||
|
_callerExceptionPtr,
|
||||||
|
std::move(_callerLambda))
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Member viral with explicit post-TO target. */
|
||||||
|
template <typename ObjectArg, typename... TailArgs>
|
||||||
|
requires (
|
||||||
|
!std::same_as<std::remove_cvref_t<ObjectArg>, std::exception_ptr>
|
||||||
|
&& !is_explicit_post_target_v<std::remove_cvref_t<ObjectArg>>)
|
||||||
|
PostingPromise(
|
||||||
|
ObjectArg &&,
|
||||||
|
ExplicitPostTarget _calleePostTarget,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: PostingPromise(std::move(_calleePostTarget))
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Member viral: peel implicit object parameter. */
|
||||||
|
template <typename ObjectArg, typename FirstArg, typename... TailArgs>
|
||||||
|
requires (
|
||||||
|
!std::same_as<std::remove_cvref_t<ObjectArg>, std::exception_ptr>
|
||||||
|
&& !is_explicit_post_target_v<std::remove_cvref_t<ObjectArg>>
|
||||||
|
&& !is_explicit_post_target_v<std::remove_cvref_t<FirstArg>>)
|
||||||
|
PostingPromise(
|
||||||
|
ObjectArg &&,
|
||||||
|
FirstArg &&,
|
||||||
|
TailArgs &&...) noexcept
|
||||||
|
: PostingPromise()
|
||||||
|
{}
|
||||||
|
|
||||||
|
~PostingPromise() noexcept
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Destructing.\n";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void unhandled_exception() noexcept
|
||||||
|
{
|
||||||
|
returnValues.myExceptionPtr = std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAcquiredLock(CoQutex &coQutex) noexcept override
|
||||||
|
{
|
||||||
|
eraseFirstMatchingAcquiredLock(coQutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PromiseChainLink *callerPromiseChainLink() const noexcept override
|
||||||
|
{ return callerChainLink; }
|
||||||
|
|
||||||
|
PromiseChainLink *callerPromiseChainLink() noexcept override
|
||||||
|
{ return callerChainLink; }
|
||||||
|
|
||||||
|
/** Non-viral: post completion lambda to callerIoContext from this thread.
|
||||||
|
* Viral: run CalleeFlowExecutor (handshake flags); caller may post caller resume
|
||||||
|
* later via CallerFlowExecutor. See docs/caller-posts-to-own-io-context.md.
|
||||||
|
* Work runs in FinalSuspendPostingInvoker::await_suspend after the suspend point
|
||||||
|
* advances (see docs/prompts/post-to-and-back-in-invokables.md).
|
||||||
|
*/
|
||||||
|
auto final_suspend() noexcept
|
||||||
|
{
|
||||||
|
return FinalSuspendPostingInvoker(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnValues<T> returnValues;
|
||||||
|
std::function<void()> callerLambda;
|
||||||
|
boost::asio::io_context &callerIoContext =
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext();
|
||||||
|
std::optional<ExplicitPostTarget> calleePostTarget;
|
||||||
|
std::coroutine_handle<> selfSchedHandle;
|
||||||
|
std::coroutine_handle<void> callerSchedHandle;
|
||||||
|
PromiseChainLink *callerChainLink = nullptr;
|
||||||
|
PostBackStatus postBackStatus;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void setSelfSchedHandle(std::coroutine_handle<> schedHandle) noexcept
|
||||||
|
{
|
||||||
|
selfSchedHandle = schedHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCallerPromiseChainLink(PromiseChainLink *chainLink) noexcept
|
||||||
|
{
|
||||||
|
callerChainLink = chainLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename, typename>
|
||||||
|
friend class Invoker;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T, typename ThreadTag>
|
||||||
|
struct TaggedPostingPromise
|
||||||
|
: public PostingPromise<T>,
|
||||||
|
public PromiseReturnOps<TaggedPostingPromise<T, ThreadTag>, T>
|
||||||
|
{
|
||||||
|
using PostingPromise<T>::PostingPromise;
|
||||||
|
|
||||||
|
auto initial_suspend() noexcept
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " About to post selfSchedHandle to " << typeid(ThreadTag).name() << ".\n";
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id() << " Returning InitialSuspendPostingInvoker.\n";
|
||||||
|
#endif
|
||||||
|
boost::asio::io_context &postToIoContext =
|
||||||
|
this->calleePostTarget
|
||||||
|
? this->calleePostTarget->ioContext
|
||||||
|
: ThreadTag::io_context();
|
||||||
|
|
||||||
|
return typename PostingPromise<T>::InitialSuspendPostingInvoker(
|
||||||
|
postToIoContext,
|
||||||
|
this->selfSchedHandle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // POSTING_PROMISE_H
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#ifndef PROMISE_CHAIN_LINK_H
|
||||||
|
#define PROMISE_CHAIN_LINK_H
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
class CoQutex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-template base for coroutine promises participating in a logical
|
||||||
|
* promise chain (analogous to libspinscale AsynchronousContinuationChainLink).
|
||||||
|
* A future deadlock detector can walk callerPromiseChainLink() without
|
||||||
|
* knowing concrete promise_type.
|
||||||
|
*/
|
||||||
|
class PromiseChainLink
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~PromiseChainLink() = default;
|
||||||
|
|
||||||
|
/** Reserved for deadlock detection: link toward the caller / outer coroutine. */
|
||||||
|
virtual const PromiseChainLink *callerPromiseChainLink() const noexcept
|
||||||
|
{ return nullptr; }
|
||||||
|
virtual PromiseChainLink *callerPromiseChainLink() noexcept
|
||||||
|
{ return nullptr; }
|
||||||
|
|
||||||
|
void addAcquiredLock(CoQutex &coQutex) noexcept
|
||||||
|
{ acquiredLocks.emplace_back(std::ref(coQutex)); }
|
||||||
|
|
||||||
|
bool holdsAcquiredLock(const CoQutex &coQutex) const noexcept
|
||||||
|
{ return findMatchingAcquiredLock(coQutex) != acquiredLocks.end(); }
|
||||||
|
|
||||||
|
virtual void removeAcquiredLock(CoQutex &coQutex) noexcept = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
using AcquiredLockList = std::list<std::reference_wrapper<CoQutex>>;
|
||||||
|
|
||||||
|
AcquiredLockList::iterator findMatchingAcquiredLock(CoQutex &coQutex) noexcept
|
||||||
|
{
|
||||||
|
for (auto it = acquiredLocks.begin(); it != acquiredLocks.end(); ++it) {
|
||||||
|
if (&it->get() == &coQutex) {
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acquiredLocks.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
AcquiredLockList::const_iterator findMatchingAcquiredLock(const CoQutex &coQutex) const noexcept
|
||||||
|
{
|
||||||
|
for (auto it = acquiredLocks.begin(); it != acquiredLocks.end(); ++it) {
|
||||||
|
if (&it->get() == &coQutex) {
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acquiredLocks.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void eraseFirstMatchingAcquiredLock(CoQutex &coQutex) noexcept
|
||||||
|
{
|
||||||
|
auto match = findMatchingAcquiredLock(coQutex);
|
||||||
|
if (match != acquiredLocks.end()) {
|
||||||
|
acquiredLocks.erase(match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AcquiredLockList acquiredLocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // PROMISE_CHAIN_LINK_H
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#ifndef PROMISE_CHAIN_WALKER_H
|
||||||
|
#define PROMISE_CHAIN_WALKER_H
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <spinscale/co/promiseChainLink.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upper bound on caller-chain links visited after the root (guards cycles / bugs).
|
||||||
|
*
|
||||||
|
* Design posture (cf. docs/3rdParty/smo/libspinscale — continuation tracing vs
|
||||||
|
* interpretation): this header performs trace-only walks along
|
||||||
|
* PromiseChainLink::callerPromiseChainLink(); deadlock interpretation stays at
|
||||||
|
* call sites / later policy.
|
||||||
|
*/
|
||||||
|
inline constexpr std::size_t kMaxCallerPromiseChainTraversalSteps = 4096;
|
||||||
|
|
||||||
|
inline const PromiseChainLink *nextOnCallerPromiseChain(
|
||||||
|
const PromiseChainLink &link) noexcept
|
||||||
|
{
|
||||||
|
return link.callerPromiseChainLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool callerChainHopUnderStepLimit(
|
||||||
|
std::size_t hopIndex) noexcept
|
||||||
|
{
|
||||||
|
return hopIndex < kMaxCallerPromiseChainTraversalSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Visitor>
|
||||||
|
void walkCallerPromiseChainFrom(
|
||||||
|
const PromiseChainLink &root, Visitor &&visitor)
|
||||||
|
{
|
||||||
|
visitor(root);
|
||||||
|
const PromiseChainLink *next = nextOnCallerPromiseChain(root);
|
||||||
|
for (std::size_t hopIndex = 0;
|
||||||
|
next != nullptr && callerChainHopUnderStepLimit(hopIndex);
|
||||||
|
++hopIndex)
|
||||||
|
{
|
||||||
|
visitor(*next);
|
||||||
|
next = nextOnCallerPromiseChain(*next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // PROMISE_CHAIN_WALKER_H
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef PROMISE_RETURN_OPS_H
|
||||||
|
#define PROMISE_RETURN_OPS_H
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <spinscale/co/returnValues.h>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/** `return_value` / `return_void` only. ThreadTag is not a template parameter here:
|
||||||
|
* for tagged promises, PromiseType is `TaggedPostingPromise<T, ThreadTag>`.
|
||||||
|
*/
|
||||||
|
template <typename PromiseType, typename T, bool IsVoid = std::is_void_v<T>>
|
||||||
|
struct PromiseReturnOps;
|
||||||
|
|
||||||
|
template <typename PromiseType, typename T>
|
||||||
|
struct PromiseReturnOps<PromiseType, T, false>
|
||||||
|
{
|
||||||
|
void return_value(T returnValue) noexcept
|
||||||
|
{
|
||||||
|
static_cast<PromiseType *>(this)->returnValues.myReturnValue =
|
||||||
|
std::move(returnValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename PromiseType, typename T>
|
||||||
|
struct PromiseReturnOps<PromiseType, T, true>
|
||||||
|
{
|
||||||
|
void return_void() noexcept
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // PROMISE_RETURN_OPS_H
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#ifndef RETURN_VALUES_H
|
||||||
|
#define RETURN_VALUES_H
|
||||||
|
|
||||||
|
#include <config.h>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
template <typename T, bool IsVoid = std::is_void_v<T>>
|
||||||
|
struct ReturnValueStorage;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct ReturnValueStorage<T, false>
|
||||||
|
{
|
||||||
|
T myReturnValue{};
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct ReturnValueStorage<T, true>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct ReturnValues
|
||||||
|
: public ReturnValueStorage<T>
|
||||||
|
{
|
||||||
|
ReturnValues() noexcept
|
||||||
|
: myExceptionPtr(myMemberExceptionPtr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
explicit ReturnValues(std::exception_ptr &callerExceptionPtr) noexcept
|
||||||
|
: myExceptionPtr(callerExceptionPtr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
~ReturnValues() noexcept
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_LIBSSCL_DEBUG_CO
|
||||||
|
std::cout << __func__ << ": " << std::this_thread::get_id()
|
||||||
|
<< " Destructing.\n";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* The exception_ptr ref here can either point to the exception_ptr
|
||||||
|
* a non-viral coroutine supplied to us as its storage space for
|
||||||
|
* where we should store any exception that is thrown;
|
||||||
|
*
|
||||||
|
* Or it could point to the member exception_ptr in this very class,
|
||||||
|
* which is used for viral coroutines that can bubble their exception
|
||||||
|
* up and automatically via the language runtime.
|
||||||
|
*/
|
||||||
|
std::exception_ptr &myExceptionPtr;
|
||||||
|
std::exception_ptr myMemberExceptionPtr = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // RETURN_VALUES_H
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#ifndef SYNC_AWAIT_NON_VIRAL_CORO_H
|
||||||
|
#define SYNC_AWAIT_NON_VIRAL_CORO_H
|
||||||
|
|
||||||
|
#include <boostAsioLinkageFix.h>
|
||||||
|
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
#include <spinscale/co/nonViralTaskNursery.h>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace sscl::co {
|
||||||
|
|
||||||
|
/** Launch a non-viral coroutine on the current ComponentThread io_context and
|
||||||
|
* block until it settles, rethrowing any stored exception.
|
||||||
|
*/
|
||||||
|
template<typename InvokerFactory>
|
||||||
|
void syncAwaitNonViralCoro(InvokerFactory&& _invokerFactory)
|
||||||
|
{
|
||||||
|
std::exception_ptr slotException;
|
||||||
|
|
||||||
|
NonViralTaskNursery nursery;
|
||||||
|
nursery.openAdmission();
|
||||||
|
nursery.launch(
|
||||||
|
[&_invokerFactory](NonViralTaskNursery::Slot::Lease& lease)
|
||||||
|
{
|
||||||
|
return _invokerFactory(lease);
|
||||||
|
},
|
||||||
|
[&slotException](std::exception_ptr& exceptionPtr)
|
||||||
|
{
|
||||||
|
slotException = exceptionPtr;
|
||||||
|
});
|
||||||
|
nursery.closeAdmission();
|
||||||
|
nursery.syncAwaitAllSettlements(
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext());
|
||||||
|
|
||||||
|
if (slotException) {
|
||||||
|
std::rethrow_exception(slotException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::co
|
||||||
|
|
||||||
|
#endif // SYNC_AWAIT_NON_VIRAL_CORO_H
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
#include <config.h>
|
#include <config.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <spinscale/callback.h>
|
#include <spinscale/cps/callback.h>
|
||||||
#include <spinscale/puppetApplication.h>
|
#include <spinscale/puppetApplication.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
#ifndef COMPONENT_THREAD_H
|
#ifndef COMPONENT_THREAD_H
|
||||||
#define COMPONENT_THREAD_H
|
#define COMPONENT_THREAD_H
|
||||||
|
|
||||||
#include <boostAsioLinkageFix.h>
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <boost/asio/io_service.hpp>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -13,9 +11,12 @@
|
|||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <spinscale/callback.h>
|
#include <coroutine>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
#include <spinscale/cps/callback.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl {
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ class ComponentThread
|
|||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
ComponentThread(ThreadId _id, std::string _name)
|
ComponentThread(ThreadId _id, std::string _name)
|
||||||
: id(_id), name(std::move(_name)), work(io_service)
|
: id(_id), name(std::move(_name)),
|
||||||
|
work(boost::asio::make_work_guard(io_context)), keepLooping(true)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -42,7 +44,7 @@ public:
|
|||||||
|
|
||||||
void cleanup(void);
|
void cleanup(void);
|
||||||
|
|
||||||
boost::asio::io_service& getIoService(void) { return io_service; }
|
boost::asio::io_context& getIoContext(void) { return io_context; }
|
||||||
|
|
||||||
static const std::shared_ptr<ComponentThread> getSelf(void);
|
static const std::shared_ptr<ComponentThread> getSelf(void);
|
||||||
static bool tlsInitialized(void);
|
static bool tlsInitialized(void);
|
||||||
@@ -64,8 +66,9 @@ public:
|
|||||||
public:
|
public:
|
||||||
ThreadId id;
|
ThreadId id;
|
||||||
std::string name;
|
std::string name;
|
||||||
boost::asio::io_service io_service;
|
boost::asio::io_context io_context;
|
||||||
boost::asio::io_service::work work;
|
boost::asio::executor_work_guard<
|
||||||
|
boost::asio::io_context::executor_type> work;
|
||||||
std::atomic<bool> keepLooping;
|
std::atomic<bool> keepLooping;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +154,7 @@ public:
|
|||||||
preJoltHookFn preJoltFn)
|
preJoltHookFn preJoltFn)
|
||||||
: ComponentThread(_id, std::move(name)),
|
: ComponentThread(_id, std::move(name)),
|
||||||
pinnedCpuId(-1),
|
pinnedCpuId(-1),
|
||||||
pause_work(pause_io_service),
|
pause_work(boost::asio::make_work_guard(pause_io_context)),
|
||||||
entryFnArguments(*this, component, preJoltFn),
|
entryFnArguments(*this, component, preJoltFn),
|
||||||
thread(std::move(entryPoint), std::cref(entryFnArguments))
|
thread(std::move(entryPoint), std::cref(entryFnArguments))
|
||||||
{}
|
{}
|
||||||
@@ -160,12 +163,115 @@ public:
|
|||||||
|
|
||||||
void initializeTls(void);
|
void initializeTls(void);
|
||||||
|
|
||||||
// Thread management methods
|
|
||||||
typedef std::function<void()> threadLifetimeMgmtOpCbFn;
|
typedef std::function<void()> threadLifetimeMgmtOpCbFn;
|
||||||
void startThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback);
|
|
||||||
void exitThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback);
|
struct ViralThreadLifetimeMgmtInvoker
|
||||||
void pauseThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback);
|
{
|
||||||
void resumeThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback);
|
struct AsyncState
|
||||||
|
{
|
||||||
|
std::atomic<bool> settled{false};
|
||||||
|
std::coroutine_handle<> callerSchedHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
ViralThreadLifetimeMgmtInvoker(
|
||||||
|
ThreadOp _threadOp,
|
||||||
|
PuppetThread &_parentThread,
|
||||||
|
const std::shared_ptr<PuppetThread> &_selfPtr = nullptr)
|
||||||
|
: threadOp(_threadOp),
|
||||||
|
asyncState(std::make_shared<AsyncState>()),
|
||||||
|
parentThread(_parentThread),
|
||||||
|
selfPtr(_selfPtr),
|
||||||
|
lifetimeMgmtCallback{
|
||||||
|
nullptr,
|
||||||
|
[asyncState = asyncState]()
|
||||||
|
{
|
||||||
|
asyncState->settled.store(true, std::memory_order_release);
|
||||||
|
|
||||||
|
std::coroutine_handle<> handle =
|
||||||
|
asyncState->callerSchedHandle;
|
||||||
|
|
||||||
|
if (!handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Post resume to the puppeteer queue: direct resume() from
|
||||||
|
* within an asio completion handler can destroy adapter
|
||||||
|
* coroutine state while the handler is still unwinding.
|
||||||
|
*/
|
||||||
|
boost::asio::post(
|
||||||
|
ComponentThread::getPptr()->getIoContext(),
|
||||||
|
[handle]() { handle.resume(); });
|
||||||
|
}}
|
||||||
|
{
|
||||||
|
if (threadOp == ThreadOp::JOLT && selfPtr == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(std::string(__func__)
|
||||||
|
+ ": JOLT request must be made with a valid selfPtr");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (threadOp)
|
||||||
|
{
|
||||||
|
case ThreadOp::START:
|
||||||
|
parentThread.startThreadReq(lifetimeMgmtCallback);
|
||||||
|
break;
|
||||||
|
case ThreadOp::PAUSE:
|
||||||
|
parentThread.pauseThreadReq(lifetimeMgmtCallback);
|
||||||
|
break;
|
||||||
|
case ThreadOp::RESUME:
|
||||||
|
parentThread.resumeThreadReq(lifetimeMgmtCallback);
|
||||||
|
break;
|
||||||
|
case ThreadOp::EXIT:
|
||||||
|
parentThread.exitThreadReq(lifetimeMgmtCallback);
|
||||||
|
break;
|
||||||
|
case ThreadOp::JOLT:
|
||||||
|
parentThread.joltThreadReq(selfPtr, lifetimeMgmtCallback);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw std::runtime_error(std::string(__func__)
|
||||||
|
+ ": Invalid thread operation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{
|
||||||
|
return asyncState->settled.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool await_suspend(
|
||||||
|
std::coroutine_handle<> _callerSchedHandle) noexcept
|
||||||
|
{
|
||||||
|
if (asyncState->settled.load(std::memory_order_acquire)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
asyncState->callerSchedHandle = _callerSchedHandle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void await_resume() noexcept {}
|
||||||
|
|
||||||
|
ThreadOp threadOp;
|
||||||
|
std::shared_ptr<AsyncState> asyncState;
|
||||||
|
PuppetThread &parentThread;
|
||||||
|
const std::shared_ptr<PuppetThread> selfPtr;
|
||||||
|
cps::Callback<threadLifetimeMgmtOpCbFn> lifetimeMgmtCallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Thread lifetime management request invokers
|
||||||
|
ViralThreadLifetimeMgmtInvoker startThreadAReq()
|
||||||
|
{ return ViralThreadLifetimeMgmtInvoker(ThreadOp::START, *this); }
|
||||||
|
ViralThreadLifetimeMgmtInvoker pauseThreadAReq()
|
||||||
|
{ return ViralThreadLifetimeMgmtInvoker(ThreadOp::PAUSE, *this); }
|
||||||
|
ViralThreadLifetimeMgmtInvoker resumeThreadAReq()
|
||||||
|
{ return ViralThreadLifetimeMgmtInvoker(ThreadOp::RESUME, *this); }
|
||||||
|
ViralThreadLifetimeMgmtInvoker exitThreadAReq()
|
||||||
|
{ return ViralThreadLifetimeMgmtInvoker(ThreadOp::EXIT, *this); }
|
||||||
|
|
||||||
|
void startThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback);
|
||||||
|
void exitThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback);
|
||||||
|
void pauseThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback);
|
||||||
|
void resumeThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JOLTs this thread to begin processing after global initialization.
|
* JOLTs this thread to begin processing after global initialization.
|
||||||
@@ -178,17 +284,22 @@ public:
|
|||||||
* isn't set up yet, so shared_from_this() can't be used)
|
* isn't set up yet, so shared_from_this() can't be used)
|
||||||
* @param callback Callback to invoke when JOLT completes
|
* @param callback Callback to invoke when JOLT completes
|
||||||
*/
|
*/
|
||||||
|
ViralThreadLifetimeMgmtInvoker joltThreadAReq(
|
||||||
|
const std::shared_ptr<PuppetThread> &selfPtr)
|
||||||
|
{ return ViralThreadLifetimeMgmtInvoker(ThreadOp::JOLT, *this, selfPtr); }
|
||||||
|
|
||||||
void joltThreadReq(
|
void joltThreadReq(
|
||||||
const std::shared_ptr<PuppetThread>& selfPtr,
|
const std::shared_ptr<PuppetThread>& selfPtr,
|
||||||
Callback<threadLifetimeMgmtOpCbFn> callback);
|
cps::Callback<threadLifetimeMgmtOpCbFn> callback);
|
||||||
|
|
||||||
// CPU management methods
|
// CPU management methods
|
||||||
void pinToCpu(int cpuId);
|
void pinToCpu(int cpuId);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
int pinnedCpuId;
|
int pinnedCpuId;
|
||||||
boost::asio::io_service pause_io_service;
|
boost::asio::io_context pause_io_context;
|
||||||
boost::asio::io_service::work pause_work;
|
boost::asio::executor_work_guard<
|
||||||
|
boost::asio::io_context::executor_type> pause_work;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
EntryFnArguments entryFnArguments;
|
EntryFnArguments entryFnArguments;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#ifndef ASYNCHRONOUS_BRIDGE_H
|
||||||
|
#define ASYNCHRONOUS_BRIDGE_H
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
namespace sscl::cps {
|
||||||
|
|
||||||
|
class AsynchronousBridge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AsynchronousBridge(boost::asio::io_context &io_context)
|
||||||
|
: isAsyncOperationComplete(false), io_context(io_context)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void setAsyncOperationComplete(void)
|
||||||
|
{
|
||||||
|
/** EXPLANATION:
|
||||||
|
* This empty post()ed message is necessary to ensure that the thread
|
||||||
|
* that's waiting on the io_context is signaled to wake up and check
|
||||||
|
* the io_context's queue.
|
||||||
|
*/
|
||||||
|
isAsyncOperationComplete.store(true);
|
||||||
|
boost::asio::post(io_context, []{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blocks the calling thread in run_one() on the bridge's io_context.
|
||||||
|
* Used by syncAwaitAllSettlements(); that io_context must be the caller
|
||||||
|
* thread's own queue so posted completions on the caller are not starved.
|
||||||
|
*/
|
||||||
|
void waitForAsyncOperationCompleteOrIoContextStopped(void)
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
io_context.run_one();
|
||||||
|
if (isAsyncOperationComplete.load() || io_context.stopped())
|
||||||
|
{ break; }
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* In the puppeteer and mind thread loops we call checkException()
|
||||||
|
* after run() returns, but we don't have to do that here because
|
||||||
|
* setException() calls stop().
|
||||||
|
*
|
||||||
|
* So if an exception is set on our thread, we'll break out of this
|
||||||
|
* loop due to the check for stopped() above, and that'll take us
|
||||||
|
* back out to the main loop, where we'll catch the exception.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool exitedBecauseIoContextStopped(void) const
|
||||||
|
{ return io_context.stopped(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic<bool> isAsyncOperationComplete;
|
||||||
|
boost::asio::io_context &io_context;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::cps
|
||||||
|
|
||||||
|
#endif // ASYNCHRONOUS_BRIDGE_H
|
||||||
+9
-9
@@ -5,12 +5,12 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <spinscale/componentThread.h>
|
#include <spinscale/componentThread.h>
|
||||||
#include <spinscale/callback.h>
|
#include <spinscale/cps/callback.h>
|
||||||
#include <spinscale/callableTracer.h>
|
#include <spinscale/cps/callableTracer.h>
|
||||||
#include <spinscale/asynchronousContinuationChainLink.h>
|
#include <spinscale/cps/asynchronousContinuationChainLink.h>
|
||||||
|
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AsynchronousContinuation - Template base class for async sequence management
|
* AsynchronousContinuation - Template base class for async sequence management
|
||||||
@@ -90,7 +90,7 @@ public:
|
|||||||
* LockedNonPostedAsynchronousContinuation because the only way to implement
|
* LockedNonPostedAsynchronousContinuation because the only way to implement
|
||||||
* non-posted locking would be via busy-spinning or sleeplocks. This would
|
* non-posted locking would be via busy-spinning or sleeplocks. This would
|
||||||
* eliminate the throughput advantage from our Qspinning mechanism, which
|
* eliminate the throughput advantage from our Qspinning mechanism, which
|
||||||
* relies on re-posting to the io_service queue when locks are unavailable.
|
* relies on re-posting to the io_context queue when locks are unavailable.
|
||||||
*/
|
*/
|
||||||
template <class OriginalCbFnT>
|
template <class OriginalCbFnT>
|
||||||
class NonPostedAsynchronousContinuation
|
class NonPostedAsynchronousContinuation
|
||||||
@@ -129,7 +129,7 @@ class PostedAsynchronousContinuation
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PostedAsynchronousContinuation(
|
PostedAsynchronousContinuation(
|
||||||
const std::shared_ptr<ComponentThread> &caller,
|
const std::shared_ptr<sscl::ComponentThread> &caller,
|
||||||
Callback<OriginalCbFnT> originalCbFn)
|
Callback<OriginalCbFnT> originalCbFn)
|
||||||
: AsynchronousContinuation<OriginalCbFnT>(originalCbFn),
|
: AsynchronousContinuation<OriginalCbFnT>(originalCbFn),
|
||||||
caller(caller)
|
caller(caller)
|
||||||
@@ -141,7 +141,7 @@ public:
|
|||||||
if (AsynchronousContinuation<OriginalCbFnT>::originalCallback
|
if (AsynchronousContinuation<OriginalCbFnT>::originalCallback
|
||||||
.callbackFn)
|
.callbackFn)
|
||||||
{
|
{
|
||||||
caller->getIoService().post(
|
boost::asio::post(caller->getIoContext(),
|
||||||
STC(std::bind(
|
STC(std::bind(
|
||||||
AsynchronousContinuation<OriginalCbFnT>::originalCallback
|
AsynchronousContinuation<OriginalCbFnT>::originalCallback
|
||||||
.callbackFn,
|
.callbackFn,
|
||||||
@@ -150,9 +150,9 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
std::shared_ptr<ComponentThread> caller;
|
std::shared_ptr<sscl::ComponentThread> caller;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // ASYNCHRONOUS_CONTINUATION_H
|
#endif // ASYNCHRONOUS_CONTINUATION_H
|
||||||
+13
-2
@@ -1,9 +1,13 @@
|
|||||||
#ifndef ASYNCHRONOUS_CONTINUATION_CHAIN_LINK_H
|
#ifndef ASYNCHRONOUS_CONTINUATION_CHAIN_LINK_H
|
||||||
#define ASYNCHRONOUS_CONTINUATION_CHAIN_LINK_H
|
#define ASYNCHRONOUS_CONTINUATION_CHAIN_LINK_H
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
|
class LockSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Base class for all asynchronous continuation chain links
|
* @brief Base class for all asynchronous continuation chain links
|
||||||
@@ -25,8 +29,15 @@ public:
|
|||||||
|
|
||||||
virtual std::shared_ptr<AsynchronousContinuationChainLink>
|
virtual std::shared_ptr<AsynchronousContinuationChainLink>
|
||||||
getCallersContinuationShPtr() const = 0;
|
getCallersContinuationShPtr() const = 0;
|
||||||
|
|
||||||
|
virtual std::optional<std::reference_wrapper<const LockSet>>
|
||||||
|
getLockSet() const
|
||||||
|
{ return std::nullopt; }
|
||||||
|
|
||||||
|
virtual std::optional<std::reference_wrapper<LockSet>> getLockSet()
|
||||||
|
{ return std::nullopt; }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // ASYNCHRONOUS_CONTINUATION_CHAIN_LINK_H
|
#endif // ASYNCHRONOUS_CONTINUATION_CHAIN_LINK_H
|
||||||
@@ -8,14 +8,14 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <spinscale/componentThread.h>
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief CallableTracer - Wraps callables with metadata for debugging
|
* @brief CallableTracer - Wraps callables with metadata for debugging
|
||||||
*
|
*
|
||||||
* This class wraps any callable object with metadata (caller function name,
|
* This class wraps any callable object with metadata (caller function name,
|
||||||
* line number, and return addresses) to help debug cases where callables
|
* line number, and return addresses) to help debug cases where callables
|
||||||
* posted to boost::asio::io_service have gone out of scope. The metadata
|
* posted to boost::asio::io_context have gone out of scope. The metadata
|
||||||
* can be accessed from the callable's address when debugging.
|
* can be accessed from the callable's address when debugging.
|
||||||
*/
|
*/
|
||||||
class CallableTracer
|
class CallableTracer
|
||||||
@@ -49,8 +49,8 @@ public:
|
|||||||
if (optTraceCallables)
|
if (optTraceCallables)
|
||||||
{
|
{
|
||||||
std::cout << "" << __func__ << ": On thread "
|
std::cout << "" << __func__ << ": On thread "
|
||||||
<< (ComponentThread::tlsInitialized()
|
<< (sscl::ComponentThread::tlsInitialized()
|
||||||
? ComponentThread::getSelf()->name : "<TLS un-init'ed>")
|
? sscl::ComponentThread::getSelf()->name : "<TLS un-init'ed>")
|
||||||
<< ": Calling callable posted by:\n"
|
<< ": Calling callable posted by:\n"
|
||||||
<< "\t" << callerFuncName << "\n\tat line " << (int)callerLine
|
<< "\t" << callerFuncName << "\n\tat line " << (int)callerLine
|
||||||
<< " return addr 0: " << returnAddr0
|
<< " return addr 0: " << returnAddr0
|
||||||
@@ -79,7 +79,7 @@ private:
|
|||||||
std::function<void()> callable;
|
std::function<void()> callable;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief STC - SMO Traceable Callable macro
|
* @brief STC - SMO Traceable Callable macro
|
||||||
@@ -100,7 +100,7 @@ private:
|
|||||||
* - Fallback: nullptr for return addresses
|
* - Fallback: nullptr for return addresses
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* thread->getIoService().post(
|
* boost::asio::post(thread->getIoContext(),
|
||||||
* STC(std::bind(&SomeClass::method, this, arg1, arg2)));
|
* STC(std::bind(&SomeClass::method, this, arg1, arg2)));
|
||||||
*/
|
*/
|
||||||
#ifdef CONFIG_DEBUG_TRACE_CALLABLES
|
#ifdef CONFIG_DEBUG_TRACE_CALLABLES
|
||||||
@@ -109,7 +109,7 @@ private:
|
|||||||
// e.g., "void smo::SomeClass::method(int, int)"
|
// e.g., "void smo::SomeClass::method(int, int)"
|
||||||
// __builtin_return_address(0) = direct caller
|
// __builtin_return_address(0) = direct caller
|
||||||
// __builtin_return_address(1) = caller before that
|
// __builtin_return_address(1) = caller before that
|
||||||
#define STC(arg) sscl::CallableTracer( \
|
#define STC(arg) sscl::cps::CallableTracer( \
|
||||||
__PRETTY_FUNCTION__, \
|
__PRETTY_FUNCTION__, \
|
||||||
__LINE__, \
|
__LINE__, \
|
||||||
__builtin_return_address(0), \
|
__builtin_return_address(0), \
|
||||||
@@ -120,7 +120,7 @@ private:
|
|||||||
// e.g., "void __cdecl smo::SomeClass::method(int, int)"
|
// e.g., "void __cdecl smo::SomeClass::method(int, int)"
|
||||||
// _ReturnAddress() = direct caller (only one level available)
|
// _ReturnAddress() = direct caller (only one level available)
|
||||||
#include <intrin.h>
|
#include <intrin.h>
|
||||||
#define STC(arg) sscl::CallableTracer( \
|
#define STC(arg) sscl::cps::CallableTracer( \
|
||||||
__FUNCSIG__, \
|
__FUNCSIG__, \
|
||||||
__LINE__, \
|
__LINE__, \
|
||||||
_ReturnAddress(), \
|
_ReturnAddress(), \
|
||||||
@@ -129,7 +129,7 @@ private:
|
|||||||
#else
|
#else
|
||||||
// Fallback to standard __func__ (unqualified name only)
|
// Fallback to standard __func__ (unqualified name only)
|
||||||
// No return address support
|
// No return address support
|
||||||
#define STC(arg) sscl::CallableTracer( \
|
#define STC(arg) sscl::cps::CallableTracer( \
|
||||||
__func__, \
|
__func__, \
|
||||||
__LINE__, \
|
__LINE__, \
|
||||||
nullptr, \
|
nullptr, \
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declaration
|
||||||
class AsynchronousContinuationChainLink;
|
class AsynchronousContinuationChainLink;
|
||||||
@@ -26,6 +26,6 @@ public:
|
|||||||
CbFnT callbackFn;
|
CbFnT callbackFn;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // SPINSCALE_CALLBACK_H
|
#endif // SPINSCALE_CALLBACK_H
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class AsynchronousContinuationChainLink;
|
class AsynchronousContinuationChainLink;
|
||||||
@@ -80,6 +80,6 @@ private:
|
|||||||
AdjacencyList adjacencyList;
|
AdjacencyList adjacencyList;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // DEPENDENCY_GRAPH_H
|
#endif // DEPENDENCY_GRAPH_H
|
||||||
@@ -6,20 +6,31 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <spinscale/qutex.h>
|
#include <spinscale/cps/qutex.h>
|
||||||
#include <spinscale/lockerAndInvokerBase.h>
|
#include <spinscale/cps/lockerAndInvokerBase.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
// Forward declarations
|
|
||||||
template <class OriginalCbFnT>
|
|
||||||
class SerializedAsynchronousContinuation;
|
|
||||||
class Qutex;
|
class Qutex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief LockSet - Manages a collection of locks for acquisition/release
|
* @brief LockSet - Manages a collection of locks for acquisition/release
|
||||||
|
*
|
||||||
|
* LockSet exists only because the CPS re-enqueuing model had no way to acquire
|
||||||
|
* locks in a fine-grained way. A LockerAndInvoker could re-post only the entire
|
||||||
|
* continuation, and only before that continuation began executing; there was no
|
||||||
|
* mechanism to re-enqueue individual segments within a continuation. The
|
||||||
|
* practical consequence was that all required Qutexes had to be acquired at
|
||||||
|
* once up front, before the continuation body could run at all.
|
||||||
|
*
|
||||||
|
* releaseQutexEarly() was a partial workaround for finer-grained control, but
|
||||||
|
* it only helped on the release side and did not solve the fundamental problem
|
||||||
|
* of acquiring locks one-at-a-time mid-sequence.
|
||||||
|
*
|
||||||
|
* co::CoQutex supersedes this abstraction: coroutines can co_await individual
|
||||||
|
* locks at the points where they are actually needed, which is the finer control
|
||||||
|
* LockSet and releaseQutexEarly() were aiming for with limited success.
|
||||||
*/
|
*/
|
||||||
template <class OriginalCbFnT>
|
|
||||||
class LockSet
|
class LockSet
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -44,15 +55,10 @@ public:
|
|||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Constructor
|
* @brief Constructor
|
||||||
* @param parentContinuation Reference to the parent
|
|
||||||
* SerializedAsynchronousContinuation
|
|
||||||
* @param qutexes Vector of Qutex references that must be acquired
|
* @param qutexes Vector of Qutex references that must be acquired
|
||||||
*/
|
*/
|
||||||
LockSet(
|
explicit LockSet(std::vector<std::reference_wrapper<Qutex>> qutexes = {})
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT> &parentContinuation,
|
: allLocksAcquired(false), registeredInQutexQueues(false)
|
||||||
std::vector<std::reference_wrapper<Qutex>> qutexes = {})
|
|
||||||
: parentContinuation(parentContinuation), allLocksAcquired(false),
|
|
||||||
registeredInQutexQueues(false)
|
|
||||||
{
|
{
|
||||||
/* Convert Qutex references to LockUsageDesc (iterators will be filled
|
/* Convert Qutex references to LockUsageDesc (iterators will be filled
|
||||||
* in during registration)
|
* in during registration)
|
||||||
@@ -77,7 +83,7 @@ public:
|
|||||||
* time it will leave the qutexQ is when the program terminates.
|
* time it will leave the qutexQ is when the program terminates.
|
||||||
*
|
*
|
||||||
* I'm not sure we'll actually cancal all in-flight async sequences --
|
* I'm not sure we'll actually cancal all in-flight async sequences --
|
||||||
* and especially not all those that aren't even in any io_service queues.
|
* and especially not all those that aren't even in any io_context queues.
|
||||||
* To whatever extent these objects get cleaned up, they'll probably be
|
* To whatever extent these objects get cleaned up, they'll probably be
|
||||||
* cleaned up in the qutexQ's std::list destructor -- and that won't
|
* cleaned up in the qutexQ's std::list destructor -- and that won't
|
||||||
* execute any fancy cleanup logic. It'll just clear() out the list.
|
* execute any fancy cleanup logic. It'll just clear() out the list.
|
||||||
@@ -131,7 +137,6 @@ public:
|
|||||||
bool tryAcquireOrBackOff(
|
bool tryAcquireOrBackOff(
|
||||||
LockerAndInvokerBase &lockvoker,
|
LockerAndInvokerBase &lockvoker,
|
||||||
std::optional<std::reference_wrapper<Qutex>> &firstFailedQutex
|
std::optional<std::reference_wrapper<Qutex>> &firstFailedQutex
|
||||||
= std::nullopt
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!registeredInQutexQueues)
|
if (!registeredInQutexQueues)
|
||||||
@@ -207,15 +212,52 @@ public:
|
|||||||
allLocksAcquired = false;
|
allLocksAcquired = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LockUsageDesc &getLockUsageDesc(const Qutex &criterionLock) const
|
std::optional<std::reference_wrapper<LockUsageDesc>>
|
||||||
|
findLockUsageDesc(const Qutex &criterionLock)
|
||||||
{
|
{
|
||||||
for (auto& lockUsageDesc : locks)
|
for (auto& lockUsageDesc : locks)
|
||||||
{
|
{
|
||||||
if (&lockUsageDesc.qutex.get() == &criterionLock) {
|
if (&lockUsageDesc.qutex.get() == &criterionLock) {
|
||||||
return lockUsageDesc;
|
return std::ref(lockUsageDesc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::reference_wrapper<const LockUsageDesc>>
|
||||||
|
findLockUsageDesc(const Qutex &criterionLock) const
|
||||||
|
{
|
||||||
|
for (const auto& lockUsageDesc : locks)
|
||||||
|
{
|
||||||
|
if (&lockUsageDesc.qutex.get() == &criterionLock) {
|
||||||
|
return std::cref(lockUsageDesc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
LockUsageDesc &getLockUsageDesc(const Qutex &criterionLock)
|
||||||
|
{
|
||||||
|
auto lockUsageDesc = findLockUsageDesc(criterionLock);
|
||||||
|
if (lockUsageDesc.has_value()) {
|
||||||
|
return lockUsageDesc->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never happen if the LockSet is properly constructed
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) +
|
||||||
|
": Qutex not found in this LockSet");
|
||||||
|
}
|
||||||
|
|
||||||
|
const LockUsageDesc &getLockUsageDesc(const Qutex &criterionLock) const
|
||||||
|
{
|
||||||
|
auto lockUsageDesc = findLockUsageDesc(criterionLock);
|
||||||
|
if (lockUsageDesc.has_value()) {
|
||||||
|
return lockUsageDesc->get();
|
||||||
|
}
|
||||||
|
|
||||||
// Should never happen if the LockSet is properly constructed
|
// Should never happen if the LockSet is properly constructed
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
std::string(__func__) +
|
std::string(__func__) +
|
||||||
@@ -235,8 +277,7 @@ public:
|
|||||||
": LockSet::releaseQutexEarly() called but allLocksAcquired is false");
|
": LockSet::releaseQutexEarly() called but allLocksAcquired is false");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& lockUsageDesc = const_cast<LockUsageDesc&>(
|
auto& lockUsageDesc = getLockUsageDesc(qutex);
|
||||||
getLockUsageDesc(qutex));
|
|
||||||
|
|
||||||
if (!lockUsageDesc.hasBeenReleased)
|
if (!lockUsageDesc.hasBeenReleased)
|
||||||
{
|
{
|
||||||
@@ -251,10 +292,9 @@ public:
|
|||||||
std::vector<LockUsageDesc> locks;
|
std::vector<LockUsageDesc> locks;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT> &parentContinuation;
|
|
||||||
bool allLocksAcquired, registeredInQutexQueues;
|
bool allLocksAcquired, registeredInQutexQueues;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // LOCK_SET_H
|
#endif // LOCK_SET_H
|
||||||
+5
-5
@@ -4,7 +4,7 @@
|
|||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declaration
|
||||||
class Qutex;
|
class Qutex;
|
||||||
@@ -39,7 +39,7 @@ public:
|
|||||||
virtual List::iterator getLockvokerIteratorForQutex(Qutex& qutex) const = 0;
|
virtual List::iterator getLockvokerIteratorForQutex(Qutex& qutex) const = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Awaken this lockvoker by posting it to its io_service
|
* @brief Awaken this lockvoker by posting it to its io_context
|
||||||
* @param forceAwaken If true, post even if already awake
|
* @param forceAwaken If true, post even if already awake
|
||||||
*/
|
*/
|
||||||
virtual void awaken(bool forceAwaken = false) = 0;
|
virtual void awaken(bool forceAwaken = false) = 0;
|
||||||
@@ -55,12 +55,12 @@ public:
|
|||||||
*
|
*
|
||||||
* Compare by the address of the continuation objects. Why?
|
* Compare by the address of the continuation objects. Why?
|
||||||
* Because there's no guarantee that the lockvoker object that was
|
* Because there's no guarantee that the lockvoker object that was
|
||||||
* passed in by the io_service invocation is the same object as that
|
* passed in by the io_context invocation is the same object as that
|
||||||
* which is in the qutexQs. Especially because we make_shared() a
|
* which is in the qutexQs. Especially because we make_shared() a
|
||||||
* copy when registerInQutexQueues()ing.
|
* copy when registerInQutexQueues()ing.
|
||||||
*
|
*
|
||||||
* Generally when we "wake" a lockvoker by enqueuing it, boost's
|
* Generally when we "wake" a lockvoker by enqueuing it, boost's
|
||||||
* io_service::post will copy the lockvoker object.
|
* io_context::post will copy the lockvoker object.
|
||||||
*/
|
*/
|
||||||
bool operator==(const LockerAndInvokerBase &other) const
|
bool operator==(const LockerAndInvokerBase &other) const
|
||||||
{
|
{
|
||||||
@@ -82,6 +82,6 @@ protected:
|
|||||||
const void* serializedContinuationVaddr;
|
const void* serializedContinuationVaddr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // LOCKER_AND_INVOKER_BASE_H
|
#endif // LOCKER_AND_INVOKER_BASE_H
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <spinscale/spinLock.h>
|
#include <spinscale/spinLock.h>
|
||||||
#include <spinscale/lockerAndInvokerBase.h>
|
#include <spinscale/cps/lockerAndInvokerBase.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Qutex - Queue-based mutex for asynchronous lock management
|
* @brief Qutex - Queue-based mutex for asynchronous lock management
|
||||||
@@ -97,11 +97,11 @@ public:
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::shared_ptr<LockerAndInvokerBase> currOwner;
|
std::shared_ptr<LockerAndInvokerBase> currOwner;
|
||||||
#endif
|
#endif
|
||||||
SpinLock lock;
|
sscl::SpinLock lock;
|
||||||
LockerAndInvokerBase::List queue;
|
LockerAndInvokerBase::List queue;
|
||||||
bool isOwned;
|
bool isOwned;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // QUTEX_H
|
#endif // QUTEX_H
|
||||||
+4
-4
@@ -5,10 +5,10 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <forward_list>
|
#include <forward_list>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "spinLock.h"
|
#include <spinscale/spinLock.h>
|
||||||
|
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class Qutex;
|
class Qutex;
|
||||||
@@ -155,10 +155,10 @@ private:
|
|||||||
* Therefore, it's best to use a SpinLock on the history class to avoid
|
* Therefore, it's best to use a SpinLock on the history class to avoid
|
||||||
* these coupling issues.
|
* these coupling issues.
|
||||||
*/
|
*/
|
||||||
SpinLock acquisitionHistoryLock;
|
sscl::SpinLock acquisitionHistoryLock;
|
||||||
AcquisitionHistoryMap acquisitionHistory;
|
AcquisitionHistoryMap acquisitionHistory;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // QUTEX_ACQUISITION_HISTORY_TRACKER_H
|
#endif // QUTEX_ACQUISITION_HISTORY_TRACKER_H
|
||||||
+74
-59
@@ -8,13 +8,13 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <spinscale/componentThread.h>
|
#include <spinscale/componentThread.h>
|
||||||
#include <spinscale/lockSet.h>
|
#include <spinscale/cps/lockSet.h>
|
||||||
#include <spinscale/asynchronousContinuation.h>
|
#include <spinscale/cps/asynchronousContinuation.h>
|
||||||
#include <spinscale/lockerAndInvokerBase.h>
|
#include <spinscale/cps/lockerAndInvokerBase.h>
|
||||||
#include <spinscale/callback.h>
|
#include <spinscale/cps/callback.h>
|
||||||
#include <spinscale/qutexAcquisitionHistoryTracker.h>
|
#include <spinscale/cps/qutexAcquisitionHistoryTracker.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
template <class OriginalCbFnT>
|
template <class OriginalCbFnT>
|
||||||
class SerializedAsynchronousContinuation
|
class SerializedAsynchronousContinuation
|
||||||
@@ -22,13 +22,20 @@ class SerializedAsynchronousContinuation
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SerializedAsynchronousContinuation(
|
SerializedAsynchronousContinuation(
|
||||||
const std::shared_ptr<ComponentThread> &caller,
|
const std::shared_ptr<sscl::ComponentThread> &caller,
|
||||||
Callback<OriginalCbFnT> originalCbFn,
|
Callback<OriginalCbFnT> originalCbFn,
|
||||||
std::vector<std::reference_wrapper<Qutex>> requiredLocks)
|
std::vector<std::reference_wrapper<Qutex>> requiredLocks)
|
||||||
: PostedAsynchronousContinuation<OriginalCbFnT>(caller, originalCbFn),
|
: PostedAsynchronousContinuation<OriginalCbFnT>(caller, originalCbFn),
|
||||||
requiredLocks(*this, std::move(requiredLocks))
|
requiredLocks(std::move(requiredLocks))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
std::optional<std::reference_wrapper<const LockSet>>
|
||||||
|
getLockSet() const override
|
||||||
|
{ return std::cref(requiredLocks); }
|
||||||
|
|
||||||
|
std::optional<std::reference_wrapper<LockSet>> getLockSet() override
|
||||||
|
{ return std::ref(requiredLocks); }
|
||||||
|
|
||||||
template<typename... Args>
|
template<typename... Args>
|
||||||
void callOriginalCb(Args&&... args)
|
void callOriginalCb(Args&&... args)
|
||||||
{
|
{
|
||||||
@@ -40,7 +47,8 @@ public:
|
|||||||
// Return list of all qutexes in predecessors' LockSets; excludes self.
|
// Return list of all qutexes in predecessors' LockSets; excludes self.
|
||||||
[[nodiscard]]
|
[[nodiscard]]
|
||||||
std::unique_ptr<std::forward_list<std::reference_wrapper<Qutex>>>
|
std::unique_ptr<std::forward_list<std::reference_wrapper<Qutex>>>
|
||||||
getAcquiredQutexHistory() const;
|
getAcquiredQutexHistory(
|
||||||
|
bool includeLocksWhichHaveBeenReleased = false) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Release a specific qutex early
|
* @brief Release a specific qutex early
|
||||||
@@ -50,14 +58,14 @@ public:
|
|||||||
{ requiredLocks.releaseQutexEarly(qutex); }
|
{ requiredLocks.releaseQutexEarly(qutex); }
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LockSet<OriginalCbFnT> requiredLocks;
|
LockSet requiredLocks;
|
||||||
std::atomic<bool> isAwakeOrBeingAwakened{false};
|
std::atomic<bool> isAwakeOrBeingAwakened{false};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief LockerAndInvoker - Template class for lockvoking mechanism
|
* @brief LockerAndInvoker - Template class for lockvoking mechanism
|
||||||
*
|
*
|
||||||
* This class wraps a std::bind result and provides locking functionality.
|
* This class wraps a std::bind result and provides locking functionality.
|
||||||
* When locks cannot be acquired, the object re-posts itself to the io_service
|
* When locks cannot be acquired, the object re-posts itself to the io_context
|
||||||
* queue, implementing the "spinqueueing" pattern.
|
* queue, implementing the "spinqueueing" pattern.
|
||||||
*/
|
*/
|
||||||
template <class InvocationTargetT>
|
template <class InvocationTargetT>
|
||||||
@@ -66,16 +74,16 @@ public:
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Constructor that immediately posts to io_service
|
* @brief Constructor that immediately posts to io_context
|
||||||
* @param serializedContinuation Reference to the serialized continuation
|
* @param serializedContinuation Reference to the serialized continuation
|
||||||
* containing LockSet and target io_service
|
* containing LockSet and target io_context
|
||||||
* @param target The ComponentThread whose io_service to post to
|
* @param target The ComponentThread whose io_context to post to
|
||||||
* @param invocationTarget The std::bind result to invoke when locks are acquired
|
* @param invocationTarget The std::bind result to invoke when locks are acquired
|
||||||
*/
|
*/
|
||||||
LockerAndInvoker(
|
LockerAndInvoker(
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT>
|
SerializedAsynchronousContinuation<OriginalCbFnT>
|
||||||
&serializedContinuation,
|
&serializedContinuation,
|
||||||
const std::shared_ptr<ComponentThread>& target,
|
const std::shared_ptr<sscl::ComponentThread>& target,
|
||||||
InvocationTargetT invocationTarget)
|
InvocationTargetT invocationTarget)
|
||||||
: LockerAndInvokerBase(&serializedContinuation),
|
: LockerAndInvokerBase(&serializedContinuation),
|
||||||
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
|
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
|
||||||
@@ -119,7 +127,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Awaken this lockvoker by posting it to its io_service
|
* @brief Awaken this lockvoker by posting it to its io_context
|
||||||
* @param forceAwaken If true, post even if already awake
|
* @param forceAwaken If true, post even if already awake
|
||||||
*/
|
*/
|
||||||
void awaken(bool forceAwaken = false) override
|
void awaken(bool forceAwaken = false) override
|
||||||
@@ -130,7 +138,7 @@ public:
|
|||||||
if (prevVal == true && !forceAwaken)
|
if (prevVal == true && !forceAwaken)
|
||||||
{ return; }
|
{ return; }
|
||||||
|
|
||||||
target->getIoService().post(*this);
|
boost::asio::post(target->getIoContext(), *this);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t getLockSetSize() const override
|
size_t getLockSetSize() const override
|
||||||
@@ -153,14 +161,14 @@ public:
|
|||||||
* the AsyncContinuation sh_ptr (which the Lockvoker contains within
|
* the AsyncContinuation sh_ptr (which the Lockvoker contains within
|
||||||
* itself) alive without wasting too much memory.
|
* itself) alive without wasting too much memory.
|
||||||
*
|
*
|
||||||
* This way the io_service objects can remove the lockvoker from
|
* This way the io_context objects can remove the lockvoker from
|
||||||
* their queues and there'll be a copy of the lockvoker in each
|
* their queues and there'll be a copy of the lockvoker in each
|
||||||
* Qutex's queue.
|
* Qutex's queue.
|
||||||
*
|
*
|
||||||
* For non-serialized, posted continuations, they won't be removed
|
* For non-serialized, posted continuations, they won't be removed
|
||||||
* from the io_service queue until they're executed, so there's no
|
* from the io_context queue until they're executed, so there's no
|
||||||
* need to create copies of them. Lockvokers are removed from their
|
* need to create copies of them. Lockvokers are removed from their
|
||||||
* io_service, potentially without being executed if they fail to
|
* io_context, potentially without being executed if they fail to
|
||||||
* acquire all locks.
|
* acquire all locks.
|
||||||
*/
|
*/
|
||||||
void registerInLockSet()
|
void registerInLockSet()
|
||||||
@@ -177,7 +185,7 @@ public:
|
|||||||
*
|
*
|
||||||
* Sets isAwake=true before calling awaken with forceAwaken to ensure
|
* Sets isAwake=true before calling awaken with forceAwaken to ensure
|
||||||
* that none of the locks we just registered with awaken()s a duplicate
|
* that none of the locks we just registered with awaken()s a duplicate
|
||||||
* copy of this lockvoker on the io_service.
|
* copy of this lockvoker on the io_context.
|
||||||
*/
|
*/
|
||||||
void firstWake()
|
void firstWake()
|
||||||
{
|
{
|
||||||
@@ -205,8 +213,17 @@ public:
|
|||||||
{ return isDeadlockLikely(); }
|
{ return isDeadlockLikely(); }
|
||||||
|
|
||||||
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
|
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
|
||||||
struct obsolete {
|
friend struct obsolete;
|
||||||
bool traceContinuationHistoryForGridlockOn(Qutex &firstFailedQutex);
|
|
||||||
|
struct obsolete
|
||||||
|
{
|
||||||
|
explicit obsolete(LockerAndInvoker &_parent) : parent(_parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool traceContinuationHistoryForGridlockOn(
|
||||||
|
Qutex &firstFailedQutex);
|
||||||
|
|
||||||
|
LockerAndInvoker &parent;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool traceContinuationHistoryForDeadlockOn(Qutex &firstFailedQutex);
|
bool traceContinuationHistoryForDeadlockOn(Qutex &firstFailedQutex);
|
||||||
@@ -258,7 +275,7 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT>
|
SerializedAsynchronousContinuation<OriginalCbFnT>
|
||||||
&serializedContinuation;
|
&serializedContinuation;
|
||||||
std::shared_ptr<ComponentThread> target;
|
std::shared_ptr<sscl::ComponentThread> target;
|
||||||
InvocationTargetT invocationTarget;
|
InvocationTargetT invocationTarget;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -269,8 +286,8 @@ public:
|
|||||||
|
|
||||||
template <class OriginalCbFnT>
|
template <class OriginalCbFnT>
|
||||||
std::unique_ptr<std::forward_list<std::reference_wrapper<Qutex>>>
|
std::unique_ptr<std::forward_list<std::reference_wrapper<Qutex>>>
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT>::getAcquiredQutexHistory()
|
SerializedAsynchronousContinuation<OriginalCbFnT>::getAcquiredQutexHistory(
|
||||||
const
|
bool includeLocksWhichHaveBeenReleased) const
|
||||||
{
|
{
|
||||||
auto heldLocks = std::make_unique<
|
auto heldLocks = std::make_unique<
|
||||||
std::forward_list<std::reference_wrapper<Qutex>>>();
|
std::forward_list<std::reference_wrapper<Qutex>>>();
|
||||||
@@ -287,15 +304,19 @@ const
|
|||||||
currContin != nullptr;
|
currContin != nullptr;
|
||||||
currContin = currContin->getCallersContinuationShPtr())
|
currContin = currContin->getCallersContinuationShPtr())
|
||||||
{
|
{
|
||||||
auto serializedCont = std::dynamic_pointer_cast<
|
auto heldLockSet = currContin->getLockSet();
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT>>(currContin);
|
if (!heldLockSet.has_value()) { continue; }
|
||||||
|
|
||||||
if (serializedCont == nullptr) { continue; }
|
|
||||||
|
|
||||||
// Add this continuation's locks to the held locks list
|
// Add this continuation's locks to the held locks list
|
||||||
for (size_t i = 0; i < serializedCont->requiredLocks.locks.size(); ++i)
|
for (size_t i = 0; i < heldLockSet->get().locks.size(); ++i)
|
||||||
{
|
{
|
||||||
heldLocks->push_front(serializedCont->requiredLocks.locks[i].qutex);
|
if (!includeLocksWhichHaveBeenReleased
|
||||||
|
&& heldLockSet->get().locks[i].hasBeenReleased)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
heldLocks->push_front(heldLockSet->get().locks[i].qutex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,16 +348,15 @@ SerializedAsynchronousContinuation<OriginalCbFnT>
|
|||||||
currContin != nullptr;
|
currContin != nullptr;
|
||||||
currContin = currContin->getCallersContinuationShPtr())
|
currContin = currContin->getCallersContinuationShPtr())
|
||||||
{
|
{
|
||||||
auto serializedCont = std::dynamic_pointer_cast<
|
auto heldLockSet = currContin->getLockSet();
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT>>(currContin);
|
if (!heldLockSet.has_value()) { continue; }
|
||||||
|
|
||||||
if (serializedCont == nullptr) { continue; }
|
// A miss is expected here; only a hit indicates a potential deadlock.
|
||||||
|
auto lockUsageDesc = heldLockSet->get().findLockUsageDesc(
|
||||||
// Check if the firstFailedQutex is in this continuation's LockSet
|
firstFailedQutex);
|
||||||
try {
|
if (!lockUsageDesc.has_value()
|
||||||
serializedCont->requiredLocks.getLockUsageDesc(firstFailedQutex);
|
|| lockUsageDesc->get().hasBeenReleased)
|
||||||
} catch (const std::runtime_error& e) {
|
{
|
||||||
std::cerr << __func__ << ": " << e.what() << std::endl;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +364,7 @@ SerializedAsynchronousContinuation<OriginalCbFnT>
|
|||||||
<< "firstFailedQutex @" << &firstFailedQutex
|
<< "firstFailedQutex @" << &firstFailedQutex
|
||||||
<< " (" << firstFailedQutex.name << ") in LockSet of "
|
<< " (" << firstFailedQutex.name << ") in LockSet of "
|
||||||
<< "SerializedAsynchronousContinuation @"
|
<< "SerializedAsynchronousContinuation @"
|
||||||
<< serializedCont.get() << std::endl;
|
<< currContin.get() << std::endl;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -424,34 +444,29 @@ SerializedAsynchronousContinuation<OriginalCbFnT>
|
|||||||
* should eventually be able to acquire that lock.
|
* should eventually be able to acquire that lock.
|
||||||
*/
|
*/
|
||||||
for (std::shared_ptr<AsynchronousContinuationChainLink> currContin =
|
for (std::shared_ptr<AsynchronousContinuationChainLink> currContin =
|
||||||
this->serializedContinuation.getCallersContinuationShPtr();
|
parent.serializedContinuation
|
||||||
|
.getCallersContinuationShPtr();
|
||||||
currContin != nullptr;
|
currContin != nullptr;
|
||||||
currContin = currContin->getCallersContinuationShPtr())
|
currContin = currContin->getCallersContinuationShPtr())
|
||||||
{
|
{
|
||||||
auto serializedCont = std::dynamic_pointer_cast<
|
auto heldLockSet = currContin->getLockSet();
|
||||||
SerializedAsynchronousContinuation<OriginalCbFnT>>(currContin);
|
if (!heldLockSet.has_value()) { continue; }
|
||||||
|
|
||||||
if (serializedCont == nullptr) { continue; }
|
// A miss is expected here; a hit indicates a potential gridlock.
|
||||||
|
auto lockUsageDesc = heldLockSet->get().findLockUsageDesc(
|
||||||
// Check if this continuation holds the foreign lock
|
foreignLock);
|
||||||
try {
|
if (!lockUsageDesc.has_value()) { continue; }
|
||||||
const auto& lockUsageDesc = serializedCont->requiredLocks
|
|
||||||
.getLockUsageDesc(foreignLock);
|
|
||||||
|
|
||||||
// Matched! We hold a lock that the foreign owner is waiting for
|
// Matched! We hold a lock that the foreign owner is waiting for
|
||||||
std::cout << __func__ << ": Gridlock detected: We hold lock @"
|
std::cout << __func__ << ": Gridlock detected: We hold lock @"
|
||||||
<< &foreignLock << " (" << foreignLock.name << ") in "
|
<< &foreignLock << " (" << foreignLock.name << ") in "
|
||||||
"continuation @" << serializedCont.get()
|
"continuation @" << currContin.get()
|
||||||
<< ", while foreign owner @" << &foreignOwner
|
<< ", while foreign owner @" << &foreignOwner
|
||||||
<< " holds lock @" << &firstFailedQutex << " ("
|
<< " holds lock @" << &firstFailedQutex << " ("
|
||||||
<< firstFailedQutex.name << ") that we're waiting for"
|
<< firstFailedQutex.name << ") that we're waiting for"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::runtime_error& e) {
|
|
||||||
// This continuation doesn't hold the foreign lock. Continue.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,7 +480,7 @@ template <class InvocationTargetT>
|
|||||||
void SerializedAsynchronousContinuation<OriginalCbFnT>
|
void SerializedAsynchronousContinuation<OriginalCbFnT>
|
||||||
::LockerAndInvoker<InvocationTargetT>::operator()()
|
::LockerAndInvoker<InvocationTargetT>::operator()()
|
||||||
{
|
{
|
||||||
if (ComponentThread::getSelf() != target)
|
if (sscl::ComponentThread::getSelf() != target)
|
||||||
{
|
{
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LockerAndInvoker::operator(): Thread safety violation - "
|
"LockerAndInvoker::operator(): Thread safety violation - "
|
||||||
@@ -479,7 +494,7 @@ void SerializedAsynchronousContinuation<OriginalCbFnT>
|
|||||||
if (!serializedContinuation.requiredLocks.tryAcquireOrBackOff(
|
if (!serializedContinuation.requiredLocks.tryAcquireOrBackOff(
|
||||||
*this, firstFailedQutexRet))
|
*this, firstFailedQutexRet))
|
||||||
{
|
{
|
||||||
// Just allow this lockvoker to be dropped from its io_service.
|
// Just allow this lockvoker to be dropped from its io_context.
|
||||||
allowAwakening();
|
allowAwakening();
|
||||||
if (!deadlockLikely && !gridlockLikely)
|
if (!deadlockLikely && !gridlockLikely)
|
||||||
{ return; }
|
{ return; }
|
||||||
@@ -583,6 +598,6 @@ void SerializedAsynchronousContinuation<OriginalCbFnT>
|
|||||||
invocationTarget();
|
invocationTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|
||||||
#endif // SERIALIZED_ASYNCHRONOUS_CONTINUATION_H
|
#endif // SERIALIZED_ASYNCHRONOUS_CONTINUATION_H
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#ifndef SPINSCALE_ENV_KV_STORE_H
|
||||||
|
#define SPINSCALE_ENV_KV_STORE_H
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <ostream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sscl {
|
||||||
|
|
||||||
|
class EnvKvStore
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit EnvKvStore(
|
||||||
|
const std::vector<std::filesystem::path> &envFilePaths,
|
||||||
|
std::ostream &warningStream);
|
||||||
|
explicit EnvKvStore(
|
||||||
|
const std::vector<std::filesystem::path> &envFilePaths);
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Precedence: process getenv wins over compiled file-store values unless
|
||||||
|
* bypassProcessEnvironment is true (file store only).
|
||||||
|
*/
|
||||||
|
std::optional<std::string> find(
|
||||||
|
std::string_view name,
|
||||||
|
bool bypassProcessEnvironment = false) const;
|
||||||
|
|
||||||
|
/** Throws if find() returns nullopt. */
|
||||||
|
std::string get(
|
||||||
|
std::string_view name,
|
||||||
|
bool bypassProcessEnvironment = false) const;
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Typed int accessors. defaultValue applies only when find() is nullopt;
|
||||||
|
* nullopt defaultValue with a missing key throws. A present value that fails
|
||||||
|
* to parse or fails the positivity constraint always throws.
|
||||||
|
*/
|
||||||
|
int getInt(
|
||||||
|
std::string_view name,
|
||||||
|
std::optional<int> defaultValue = std::nullopt) const
|
||||||
|
{
|
||||||
|
return getIntWithConstraint(name, defaultValue, IntConstraint::Any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parsed value must be >= 0. */
|
||||||
|
int getPositiveInt(
|
||||||
|
std::string_view name,
|
||||||
|
std::optional<int> defaultValue = std::nullopt) const
|
||||||
|
{
|
||||||
|
return getIntWithConstraint(
|
||||||
|
name, defaultValue, IntConstraint::NonNegative);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parsed value must be > 0. */
|
||||||
|
int getPositiveNonZeroInt(
|
||||||
|
std::string_view name,
|
||||||
|
std::optional<int> defaultValue = std::nullopt) const
|
||||||
|
{
|
||||||
|
return getIntWithConstraint(
|
||||||
|
name, defaultValue, IntConstraint::PositiveNonZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** dotenv line parsing owned by EnvKvStore (definition in .cpp). */
|
||||||
|
class DotenvParser;
|
||||||
|
|
||||||
|
enum class IntConstraint
|
||||||
|
{
|
||||||
|
Any,
|
||||||
|
NonNegative,
|
||||||
|
PositiveNonZero,
|
||||||
|
};
|
||||||
|
|
||||||
|
static int parseInt(std::string_view name, const std::string &raw);
|
||||||
|
int getIntWithConstraint(
|
||||||
|
std::string_view name,
|
||||||
|
std::optional<int> defaultValue,
|
||||||
|
IntConstraint constraint) const;
|
||||||
|
|
||||||
|
void loadFiles(
|
||||||
|
const std::vector<std::filesystem::path> &envFilePaths,
|
||||||
|
std::ostream &warningStream);
|
||||||
|
void loadFile(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
std::ostream &warningStream);
|
||||||
|
void storeValue(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
const std::string &name,
|
||||||
|
const std::string &value,
|
||||||
|
std::ostream &warningStream);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, std::string> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl
|
||||||
|
|
||||||
|
#endif // SPINSCALE_ENV_KV_STORE_H
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#ifndef MULTI_OPERATION_RESULT_SET_H
|
||||||
|
#define MULTI_OPERATION_RESULT_SET_H
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
namespace sscl {
|
||||||
|
namespace co {
|
||||||
|
struct Group;
|
||||||
|
} // namespace co
|
||||||
|
|
||||||
|
/** Plain aggregate for fan-out / fan-in results returned from coroutines. */
|
||||||
|
struct MultiOperationResultSet
|
||||||
|
{
|
||||||
|
MultiOperationResultSet(
|
||||||
|
unsigned int total = 0,
|
||||||
|
unsigned int succeeded = 0,
|
||||||
|
unsigned int failed = 0)
|
||||||
|
: nTotal(total), nSucceeded(succeeded), nFailed(failed)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool isComplete() const
|
||||||
|
{ return nSucceeded + nFailed == nTotal; }
|
||||||
|
|
||||||
|
bool nTotalIsZero() const
|
||||||
|
{ return nTotal == 0; }
|
||||||
|
|
||||||
|
unsigned int nTotal;
|
||||||
|
unsigned int nSucceeded;
|
||||||
|
unsigned int nFailed;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Fan-out / fan-in counts plus optional aggregated member failure. */
|
||||||
|
struct MultiOperationResultSetWithException
|
||||||
|
{
|
||||||
|
MultiOperationResultSetWithException() = default;
|
||||||
|
|
||||||
|
MultiOperationResultSetWithException(
|
||||||
|
MultiOperationResultSet resultsIn,
|
||||||
|
std::exception_ptr memberFailureExceptionIn = nullptr)
|
||||||
|
: results(resultsIn),
|
||||||
|
memberFailureException(memberFailureExceptionIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/** Summarize a settled Group into counts + aggregated member failure. */
|
||||||
|
explicit MultiOperationResultSetWithException(const co::Group &group);
|
||||||
|
|
||||||
|
bool hasMemberFailure() const
|
||||||
|
{ return memberFailureException != nullptr; }
|
||||||
|
|
||||||
|
/** Combine this result set with another phase's counts and exception. */
|
||||||
|
MultiOperationResultSetWithException mergeWith(
|
||||||
|
const MultiOperationResultSetWithException &other) const
|
||||||
|
{
|
||||||
|
std::exception_ptr memberFailure = memberFailureException;
|
||||||
|
if (!memberFailure && other.hasMemberFailure()) {
|
||||||
|
memberFailure = other.memberFailureException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MultiOperationResultSetWithException(
|
||||||
|
MultiOperationResultSet(
|
||||||
|
results.nTotal + other.results.nTotal,
|
||||||
|
results.nSucceeded + other.results.nSucceeded,
|
||||||
|
results.nFailed + other.results.nFailed),
|
||||||
|
memberFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiOperationResultSet results;
|
||||||
|
std::exception_ptr memberFailureException = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl
|
||||||
|
|
||||||
|
#endif // MULTI_OPERATION_RESULT_SET_H
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
#define PUPPET_APPLICATION_H
|
#define PUPPET_APPLICATION_H
|
||||||
|
|
||||||
#include <config.h>
|
#include <config.h>
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <spinscale/callback.h>
|
|
||||||
|
#include <spinscale/co/group.h>
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
#include <spinscale/componentThread.h>
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl {
|
||||||
@@ -18,24 +20,25 @@ public:
|
|||||||
const std::vector<std::shared_ptr<PuppetThread>> &threads);
|
const std::vector<std::shared_ptr<PuppetThread>> &threads);
|
||||||
~PuppetApplication() = default;
|
~PuppetApplication() = default;
|
||||||
|
|
||||||
// Thread management methods
|
co::ViralNonPostingInvoker<void> joltAllPuppetThreadsCReq();
|
||||||
typedef std::function<void()> puppetThreadLifetimeMgmtOpCbFn;
|
co::ViralNonPostingInvoker<void> startAllPuppetThreadsCReq();
|
||||||
void joltAllPuppetThreadsReq(
|
co::ViralNonPostingInvoker<void> pauseAllPuppetThreadsCReq();
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback);
|
co::ViralNonPostingInvoker<void> resumeAllPuppetThreadsCReq();
|
||||||
void startAllPuppetThreadsReq(
|
co::ViralNonPostingInvoker<void> exitAllPuppetThreadsCReq();
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback);
|
|
||||||
void pauseAllPuppetThreadsReq(
|
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback);
|
|
||||||
void resumeAllPuppetThreadsReq(
|
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback);
|
|
||||||
void exitAllPuppetThreadsReq(
|
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback);
|
|
||||||
|
|
||||||
// CPU distribution method
|
// CPU distribution method
|
||||||
void distributeAndPinThreadsAcrossCpus();
|
void distributeAndPinThreadsAcrossCpus();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Collection of PuppetThread instances
|
using PuppetLifetimeMgmtInvoker =
|
||||||
|
PuppetThread::ViralThreadLifetimeMgmtInvoker;
|
||||||
|
using PuppetLifetimeMgmtGroup = co::Group;
|
||||||
|
|
||||||
|
void addAllPuppetLifetimeInvokersToGroup(
|
||||||
|
PuppetLifetimeMgmtGroup &group,
|
||||||
|
std::vector<PuppetLifetimeMgmtInvoker> &invokers,
|
||||||
|
PuppetThread::ThreadOp threadOp) const;
|
||||||
|
|
||||||
std::vector<std::shared_ptr<PuppetThread>> componentThreads;
|
std::vector<std::shared_ptr<PuppetThread>> componentThreads;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +63,9 @@ protected:
|
|||||||
bool threadsHaveBeenJolted = false;
|
bool threadsHaveBeenJolted = false;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class PuppetThreadLifetimeMgmtOp;
|
co::ViralNonPostingInvoker<void> allPuppetThreadsLifetimeOpCReq(
|
||||||
|
PuppetThread::ThreadOp threadOp,
|
||||||
|
std::string_view emptyThreadsLogMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef SHARED_RESOURCE_GROUP_H
|
||||||
|
#define SHARED_RESOURCE_GROUP_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace sscl {
|
||||||
|
|
||||||
|
template <typename LockType, typename ResourceType>
|
||||||
|
class SharedResourceGroup
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SharedResourceGroup() = default;
|
||||||
|
|
||||||
|
explicit SharedResourceGroup(const std::string& lockName)
|
||||||
|
: lock(lockName)
|
||||||
|
{}
|
||||||
|
|
||||||
|
SharedResourceGroup(
|
||||||
|
const std::string& lockName, const ResourceType& initialRsrc)
|
||||||
|
: lock(lockName), rsrc(initialRsrc)
|
||||||
|
{}
|
||||||
|
|
||||||
|
SharedResourceGroup(
|
||||||
|
const std::string& lockName, ResourceType&& initialRsrc)
|
||||||
|
: lock(lockName), rsrc(std::move(initialRsrc))
|
||||||
|
{}
|
||||||
|
|
||||||
|
~SharedResourceGroup() = default;
|
||||||
|
|
||||||
|
LockType lock;
|
||||||
|
ResourceType rsrc;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl
|
||||||
|
|
||||||
|
#endif // SHARED_RESOURCE_GROUP_H
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#ifndef SYNC_CANCELER_FOR_ASYNC_WORK_H
|
||||||
|
#define SYNC_CANCELER_FOR_ASYNC_WORK_H
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <spinscale/sharedResourceGroup.h>
|
||||||
|
#include <spinscale/spinLock.h>
|
||||||
|
|
||||||
|
namespace sscl {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SyncCancelerForAsyncWork
|
||||||
|
*
|
||||||
|
* A small helper to coordinate synchronous cancellation requests with
|
||||||
|
* asynchronous work that must only observe cancellation at explicit
|
||||||
|
* uncancelable segment boundaries.
|
||||||
|
*
|
||||||
|
* The async callee should structure its logic as:
|
||||||
|
* - enter an uncancelable segment (execUncancelableSegmentOrAbort)
|
||||||
|
* - perform synchronous work that must not be interrupted
|
||||||
|
* - exit the segment
|
||||||
|
* - perform cancelable async work (outside the lock)
|
||||||
|
* - repeat
|
||||||
|
*
|
||||||
|
* requestStop() blocks until any currently-executing segment releases s.lock,
|
||||||
|
* then flips shouldContinue to false. This guarantees shouldContinue is stable
|
||||||
|
* throughout each uncancelable segment.
|
||||||
|
*
|
||||||
|
* Shutdown call sites that also cancel internal async operations (timers, I/O,
|
||||||
|
* hardware capture, etc.) must call requestStop() on slot cancelers before
|
||||||
|
* cancelling those internal operations, so callees observe stop intent when
|
||||||
|
* the internal op unblocks their co_await.
|
||||||
|
*
|
||||||
|
* startAcceptingWork() is intentionally unlocked. Precondition: callers must
|
||||||
|
* only call startAcceptingWork() when no async callee is running yet (e.g. at
|
||||||
|
* the end of setup(), before posting/arming the first async work). If this
|
||||||
|
* method races a running callee, that is a caller bug.
|
||||||
|
*/
|
||||||
|
class SyncCancelerForAsyncWork
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Resources
|
||||||
|
{
|
||||||
|
bool shouldContinue = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
SyncCancelerForAsyncWork() = default;
|
||||||
|
|
||||||
|
void startAcceptingWork()
|
||||||
|
{
|
||||||
|
// Intentionally unlocked — see class-level EXPLANATION above.
|
||||||
|
s.rsrc.shouldContinue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return shouldContinue before this call (was work being accepted?) */
|
||||||
|
bool requestStop()
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
const bool wasContinuing = s.rsrc.shouldContinue;
|
||||||
|
s.rsrc.shouldContinue = false;
|
||||||
|
return wasContinuing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true if requestStop() has set shouldContinue to false. */
|
||||||
|
bool isCancellationRequested()
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
return isCancellationRequestedUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCancellationRequestedUnlocked() const
|
||||||
|
{ return !s.rsrc.shouldContinue; }
|
||||||
|
|
||||||
|
template<typename Body>
|
||||||
|
requires std::invocable<Body>
|
||||||
|
bool execUncancelableSegmentOrAbort(Body&& body)
|
||||||
|
{
|
||||||
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
if (!s.rsrc.shouldContinue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::forward<Body>(body)();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
sscl::SharedResourceGroup<sscl::SpinLock, Resources> s;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl
|
||||||
|
|
||||||
|
#endif // SYNC_CANCELER_FOR_ASYNC_WORK_H
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
add_library(spinscale_probe_support STATIC
|
||||||
|
probeComponentThread.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(spinscale_probe_support PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(spinscale_probe_support PUBLIC
|
||||||
|
spinscale
|
||||||
|
)
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#include <probe/probeComponentThread.h>
|
||||||
|
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <spinscale/component.h>
|
||||||
|
|
||||||
|
namespace sscl::probe {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr sscl::ThreadId PROBE_PUPPETEER_THREAD_ID = 2;
|
||||||
|
|
||||||
|
class ProbeDummyPuppeteerComponent
|
||||||
|
: public sscl::pptr::PuppeteerComponent
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ProbeDummyPuppeteerComponent(
|
||||||
|
const std::shared_ptr<sscl::PuppeteerThread>& componentThreadIn)
|
||||||
|
: sscl::pptr::PuppeteerComponent(componentThreadIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void handleLoopExceptionHook() override
|
||||||
|
{
|
||||||
|
std::cerr << "ProbeComponentThreadHarness: puppeteer loop exception\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* PuppeteerThread starts its std::thread inside the constructor, but
|
||||||
|
* enable_shared_from_this::weak_this is only armed after make_shared returns.
|
||||||
|
* Without a barrier, initializeTls()'s shared_from_this() races and throws
|
||||||
|
* std::bad_weak_ptr. DedicatedIoThread uses the same handshake.
|
||||||
|
*/
|
||||||
|
struct ProbeThreadStartupState
|
||||||
|
{
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable condition;
|
||||||
|
bool allowInitialization = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void waitForProbeThreadStartupPermission(
|
||||||
|
const std::shared_ptr<ProbeThreadStartupState>& startupState)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(startupState->mutex);
|
||||||
|
startupState->condition.wait(
|
||||||
|
lock,
|
||||||
|
[&startupState]() { return startupState->allowInitialization; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseProbeThreadStartupBarrier(
|
||||||
|
const std::shared_ptr<ProbeThreadStartupState>& startupState)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(startupState->mutex);
|
||||||
|
startupState->allowInitialization = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
startupState->condition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void probePuppeteerMain(
|
||||||
|
const sscl::PuppeteerThread::EntryFnArguments& args,
|
||||||
|
const std::function<void(
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>&)>& work,
|
||||||
|
std::promise<std::exception_ptr>& donePromise,
|
||||||
|
const std::shared_ptr<ProbeThreadStartupState>& startupState)
|
||||||
|
{
|
||||||
|
waitForProbeThreadStartupPermission(startupState);
|
||||||
|
|
||||||
|
sscl::PuppeteerThread& thr = args.usableBeforeJolt;
|
||||||
|
thr.initializeTls();
|
||||||
|
sscl::ComponentThread::setPuppeteerThreadId(PROBE_PUPPETEER_THREAD_ID);
|
||||||
|
|
||||||
|
std::shared_ptr<sscl::PuppeteerThread> thrPtr =
|
||||||
|
std::static_pointer_cast<sscl::PuppeteerThread>(thr.shared_from_this());
|
||||||
|
sscl::ComponentThread::setPuppeteerThread(thrPtr);
|
||||||
|
|
||||||
|
try {
|
||||||
|
work(thrPtr);
|
||||||
|
donePromise.set_value(nullptr);
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
donePromise.set_value(std::current_exception());
|
||||||
|
}
|
||||||
|
|
||||||
|
thr.getIoContext().stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ProbeComponentThreadHarness::ProbeComponentThreadHarness(
|
||||||
|
const char *threadName)
|
||||||
|
: threadName(threadName),
|
||||||
|
dummyComponent(std::make_shared<ProbeDummyPuppeteerComponent>(
|
||||||
|
std::shared_ptr<sscl::PuppeteerThread>()))
|
||||||
|
{}
|
||||||
|
|
||||||
|
ProbeComponentThreadHarness::~ProbeComponentThreadHarness() = default;
|
||||||
|
|
||||||
|
std::shared_ptr<sscl::ComponentThread>
|
||||||
|
ProbeComponentThreadHarness::componentThread() const
|
||||||
|
{
|
||||||
|
return lastComponentThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProbeComponentThreadHarness::runSync(
|
||||||
|
const std::function<void(
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>&)>& work)
|
||||||
|
{
|
||||||
|
std::promise<std::exception_ptr> donePromise;
|
||||||
|
std::future<std::exception_ptr> doneFuture = donePromise.get_future();
|
||||||
|
auto startupState = std::make_shared<ProbeThreadStartupState>();
|
||||||
|
|
||||||
|
std::shared_ptr<sscl::PuppeteerThread> runThread =
|
||||||
|
std::make_shared<sscl::PuppeteerThread>(
|
||||||
|
PROBE_PUPPETEER_THREAD_ID,
|
||||||
|
threadName,
|
||||||
|
[&work, &donePromise, startupState](
|
||||||
|
const sscl::PuppeteerThread::EntryFnArguments& args)
|
||||||
|
{
|
||||||
|
probePuppeteerMain(args, work, donePromise, startupState);
|
||||||
|
},
|
||||||
|
*dummyComponent,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
dummyComponent->thread = runThread;
|
||||||
|
lastComponentThread = runThread;
|
||||||
|
releaseProbeThreadStartupBarrier(startupState);
|
||||||
|
runThread->thread.join();
|
||||||
|
|
||||||
|
std::exception_ptr probeException = doneFuture.get();
|
||||||
|
if (probeException) {
|
||||||
|
std::rethrow_exception(probeException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void runNonViralNurseryOnComponentThread(
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>& componentThread,
|
||||||
|
std::function<sscl::co::NonViralNonPostingInvoker(
|
||||||
|
sscl::co::NonViralTaskNursery::Slot::Lease&)> invokerFactory,
|
||||||
|
std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
(void)timeout;
|
||||||
|
|
||||||
|
sscl::co::NonViralTaskNursery nursery;
|
||||||
|
nursery.openAdmission();
|
||||||
|
nursery.launch(
|
||||||
|
[&invokerFactory](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
|
||||||
|
{
|
||||||
|
return invokerFactory(lease);
|
||||||
|
});
|
||||||
|
nursery.closeAdmission();
|
||||||
|
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::probe
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#ifndef SPINSCALE_PROBE_COMPONENT_THREAD_H
|
||||||
|
#define SPINSCALE_PROBE_COMPONENT_THREAD_H
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
|
#include <spinscale/co/nonViralTaskNursery.h>
|
||||||
|
|
||||||
|
namespace sscl::probe {
|
||||||
|
|
||||||
|
constexpr std::chrono::milliseconds defaultProbeTaskTimeout{10000};
|
||||||
|
|
||||||
|
void runNonViralNurseryOnComponentThread(
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>& componentThread,
|
||||||
|
std::function<sscl::co::NonViralNonPostingInvoker(
|
||||||
|
sscl::co::NonViralTaskNursery::Slot::Lease&)> invokerFactory,
|
||||||
|
std::chrono::milliseconds timeout = defaultProbeTaskTimeout);
|
||||||
|
|
||||||
|
/** Sync driver: run work on a temporary puppeteer ComponentThread.
|
||||||
|
*
|
||||||
|
* Shared by lcameraDev probe tools and HIL/unit tests. Not tied to gtest.
|
||||||
|
*/
|
||||||
|
class ProbeComponentThreadHarness
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ProbeComponentThreadHarness(
|
||||||
|
const char *threadName = "spinscale-probe");
|
||||||
|
~ProbeComponentThreadHarness();
|
||||||
|
|
||||||
|
ProbeComponentThreadHarness(const ProbeComponentThreadHarness &) = delete;
|
||||||
|
ProbeComponentThreadHarness &operator=(
|
||||||
|
const ProbeComponentThreadHarness &) = delete;
|
||||||
|
|
||||||
|
std::shared_ptr<sscl::ComponentThread> componentThread() const;
|
||||||
|
|
||||||
|
void runSync(
|
||||||
|
const std::function<void(
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>&)>& work);
|
||||||
|
|
||||||
|
template <typename InvokerFactory>
|
||||||
|
void runNonViralNurseryTask(
|
||||||
|
InvokerFactory &&invokerFactory,
|
||||||
|
std::chrono::milliseconds timeout = defaultProbeTaskTimeout)
|
||||||
|
{
|
||||||
|
runSync(
|
||||||
|
[this, &invokerFactory, timeout](
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>& componentThread)
|
||||||
|
{
|
||||||
|
sscl::probe::runNonViralNurseryOnComponentThread(
|
||||||
|
componentThread,
|
||||||
|
std::forward<InvokerFactory>(invokerFactory),
|
||||||
|
timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string threadName;
|
||||||
|
std::shared_ptr<sscl::pptr::PuppeteerComponent> dummyComponent;
|
||||||
|
std::shared_ptr<sscl::ComponentThread> lastComponentThread;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::probe
|
||||||
|
|
||||||
|
#endif // SPINSCALE_PROBE_COMPONENT_THREAD_H
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include <boost/asio/detail/call_stack.hpp>
|
||||||
|
#include <boost/asio/detail/thread_context.hpp>
|
||||||
|
#include <boost/asio/detail/tss_ptr.hpp>
|
||||||
|
|
||||||
|
namespace boost {
|
||||||
|
namespace asio {
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
/** Single translation-unit definition for Boost.Asio call_stack TLS.
|
||||||
|
* Other TUs include boostAsioLinkageFix.h first and use extern template.
|
||||||
|
*/
|
||||||
|
template
|
||||||
|
tss_ptr<call_stack<thread_context, thread_info_base>::context>
|
||||||
|
call_stack<thread_context, thread_info_base>::top_;
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
} // namespace asio
|
||||||
|
} // namespace boost
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#include <spinscale/callableTracer.h>
|
#include <spinscale/cps/callableTracer.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
bool CallableTracer::optTraceCallables = false;
|
bool CallableTracer::optTraceCallables = false;
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|||||||
+7
-7
@@ -27,10 +27,10 @@ void PuppetComponent::defaultPuppetMain(
|
|||||||
if (args.preJoltHook) { args.preJoltHook(thr); }
|
if (args.preJoltHook) { args.preJoltHook(thr); }
|
||||||
|
|
||||||
/** FIXME:
|
/** FIXME:
|
||||||
* Figure out why we don't call reset() here, and then explicitly document
|
* Figure out why we don't call restart() here, and then explicitly document
|
||||||
* it.
|
* it.
|
||||||
*/
|
*/
|
||||||
thr.getIoService().run();
|
thr.getIoContext().run();
|
||||||
thr.initializeTls();
|
thr.initializeTls();
|
||||||
|
|
||||||
comp.postJoltHook();
|
comp.postJoltHook();
|
||||||
@@ -52,15 +52,15 @@ void PuppetComponent::defaultPuppetMain(
|
|||||||
/** EXPLANATION:
|
/** EXPLANATION:
|
||||||
* This reset() call is crucial for async bridging patterns
|
* This reset() call is crucial for async bridging patterns
|
||||||
* to work.
|
* to work.
|
||||||
* When the outermost thread's io_service is stop()ped (e.g.,
|
* When the outermost thread's io_context is stop()ped (e.g.,
|
||||||
* from JOLT sequence), it won't process any new work until
|
* from JOLT sequence), it won't process any new work until
|
||||||
* reset() is called, even if nested async operations try to
|
* restart() is called, even if nested async operations try to
|
||||||
* post work to it. This means async bridges invoked from
|
* post work to it. This means async bridges invoked from
|
||||||
* the outermost thread main sequence won't work until this
|
* the outermost thread main sequence won't work until this
|
||||||
* reset() call.
|
* restart() call.
|
||||||
*/
|
*/
|
||||||
thr.getIoService().reset();
|
thr.getIoContext().restart();
|
||||||
thr.getIoService().run();
|
thr.getIoContext().run();
|
||||||
}
|
}
|
||||||
catch (const std::exception& e)
|
catch (const std::exception& e)
|
||||||
{
|
{
|
||||||
|
|||||||
+28
-28
@@ -1,13 +1,13 @@
|
|||||||
#include <boostAsioLinkageFix.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <boost/asio/io_service.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
#include <spinscale/asynchronousContinuation.h>
|
#include <spinscale/cps/asynchronousContinuation.h>
|
||||||
#include <spinscale/callback.h>
|
#include <spinscale/cps/callback.h>
|
||||||
#include <spinscale/callableTracer.h>
|
#include <spinscale/cps/callableTracer.h>
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
#include <spinscale/component.h>
|
#include <spinscale/component.h>
|
||||||
#include <spinscale/componentThread.h>
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ std::shared_ptr<PuppeteerThread> ComponentThread::getPptr()
|
|||||||
void PuppeteerThread::exitLoop(void)
|
void PuppeteerThread::exitLoop(void)
|
||||||
{
|
{
|
||||||
keepLooping = false;
|
keepLooping = false;
|
||||||
getIoService().stop();
|
getIoContext().stop();
|
||||||
std::cout << name << ": Signaled main loop to exit." << "\n";
|
std::cout << name << ": Signaled main loop to exit." << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +79,14 @@ const std::shared_ptr<ComponentThread> ComponentThread::getSelf(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PuppetThread::ThreadLifetimeMgmtOp
|
class PuppetThread::ThreadLifetimeMgmtOp
|
||||||
: public PostedAsynchronousContinuation<threadLifetimeMgmtOpCbFn>
|
: public cps::PostedAsynchronousContinuation<threadLifetimeMgmtOpCbFn>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ThreadLifetimeMgmtOp(
|
ThreadLifetimeMgmtOp(
|
||||||
const std::shared_ptr<ComponentThread> &caller,
|
const std::shared_ptr<ComponentThread> &caller,
|
||||||
const std::shared_ptr<PuppetThread> &target,
|
const std::shared_ptr<PuppetThread> &target,
|
||||||
Callback<threadLifetimeMgmtOpCbFn> callback)
|
cps::Callback<threadLifetimeMgmtOpCbFn> callback)
|
||||||
: PostedAsynchronousContinuation<threadLifetimeMgmtOpCbFn>(
|
: cps::PostedAsynchronousContinuation<threadLifetimeMgmtOpCbFn>(
|
||||||
caller, callback),
|
caller, callback),
|
||||||
target(target)
|
target(target)
|
||||||
{}
|
{}
|
||||||
@@ -103,7 +103,7 @@ public:
|
|||||||
"JOLT request."
|
"JOLT request."
|
||||||
<< "\n";
|
<< "\n";
|
||||||
|
|
||||||
target->io_service.stop();
|
target->io_context.stop();
|
||||||
callOriginalCb();
|
callOriginalCb();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ public:
|
|||||||
"exitThread (main queue)." << "\n";
|
"exitThread (main queue)." << "\n";
|
||||||
|
|
||||||
target->cleanup();
|
target->cleanup();
|
||||||
target->io_service.stop();
|
target->io_context.stop();
|
||||||
callOriginalCb();
|
callOriginalCb();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,8 +141,8 @@ public:
|
|||||||
"exitThread (pause queue)."<< "\n";
|
"exitThread (pause queue)."<< "\n";
|
||||||
|
|
||||||
target->cleanup();
|
target->cleanup();
|
||||||
target->pause_io_service.stop();
|
target->pause_io_context.stop();
|
||||||
target->io_service.stop();
|
target->io_context.stop();
|
||||||
callOriginalCb();
|
callOriginalCb();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,8 +158,8 @@ public:
|
|||||||
* have a chance to invoke the callback until it's unblocked.
|
* have a chance to invoke the callback until it's unblocked.
|
||||||
*/
|
*/
|
||||||
callOriginalCb();
|
callOriginalCb();
|
||||||
target->pause_io_service.reset();
|
target->pause_io_context.restart();
|
||||||
target->pause_io_service.run();
|
target->pause_io_context.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
void resumeThreadReq1_posted(
|
void resumeThreadReq1_posted(
|
||||||
@@ -169,7 +169,7 @@ public:
|
|||||||
std::cout << __func__ << ": Thread '" << target->name << "': handling "
|
std::cout << __func__ << ": Thread '" << target->name << "': handling "
|
||||||
"resumeThread." << "\n";
|
"resumeThread." << "\n";
|
||||||
|
|
||||||
target->pause_io_service.stop();
|
target->pause_io_context.stop();
|
||||||
callOriginalCb();
|
callOriginalCb();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -181,7 +181,7 @@ void ComponentThread::cleanup(void)
|
|||||||
|
|
||||||
void PuppetThread::joltThreadReq(
|
void PuppetThread::joltThreadReq(
|
||||||
const std::shared_ptr<PuppetThread>& selfPtr,
|
const std::shared_ptr<PuppetThread>& selfPtr,
|
||||||
Callback<threadLifetimeMgmtOpCbFn> callback)
|
cps::Callback<threadLifetimeMgmtOpCbFn> callback)
|
||||||
{
|
{
|
||||||
/** EXPLANATION:
|
/** EXPLANATION:
|
||||||
* We can't use shared_from_this() here because JOLTing occurs prior to
|
* We can't use shared_from_this() here because JOLTing occurs prior to
|
||||||
@@ -209,45 +209,45 @@ void PuppetThread::joltThreadReq(
|
|||||||
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
||||||
puppeteer, selfPtr, callback);
|
puppeteer, selfPtr, callback);
|
||||||
|
|
||||||
this->getIoService().post(
|
boost::asio::post(this->getIoContext(),
|
||||||
STC(std::bind(
|
STC(std::bind(
|
||||||
&ThreadLifetimeMgmtOp::joltThreadReq1_posted,
|
&ThreadLifetimeMgmtOp::joltThreadReq1_posted,
|
||||||
request.get(), request)));
|
request.get(), request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread management method implementations
|
// Thread management method implementations
|
||||||
void PuppetThread::startThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback)
|
void PuppetThread::startThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback)
|
||||||
{
|
{
|
||||||
std::shared_ptr<ComponentThread> caller = getSelf();
|
std::shared_ptr<ComponentThread> caller = getSelf();
|
||||||
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
||||||
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
||||||
callback);
|
callback);
|
||||||
|
|
||||||
this->getIoService().post(
|
boost::asio::post(this->getIoContext(),
|
||||||
STC(std::bind(
|
STC(std::bind(
|
||||||
&ThreadLifetimeMgmtOp::startThreadReq1_posted,
|
&ThreadLifetimeMgmtOp::startThreadReq1_posted,
|
||||||
request.get(), request)));
|
request.get(), request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetThread::exitThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback)
|
void PuppetThread::exitThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback)
|
||||||
{
|
{
|
||||||
std::shared_ptr<ComponentThread> caller = getSelf();
|
std::shared_ptr<ComponentThread> caller = getSelf();
|
||||||
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
||||||
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
||||||
callback);
|
callback);
|
||||||
|
|
||||||
this->getIoService().post(
|
boost::asio::post(this->getIoContext(),
|
||||||
STC(std::bind(
|
STC(std::bind(
|
||||||
&ThreadLifetimeMgmtOp::exitThreadReq1_mainQueue_posted,
|
&ThreadLifetimeMgmtOp::exitThreadReq1_mainQueue_posted,
|
||||||
request.get(), request)));
|
request.get(), request)));
|
||||||
|
|
||||||
pause_io_service.post(
|
boost::asio::post(pause_io_context,
|
||||||
STC(std::bind(
|
STC(std::bind(
|
||||||
&ThreadLifetimeMgmtOp::exitThreadReq1_pauseQueue_posted,
|
&ThreadLifetimeMgmtOp::exitThreadReq1_pauseQueue_posted,
|
||||||
request.get(), request)));
|
request.get(), request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetThread::pauseThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback)
|
void PuppetThread::pauseThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback)
|
||||||
{
|
{
|
||||||
if (id == sscl::pptr::puppeteerThreadId)
|
if (id == sscl::pptr::puppeteerThreadId)
|
||||||
{
|
{
|
||||||
@@ -260,13 +260,13 @@ void PuppetThread::pauseThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback)
|
|||||||
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
||||||
callback);
|
callback);
|
||||||
|
|
||||||
this->getIoService().post(
|
boost::asio::post(this->getIoContext(),
|
||||||
STC(std::bind(
|
STC(std::bind(
|
||||||
&ThreadLifetimeMgmtOp::pauseThreadReq1_posted,
|
&ThreadLifetimeMgmtOp::pauseThreadReq1_posted,
|
||||||
request.get(), request)));
|
request.get(), request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetThread::resumeThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback)
|
void PuppetThread::resumeThreadReq(cps::Callback<threadLifetimeMgmtOpCbFn> callback)
|
||||||
{
|
{
|
||||||
if (id == sscl::pptr::puppeteerThreadId)
|
if (id == sscl::pptr::puppeteerThreadId)
|
||||||
{
|
{
|
||||||
@@ -274,13 +274,13 @@ void PuppetThread::resumeThreadReq(Callback<threadLifetimeMgmtOpCbFn> callback)
|
|||||||
+ ": invoked on puppeteer thread");
|
+ ": invoked on puppeteer thread");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post to the pause_io_service to unblock the paused thread
|
// Post to the pause_io_context to unblock the paused thread
|
||||||
std::shared_ptr<ComponentThread> caller = getSelf();
|
std::shared_ptr<ComponentThread> caller = getSelf();
|
||||||
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
auto request = std::make_shared<ThreadLifetimeMgmtOp>(
|
||||||
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
caller, std::static_pointer_cast<PuppetThread>(shared_from_this()),
|
||||||
callback);
|
callback);
|
||||||
|
|
||||||
pause_io_service.post(
|
boost::asio::post(pause_io_context,
|
||||||
STC(std::bind(
|
STC(std::bind(
|
||||||
&ThreadLifetimeMgmtOp::resumeThreadReq1_posted,
|
&ThreadLifetimeMgmtOp::resumeThreadReq1_posted,
|
||||||
request.get(), request)));
|
request.get(), request)));
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <ranges>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <spinscale/envKvStore.h>
|
||||||
|
|
||||||
|
namespace sscl {
|
||||||
|
|
||||||
|
class EnvKvStore::DotenvParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static bool lineIsBlankOrComment(const std::string &line)
|
||||||
|
{
|
||||||
|
std::string trimmed = trim(line);
|
||||||
|
return trimmed.empty() || trimmed.front() == '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::pair<std::string, std::string> parseAssignment(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
std::size_t lineNumber,
|
||||||
|
const std::string &line)
|
||||||
|
{
|
||||||
|
std::size_t separator = line.find('=');
|
||||||
|
if (separator == std::string::npos)
|
||||||
|
{
|
||||||
|
throw makeParseError(
|
||||||
|
envFilePath, lineNumber, "Expected KEY=value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string name = trim(std::string_view(line).substr(0, separator));
|
||||||
|
if (!nameIsValid(name))
|
||||||
|
{
|
||||||
|
throw makeParseError(
|
||||||
|
envFilePath, lineNumber, "Invalid variable name.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
std::move(name),
|
||||||
|
parseValue(
|
||||||
|
envFilePath,
|
||||||
|
lineNumber,
|
||||||
|
std::string_view(line).substr(separator + 1))};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string trim(std::string_view value)
|
||||||
|
{
|
||||||
|
auto begin = std::ranges::find_if_not(
|
||||||
|
value, [](unsigned char c) { return std::isspace(c); });
|
||||||
|
auto rbegin = std::ranges::find_if_not(
|
||||||
|
value | std::views::reverse,
|
||||||
|
[](unsigned char c) { return std::isspace(c); });
|
||||||
|
auto end = rbegin.base();
|
||||||
|
if (begin >= end) { return {}; }
|
||||||
|
return std::string(begin, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool characterIsValidNameStart(char c)
|
||||||
|
{
|
||||||
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool characterIsValidNameBody(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool nameIsValid(std::string_view name)
|
||||||
|
{
|
||||||
|
if (name.empty() || !characterIsValidNameStart(name.front())) { return false; }
|
||||||
|
return std::ranges::all_of(name.substr(1), characterIsValidNameBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::runtime_error makeParseError(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
std::size_t lineNumber,
|
||||||
|
const std::string &message)
|
||||||
|
{
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << envFilePath << ":" << lineNumber << ": " << message;
|
||||||
|
return std::runtime_error(stream.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::size_t findClosingQuote(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
std::size_t lineNumber,
|
||||||
|
std::string_view value)
|
||||||
|
{
|
||||||
|
char quote = value.front();
|
||||||
|
bool escapeNext = false;
|
||||||
|
for (std::size_t i = 1; i < value.size(); ++i)
|
||||||
|
{
|
||||||
|
if (escapeNext)
|
||||||
|
{
|
||||||
|
escapeNext = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (quote == '"' && value[i] == '\\')
|
||||||
|
{
|
||||||
|
escapeNext = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value[i] == quote) { return i; }
|
||||||
|
}
|
||||||
|
throw makeParseError(
|
||||||
|
envFilePath, lineNumber, "Unterminated quoted value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string decodeDoubleQuotedValue(std::string_view value)
|
||||||
|
{
|
||||||
|
std::string decoded;
|
||||||
|
decoded.reserve(value.size());
|
||||||
|
bool escapeNext = false;
|
||||||
|
for (char c : value)
|
||||||
|
{
|
||||||
|
if (!escapeNext && c == '\\')
|
||||||
|
{
|
||||||
|
escapeNext = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (escapeNext)
|
||||||
|
{
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case 'n':
|
||||||
|
decoded.push_back('\n');
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
decoded.push_back('\r');
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
decoded.push_back('\t');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
decoded.push_back(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
escapeNext = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
decoded.push_back(c);
|
||||||
|
}
|
||||||
|
if (escapeNext) { decoded.push_back('\\'); }
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string parseQuotedValue(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
std::size_t lineNumber,
|
||||||
|
std::string_view value)
|
||||||
|
{
|
||||||
|
char quote = value.front();
|
||||||
|
std::size_t closingQuote =
|
||||||
|
findClosingQuote(envFilePath, lineNumber, value);
|
||||||
|
std::string trailing = trim(value.substr(closingQuote + 1));
|
||||||
|
if (!trailing.empty() && trailing.front() != '#')
|
||||||
|
{
|
||||||
|
throw makeParseError(
|
||||||
|
envFilePath,
|
||||||
|
lineNumber,
|
||||||
|
"Unexpected text after quoted value.");
|
||||||
|
}
|
||||||
|
std::string_view quotedBody = value.substr(1, closingQuote - 1);
|
||||||
|
if (quote == '"') { return decodeDoubleQuotedValue(quotedBody); }
|
||||||
|
return std::string(quotedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string parseValue(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
std::size_t lineNumber,
|
||||||
|
std::string_view rawValue)
|
||||||
|
{
|
||||||
|
std::string value = trim(rawValue);
|
||||||
|
if (value.empty()) { return {}; }
|
||||||
|
if (value.front() == '\'' || value.front() == '"') { return parseQuotedValue(envFilePath, lineNumber, value); }
|
||||||
|
return trim(value.substr(0, value.find('#')));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
EnvKvStore::EnvKvStore(
|
||||||
|
const std::vector<std::filesystem::path> &envFilePaths,
|
||||||
|
std::ostream &warningStream)
|
||||||
|
{
|
||||||
|
loadFiles(envFilePaths, warningStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvKvStore::EnvKvStore(
|
||||||
|
const std::vector<std::filesystem::path> &envFilePaths)
|
||||||
|
: EnvKvStore(envFilePaths, std::cerr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void EnvKvStore::loadFiles(
|
||||||
|
const std::vector<std::filesystem::path> &envFilePaths,
|
||||||
|
std::ostream &warningStream)
|
||||||
|
{
|
||||||
|
for (const std::filesystem::path &envFilePath : envFilePaths)
|
||||||
|
{
|
||||||
|
loadFile(envFilePath, warningStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> EnvKvStore::find(
|
||||||
|
std::string_view name,
|
||||||
|
bool bypassProcessEnvironment) const
|
||||||
|
{
|
||||||
|
if (!bypassProcessEnvironment)
|
||||||
|
{
|
||||||
|
std::string ownedName(name);
|
||||||
|
if (const char *value = std::getenv(ownedName.c_str())) {
|
||||||
|
return std::string(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto value = values.find(std::string(name));
|
||||||
|
if (value == values.end()) { return std::nullopt; }
|
||||||
|
return value->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string EnvKvStore::get(
|
||||||
|
std::string_view name,
|
||||||
|
bool bypassProcessEnvironment) const
|
||||||
|
{
|
||||||
|
std::optional<std::string> value = find(name, bypassProcessEnvironment);
|
||||||
|
if (!value.has_value())
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("EnvKvStore: missing key '")
|
||||||
|
+ std::string(name)
|
||||||
|
+ "'");
|
||||||
|
}
|
||||||
|
return *value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EnvKvStore::parseInt(std::string_view name, const std::string &raw)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::size_t consumed = 0;
|
||||||
|
const long parsed = std::stol(raw, &consumed);
|
||||||
|
if (consumed != raw.size())
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("EnvKvStore: '")
|
||||||
|
+ std::string(name)
|
||||||
|
+ "' must be an integer, got: "
|
||||||
|
+ raw);
|
||||||
|
}
|
||||||
|
if (parsed < std::numeric_limits<int>::min()
|
||||||
|
|| parsed > std::numeric_limits<int>::max())
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("EnvKvStore: '")
|
||||||
|
+ std::string(name)
|
||||||
|
+ "' is out of int range, got: "
|
||||||
|
+ raw);
|
||||||
|
}
|
||||||
|
return static_cast<int>(parsed);
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (const std::exception &)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("EnvKvStore: failed to parse '")
|
||||||
|
+ std::string(name)
|
||||||
|
+ "' as an integer, got: "
|
||||||
|
+ raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int EnvKvStore::getIntWithConstraint(
|
||||||
|
std::string_view name,
|
||||||
|
std::optional<int> defaultValue,
|
||||||
|
IntConstraint constraint) const
|
||||||
|
{
|
||||||
|
const std::optional<std::string> raw = find(name);
|
||||||
|
if (!raw.has_value())
|
||||||
|
{
|
||||||
|
if (!defaultValue.has_value())
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("EnvKvStore: missing key '")
|
||||||
|
+ std::string(name)
|
||||||
|
+ "'");
|
||||||
|
}
|
||||||
|
return *defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int parsed = parseInt(name, *raw);
|
||||||
|
if (constraint == IntConstraint::NonNegative && parsed < 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("EnvKvStore: '")
|
||||||
|
+ std::string(name)
|
||||||
|
+ "' must be a non-negative integer, got: "
|
||||||
|
+ *raw);
|
||||||
|
}
|
||||||
|
if (constraint == IntConstraint::PositiveNonZero && parsed <= 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string("EnvKvStore: '")
|
||||||
|
+ std::string(name)
|
||||||
|
+ "' must be a positive non-zero integer, got: "
|
||||||
|
+ *raw);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnvKvStore::loadFile(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
std::ostream &warningStream)
|
||||||
|
{
|
||||||
|
std::ifstream file(envFilePath);
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Failed to open env file: " + envFilePath.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
std::size_t lineNumber = 0;
|
||||||
|
while (std::getline(file, line))
|
||||||
|
{
|
||||||
|
++lineNumber;
|
||||||
|
if (DotenvParser::lineIsBlankOrComment(line)) { continue; }
|
||||||
|
auto [name, value] =
|
||||||
|
DotenvParser::parseAssignment(envFilePath, lineNumber, line);
|
||||||
|
storeValue(envFilePath, name, value, warningStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnvKvStore::storeValue(
|
||||||
|
const std::filesystem::path &envFilePath,
|
||||||
|
const std::string &name,
|
||||||
|
const std::string &value,
|
||||||
|
std::ostream &warningStream)
|
||||||
|
{
|
||||||
|
if (auto oldValue = values.find(name); oldValue != values.end())
|
||||||
|
{
|
||||||
|
warningStream << "Warning: env file " << envFilePath
|
||||||
|
<< " overwrites " << name << " from `" << oldValue->second
|
||||||
|
<< "` to `" << value << "`.\n";
|
||||||
|
oldValue->second = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
values.emplace(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#include <spinscale/lockerAndInvokerBase.h>
|
|
||||||
|
|
||||||
namespace sscl {
|
|
||||||
|
|
||||||
} // namespace sscl
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#include <boostAsioLinkageFix.h>
|
||||||
|
|
||||||
|
#include <spinscale/multiOperationResultSet.h>
|
||||||
|
#include <spinscale/co/group.h>
|
||||||
|
|
||||||
|
namespace sscl {
|
||||||
|
|
||||||
|
MultiOperationResultSetWithException::MultiOperationResultSetWithException(
|
||||||
|
const co::Group &group)
|
||||||
|
{
|
||||||
|
unsigned int nSucceeded = 0;
|
||||||
|
unsigned int nFailed = 0;
|
||||||
|
using SettlementType = co::Group::SettlementDescriptor::TypeE;
|
||||||
|
|
||||||
|
for (const auto &desc : group.s.rsrc.settlements)
|
||||||
|
{
|
||||||
|
if (desc.type == SettlementType::EXCEPTION_THROWN) {
|
||||||
|
nFailed++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nSucceeded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = MultiOperationResultSet(
|
||||||
|
static_cast<unsigned int>(group.s.rsrc.settlements.size()),
|
||||||
|
nSucceeded,
|
||||||
|
nFailed);
|
||||||
|
|
||||||
|
if (nFailed > 0) {
|
||||||
|
memberFailureException = group.captureAggregatedGroupExceptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl
|
||||||
+102
-150
@@ -1,212 +1,164 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <spinscale/asynchronousContinuation.h>
|
#include <string_view>
|
||||||
#include <spinscale/asynchronousLoop.h>
|
#include <vector>
|
||||||
#include <spinscale/callback.h>
|
|
||||||
|
#include <spinscale/co/group.h>
|
||||||
#include <spinscale/puppetApplication.h>
|
#include <spinscale/puppetApplication.h>
|
||||||
#include <spinscale/componentThread.h>
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::string_view noPuppetThreadsToStartLogMessage =
|
||||||
|
"Mrntt: No puppet threads to start";
|
||||||
|
constexpr std::string_view noPuppetThreadsToPauseLogMessage =
|
||||||
|
"Mrntt: No puppet threads to pause";
|
||||||
|
constexpr std::string_view noPuppetThreadsToResumeLogMessage =
|
||||||
|
"Mrntt: No puppet threads to resume";
|
||||||
|
constexpr std::string_view noPuppetThreadsToExitLogMessage =
|
||||||
|
"Mrntt: No puppet threads to exit";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
PuppetApplication::PuppetApplication(
|
PuppetApplication::PuppetApplication(
|
||||||
const std::vector<std::shared_ptr<PuppetThread>> &threads)
|
const std::vector<std::shared_ptr<PuppetThread>> &threads)
|
||||||
: componentThreads(threads)
|
: componentThreads(threads)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
class PuppetApplication::PuppetThreadLifetimeMgmtOp
|
void PuppetApplication::addAllPuppetLifetimeInvokersToGroup(
|
||||||
: public NonPostedAsynchronousContinuation<puppetThreadLifetimeMgmtOpCbFn>
|
PuppetLifetimeMgmtGroup &group,
|
||||||
|
std::vector<PuppetLifetimeMgmtInvoker> &invokers,
|
||||||
|
PuppetThread::ThreadOp threadOp) const
|
||||||
{
|
{
|
||||||
public:
|
invokers.reserve(componentThreads.size());
|
||||||
PuppetThreadLifetimeMgmtOp(
|
|
||||||
PuppetApplication &parent, unsigned int nThreads,
|
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback)
|
|
||||||
: NonPostedAsynchronousContinuation<puppetThreadLifetimeMgmtOpCbFn>(callback),
|
|
||||||
loop(nThreads),
|
|
||||||
parent(parent)
|
|
||||||
{}
|
|
||||||
|
|
||||||
public:
|
for (const auto &thread : componentThreads)
|
||||||
AsynchronousLoop loop;
|
|
||||||
PuppetApplication &parent;
|
|
||||||
|
|
||||||
public:
|
|
||||||
void joltAllPuppetThreadsReq1(
|
|
||||||
[[maybe_unused]] std::shared_ptr<PuppetThreadLifetimeMgmtOp> context
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
loop.incrementSuccessOrFailureDueTo(true);
|
switch (threadOp)
|
||||||
if (!loop.isComplete()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.threadsHaveBeenJolted = true;
|
|
||||||
callOriginalCb();
|
|
||||||
}
|
|
||||||
|
|
||||||
void executeGenericOpOnAllPuppetThreadsReq1(
|
|
||||||
[[maybe_unused]] std::shared_ptr<PuppetThreadLifetimeMgmtOp> context
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
loop.incrementSuccessOrFailureDueTo(true);
|
case PuppetThread::ThreadOp::START:
|
||||||
if (!loop.isComplete()) {
|
invokers.emplace_back(thread->startThreadAReq());
|
||||||
return;
|
break;
|
||||||
|
case PuppetThread::ThreadOp::PAUSE:
|
||||||
|
invokers.emplace_back(thread->pauseThreadAReq());
|
||||||
|
break;
|
||||||
|
case PuppetThread::ThreadOp::RESUME:
|
||||||
|
invokers.emplace_back(thread->resumeThreadAReq());
|
||||||
|
break;
|
||||||
|
case PuppetThread::ThreadOp::EXIT:
|
||||||
|
invokers.emplace_back(thread->exitThreadAReq());
|
||||||
|
break;
|
||||||
|
case PuppetThread::ThreadOp::JOLT:
|
||||||
|
invokers.emplace_back(thread->joltThreadAReq(thread));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(__func__) + ": Invalid thread operation");
|
||||||
}
|
}
|
||||||
|
|
||||||
callOriginalCb();
|
group.add(invokers.back());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void exitAllPuppetThreadsReq1(
|
co::ViralNonPostingInvoker<void>
|
||||||
[[maybe_unused]] std::shared_ptr<PuppetThreadLifetimeMgmtOp> context
|
PuppetApplication::joltAllPuppetThreadsCReq()
|
||||||
)
|
|
||||||
{
|
|
||||||
loop.incrementSuccessOrFailureDueTo(true);
|
|
||||||
if (!loop.isComplete()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& thread : parent.componentThreads) {
|
|
||||||
thread->thread.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
callOriginalCb();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void PuppetApplication::joltAllPuppetThreadsReq(
|
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
if (threadsHaveBeenJolted)
|
if (threadsHaveBeenJolted)
|
||||||
{
|
{
|
||||||
std::cout << "Mrntt: All puppet threads already JOLTed. "
|
std::cout << "Mrntt: All puppet threads already JOLTed. "
|
||||||
<< "Skipping JOLT request." << "\n";
|
<< "Skipping JOLT request." << "\n";
|
||||||
callback.callbackFn();
|
co_return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no threads, set flag and call callback immediately
|
if (componentThreads.empty())
|
||||||
if (componentThreads.size() == 0 && callback.callbackFn)
|
|
||||||
{
|
{
|
||||||
threadsHaveBeenJolted = true;
|
threadsHaveBeenJolted = true;
|
||||||
callback.callbackFn();
|
co_return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a counter to track when all threads have been jolted
|
PuppetLifetimeMgmtGroup group;
|
||||||
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>(
|
std::vector<PuppetLifetimeMgmtInvoker> invokers;
|
||||||
*this, componentThreads.size(), callback);
|
|
||||||
|
|
||||||
for (auto& thread : componentThreads)
|
addAllPuppetLifetimeInvokersToGroup(
|
||||||
|
group, invokers, PuppetThread::ThreadOp::JOLT);
|
||||||
|
co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
group.checkForAndReThrowGroupExceptions();
|
||||||
|
|
||||||
|
threadsHaveBeenJolted = true;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
co::ViralNonPostingInvoker<void>
|
||||||
|
PuppetApplication::allPuppetThreadsLifetimeOpCReq(
|
||||||
|
PuppetThread::ThreadOp threadOp,
|
||||||
|
std::string_view emptyThreadsLogMessage)
|
||||||
{
|
{
|
||||||
thread->joltThreadReq(
|
if (componentThreads.empty())
|
||||||
thread,
|
{
|
||||||
{request, std::bind(
|
std::cout << emptyThreadsLogMessage << "\n";
|
||||||
&PuppetThreadLifetimeMgmtOp::joltAllPuppetThreadsReq1,
|
co_return;
|
||||||
request.get(), request)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetApplication::startAllPuppetThreadsReq(
|
PuppetLifetimeMgmtGroup group;
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback
|
std::vector<PuppetLifetimeMgmtInvoker> invokers;
|
||||||
)
|
|
||||||
{
|
addAllPuppetLifetimeInvokersToGroup(group, invokers, threadOp);
|
||||||
// If no threads, call callback immediately
|
co_await group.getAwaitAllSettlementsInvoker();
|
||||||
if (componentThreads.size() == 0 && callback.callbackFn)
|
group.checkForAndReThrowGroupExceptions();
|
||||||
{
|
|
||||||
callback.callbackFn();
|
co_return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a counter to track when all threads have started
|
co::ViralNonPostingInvoker<void>
|
||||||
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>(
|
PuppetApplication::startAllPuppetThreadsCReq()
|
||||||
*this, componentThreads.size(), callback);
|
|
||||||
|
|
||||||
for (auto& thread : componentThreads)
|
|
||||||
{
|
{
|
||||||
thread->startThreadReq(
|
return allPuppetThreadsLifetimeOpCReq(
|
||||||
{request, std::bind(
|
PuppetThread::ThreadOp::START,
|
||||||
&PuppetThreadLifetimeMgmtOp::executeGenericOpOnAllPuppetThreadsReq1,
|
noPuppetThreadsToStartLogMessage);
|
||||||
request.get(), request)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetApplication::pauseAllPuppetThreadsReq(
|
co::ViralNonPostingInvoker<void>
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback
|
PuppetApplication::pauseAllPuppetThreadsCReq()
|
||||||
)
|
|
||||||
{
|
{
|
||||||
// If no threads, call callback immediately
|
return allPuppetThreadsLifetimeOpCReq(
|
||||||
if (componentThreads.size() == 0 && callback.callbackFn)
|
PuppetThread::ThreadOp::PAUSE,
|
||||||
{
|
noPuppetThreadsToPauseLogMessage);
|
||||||
callback.callbackFn();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a counter to track when all threads have paused
|
co::ViralNonPostingInvoker<void>
|
||||||
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>(
|
PuppetApplication::resumeAllPuppetThreadsCReq()
|
||||||
*this, componentThreads.size(), callback);
|
|
||||||
|
|
||||||
for (auto& thread : componentThreads)
|
|
||||||
{
|
{
|
||||||
thread->pauseThreadReq(
|
return allPuppetThreadsLifetimeOpCReq(
|
||||||
{request, std::bind(
|
PuppetThread::ThreadOp::RESUME,
|
||||||
&PuppetThreadLifetimeMgmtOp::executeGenericOpOnAllPuppetThreadsReq1,
|
noPuppetThreadsToResumeLogMessage);
|
||||||
request.get(), request)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetApplication::resumeAllPuppetThreadsReq(
|
co::ViralNonPostingInvoker<void>
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback
|
PuppetApplication::exitAllPuppetThreadsCReq()
|
||||||
)
|
|
||||||
{
|
{
|
||||||
// If no threads, call callback immediately
|
if (componentThreads.empty())
|
||||||
if (componentThreads.size() == 0 && callback.callbackFn)
|
|
||||||
{
|
{
|
||||||
callback.callbackFn();
|
std::cout << noPuppetThreadsToExitLogMessage << "\n";
|
||||||
return;
|
co_return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a counter to track when all threads have resumed
|
co_await allPuppetThreadsLifetimeOpCReq(
|
||||||
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>(
|
PuppetThread::ThreadOp::EXIT,
|
||||||
*this, componentThreads.size(), callback);
|
noPuppetThreadsToExitLogMessage);
|
||||||
|
|
||||||
for (auto& thread : componentThreads)
|
for (auto &thread : componentThreads) {
|
||||||
{
|
thread->thread.join();
|
||||||
thread->resumeThreadReq(
|
|
||||||
{request, std::bind(
|
|
||||||
&PuppetThreadLifetimeMgmtOp::executeGenericOpOnAllPuppetThreadsReq1,
|
|
||||||
request.get(), request)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetApplication::exitAllPuppetThreadsReq(
|
co_return;
|
||||||
Callback<puppetThreadLifetimeMgmtOpCbFn> callback
|
|
||||||
)
|
|
||||||
{
|
|
||||||
// If no threads, call callback immediately
|
|
||||||
if (componentThreads.size() == 0 && callback.callbackFn)
|
|
||||||
{
|
|
||||||
callback.callbackFn();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a counter to track when all threads have exited
|
|
||||||
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>(
|
|
||||||
*this, componentThreads.size(), callback);
|
|
||||||
|
|
||||||
for (auto& thread : componentThreads)
|
|
||||||
{
|
|
||||||
thread->exitThreadReq(
|
|
||||||
{request, std::bind(
|
|
||||||
&PuppetThreadLifetimeMgmtOp::exitAllPuppetThreadsReq1,
|
|
||||||
request.get(), request)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PuppetApplication::distributeAndPinThreadsAcrossCpus()
|
void PuppetApplication::distributeAndPinThreadsAcrossCpus()
|
||||||
{
|
{
|
||||||
int cpuCount = ComponentThread::getAvailableCpuCount();
|
int cpuCount = ComponentThread::getAvailableCpuCount();
|
||||||
|
|
||||||
// Distribute and pin threads across CPUs
|
|
||||||
int threadIndex = 0;
|
int threadIndex = 0;
|
||||||
for (auto& thread : componentThreads)
|
for (auto& thread : componentThreads)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ void PuppeteerComponent::defaultPuppeteerMain(
|
|||||||
|
|
||||||
if (args.preJoltHook) { args.preJoltHook(thr); }
|
if (args.preJoltHook) { args.preJoltHook(thr); }
|
||||||
|
|
||||||
thr.getIoService().reset();
|
thr.getIoContext().restart();
|
||||||
thr.getIoService().run();
|
thr.getIoContext().run();
|
||||||
thr.initializeTls();
|
thr.initializeTls();
|
||||||
|
|
||||||
comp.postJoltHook();
|
comp.postJoltHook();
|
||||||
@@ -40,17 +40,17 @@ void PuppeteerComponent::defaultPuppeteerMain(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
/** EXPLANATION:
|
/** EXPLANATION:
|
||||||
* This reset() call is crucial for async bridging
|
* This restart() call is crucial for async bridging
|
||||||
* patterns to work.
|
* patterns to work.
|
||||||
* When the outermost thread's io_service is stop()ped
|
* When the outermost thread's io_context is stop()ped
|
||||||
* (e.g., from JOLT sequence), it won't process any new
|
* (e.g., from JOLT sequence), it won't process any new
|
||||||
* work until reset() is called, even if nested async
|
* work until restart() is called, even if nested async
|
||||||
* operations try to post work to it. This means async
|
* operations try to post work to it. This means async
|
||||||
* bridges invoked from the outermost thread main sequence
|
* bridges invoked from the outermost thread main sequence
|
||||||
* won't work until this reset() call.
|
* won't work until this restart() call.
|
||||||
*/
|
*/
|
||||||
thr.getIoService().reset();
|
thr.getIoContext().restart();
|
||||||
thr.getIoService().run();
|
thr.getIoContext().run();
|
||||||
}
|
}
|
||||||
catch (const std::exception& e)
|
catch (const std::exception& e)
|
||||||
{
|
{
|
||||||
|
|||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
#include <spinscale/qutex.h>
|
#include <spinscale/cps/qutex.h>
|
||||||
#include <spinscale/lockerAndInvokerBase.h>
|
#include <spinscale/cps/lockerAndInvokerBase.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
bool Qutex::tryAcquire(
|
bool Qutex::tryAcquire(
|
||||||
const LockerAndInvokerBase &tryingLockvoker, int nRequiredLocks
|
const LockerAndInvokerBase &tryingLockvoker, int nRequiredLocks
|
||||||
@@ -288,8 +288,8 @@ void Qutex::backoff(
|
|||||||
* (Assume that Lv2 was not at the front of the common qutex's
|
* (Assume that Lv2 was not at the front of the common qutex's
|
||||||
* internal queue -- it only needed to be in the top 66%.)
|
* internal queue -- it only needed to be in the top 66%.)
|
||||||
* Lv1 tries to acquire the common lock and fails. It gets taken off of
|
* Lv1 tries to acquire the common lock and fails. It gets taken off of
|
||||||
* its io_service. It's now asleep until it gets
|
* its io_context. It's now asleep until it gets
|
||||||
* re-added into an io_service.
|
* re-added into an io_context.
|
||||||
* Lv2 fails to acquire the other 2 locks it needs and backoff()s from
|
* Lv2 fails to acquire the other 2 locks it needs and backoff()s from
|
||||||
* the common lock it shares with Lv1.
|
* the common lock it shares with Lv1.
|
||||||
*
|
*
|
||||||
@@ -357,7 +357,7 @@ void Qutex::release()
|
|||||||
* Just before Lv1 can acquire the common lock, Lv2 acquires it now,
|
* Just before Lv1 can acquire the common lock, Lv2 acquires it now,
|
||||||
* because it only needs to be in the top 66% to succeed.
|
* because it only needs to be in the top 66% to succeed.
|
||||||
* Lv1 checks the currOwner and sees that it's owned. Lv1 is now
|
* Lv1 checks the currOwner and sees that it's owned. Lv1 is now
|
||||||
* dequeued from its io_service. It won't be awakened until someone
|
* dequeued from its io_context. It won't be awakened until someone
|
||||||
* awakens it.
|
* awakens it.
|
||||||
* Lv2 finishes its critical section and releas()es the common lock.
|
* Lv2 finishes its critical section and releas()es the common lock.
|
||||||
* Lv2 was not at the front of the qutexQ, so it does NOT awaken the
|
* Lv2 was not at the front of the qutexQ, so it does NOT awaken the
|
||||||
@@ -377,4 +377,4 @@ void Qutex::release()
|
|||||||
front->awaken();
|
front->awaken();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#include <spinscale/qutexAcquisitionHistoryTracker.h>
|
#include <spinscale/cps/qutexAcquisitionHistoryTracker.h>
|
||||||
#include <spinscale/serializedAsynchronousContinuation.h>
|
#include <spinscale/cps/serializedAsynchronousContinuation.h>
|
||||||
#include <spinscale/qutex.h>
|
#include <spinscale/cps/qutex.h>
|
||||||
#include <spinscale/dependencyGraph.h>
|
#include <spinscale/cps/dependencyGraph.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <forward_list>
|
#include <forward_list>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl::cps {
|
||||||
|
|
||||||
void DependencyGraph::addNode(const Node& node)
|
void DependencyGraph::addNode(const Node& node)
|
||||||
{
|
{
|
||||||
@@ -390,4 +390,4 @@ bool QutexAcquisitionHistoryTracker
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sscl
|
} // namespace sscl::cps
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
add_library(spinscale_test_support STATIC
|
||||||
|
support/threadHarness.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(spinscale_test_support PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/fixtures
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(spinscale_test_support PUBLIC
|
||||||
|
spinscale
|
||||||
|
spinscale_probe_support
|
||||||
|
gtest
|
||||||
|
)
|
||||||
|
|
||||||
|
function(add_spinscale_gtest target)
|
||||||
|
add_executable(${target} ${ARGN})
|
||||||
|
target_link_libraries(${target} PRIVATE
|
||||||
|
spinscale_test_support
|
||||||
|
gtest_main
|
||||||
|
)
|
||||||
|
add_dependencies(${target} gtest_main)
|
||||||
|
add_test(NAME ${target} COMMAND ${target})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
add_spinscale_gtest(spinscale_env_kv_store_tests
|
||||||
|
env_kv_store_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_spinscale_gtest(qutex_tests
|
||||||
|
cps/qutex_tests.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_spinscale_gtest(nonViralTaskNursery_tests
|
||||||
|
co/nonViralTaskNursery_tests.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_spinscale_gtest(co_viral_non_posting_tests
|
||||||
|
co/viral_non_posting_tests.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_spinscale_gtest(co_posting_cross_thread_tests
|
||||||
|
co/posting_cross_thread_tests.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_spinscale_gtest(co_group_edge_tests
|
||||||
|
co/group_edge_tests.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_spinscale_gtest(co_group_timer_tests
|
||||||
|
co/group_timer_tests.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_spinscale_gtest(co_component_continuation_tests
|
||||||
|
co/component_continuation_tests.cpp
|
||||||
|
)
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <spinscale/co/coQutex.h>
|
||||||
|
|
||||||
|
#include <support/threadHarness.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int leftValue = 1;
|
||||||
|
constexpr int rightValue = 2;
|
||||||
|
constexpr int expectedIntSum = 3;
|
||||||
|
constexpr int bodyArgument = 4;
|
||||||
|
constexpr const char *bodyStringArgument = "KEKW";
|
||||||
|
constexpr const char *leftString = "Hello";
|
||||||
|
constexpr const char *rightString = "World";
|
||||||
|
constexpr const char *expectedString = "Hello World";
|
||||||
|
|
||||||
|
using BodyNonViralInvoker =
|
||||||
|
sscl::tests::RoleNonViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::BODY>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using BodyViralInvoker =
|
||||||
|
sscl::tests::RoleViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::BODY,
|
||||||
|
T>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using WorldViralInvoker =
|
||||||
|
sscl::tests::RoleViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::WORLD,
|
||||||
|
T>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using LegViralInvoker =
|
||||||
|
sscl::tests::RoleViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::LEG,
|
||||||
|
T>;
|
||||||
|
|
||||||
|
class ComponentContinuationTrace
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void recordBodyThread()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
bodyThreadId = std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordWorldThread()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
worldThreadId = std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordLegThread()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
legThreadId = std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordCompletionThread()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
completionThreadId = std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordLegSum(int value)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
legSum = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordWorldString(std::string value)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
worldString = std::move(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordBodyString(std::string value)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
bodyString = std::move(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id bodyThread() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return bodyThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id worldThread() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return worldThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id legThread() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return legThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id completionThread() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return completionThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int recordedLegSum() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return legSum;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string recordedWorldString() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return worldString;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string recordedBodyString() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return bodyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::mutex mutex;
|
||||||
|
std::thread::id bodyThreadId;
|
||||||
|
std::thread::id worldThreadId;
|
||||||
|
std::thread::id legThreadId;
|
||||||
|
std::thread::id completionThreadId;
|
||||||
|
int legSum = 0;
|
||||||
|
std::string worldString;
|
||||||
|
std::string bodyString;
|
||||||
|
};
|
||||||
|
|
||||||
|
LegViralInvoker<int> print2Ints(
|
||||||
|
int arg1,
|
||||||
|
int arg2,
|
||||||
|
ComponentContinuationTrace &trace)
|
||||||
|
{
|
||||||
|
sscl::co::CoQutex print2IntsLock;
|
||||||
|
trace.recordLegThread();
|
||||||
|
auto releaseHandle =
|
||||||
|
co_await print2IntsLock.getAcquireInvocationAndSuspensionPolicy();
|
||||||
|
const int sum = arg1 + arg2;
|
||||||
|
trace.recordLegSum(sum);
|
||||||
|
releaseHandle.release();
|
||||||
|
co_return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldViralInvoker<std::string> print2Strings(
|
||||||
|
std::string arg1,
|
||||||
|
std::string arg2,
|
||||||
|
ComponentContinuationTrace &trace)
|
||||||
|
{
|
||||||
|
sscl::co::CoQutex print2StringsLock;
|
||||||
|
trace.recordWorldThread();
|
||||||
|
auto releaseHandle =
|
||||||
|
co_await print2StringsLock.getAcquireInvocationAndSuspensionPolicy();
|
||||||
|
const int returnedInt =
|
||||||
|
co_await print2Ints(leftValue, rightValue, trace);
|
||||||
|
releaseHandle.release();
|
||||||
|
|
||||||
|
if (returnedInt != expectedIntSum) {
|
||||||
|
throw std::runtime_error("LEG int return mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string returnedString = arg1 + " " + arg2;
|
||||||
|
trace.recordWorldString(returnedString);
|
||||||
|
co_return returnedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
BodyNonViralInvoker initializeDemoCReq(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
int arg3,
|
||||||
|
std::string arg4,
|
||||||
|
ComponentContinuationTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
(void)arg3;
|
||||||
|
(void)arg4;
|
||||||
|
|
||||||
|
sscl::co::CoQutex initializeLock;
|
||||||
|
trace.recordBodyThread();
|
||||||
|
auto releaseHandle =
|
||||||
|
co_await initializeLock.getAcquireInvocationAndSuspensionPolicy();
|
||||||
|
std::string returnedString =
|
||||||
|
co_await print2Strings(leftString, rightString, trace);
|
||||||
|
releaseHandle.release();
|
||||||
|
|
||||||
|
trace.recordBodyString(returnedString);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComponentContinuationTest
|
||||||
|
: public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
sscl::tests::PostingThreadSet threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(ComponentContinuationTest, SyncMainStyleContinuationCrossesComponentThreads)
|
||||||
|
{
|
||||||
|
ComponentContinuationTrace trace;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[&trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
return initializeDemoCReq(
|
||||||
|
exceptionPtr,
|
||||||
|
[&trace, completion = std::move(completion)]() mutable
|
||||||
|
{
|
||||||
|
trace.recordCompletionThread();
|
||||||
|
completion();
|
||||||
|
},
|
||||||
|
bodyArgument,
|
||||||
|
bodyStringArgument,
|
||||||
|
trace);
|
||||||
|
}));
|
||||||
|
|
||||||
|
EXPECT_EQ(trace.bodyThread(), threads.body().osThreadId());
|
||||||
|
EXPECT_EQ(trace.worldThread(), threads.world().osThreadId());
|
||||||
|
EXPECT_EQ(trace.legThread(), threads.leg().osThreadId());
|
||||||
|
EXPECT_EQ(trace.completionThread(), threads.caller().osThreadId());
|
||||||
|
|
||||||
|
EXPECT_NE(trace.bodyThread(), trace.worldThread());
|
||||||
|
EXPECT_NE(trace.worldThread(), trace.legThread());
|
||||||
|
EXPECT_NE(trace.legThread(), trace.completionThread());
|
||||||
|
|
||||||
|
EXPECT_EQ(trace.recordedLegSum(), expectedIntSum);
|
||||||
|
EXPECT_EQ(trace.recordedWorldString(), expectedString);
|
||||||
|
EXPECT_EQ(trace.recordedBodyString(), expectedString);
|
||||||
|
}
|
||||||
@@ -0,0 +1,864 @@
|
|||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/co/group.h>
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
|
#include <support/groupAssertions.h>
|
||||||
|
#include <support/threadHarness.h>
|
||||||
|
#include <support/timerAwaiters.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int delayShortMs = 50;
|
||||||
|
constexpr int delayMediumMs = 200;
|
||||||
|
constexpr int delayLongMs = 500;
|
||||||
|
constexpr int delayAddWhileSuspendedProbeMs = 80;
|
||||||
|
constexpr int expectedNonStdThrowValue = 42;
|
||||||
|
constexpr int wave2ImmediateSettlementLabel = 1000;
|
||||||
|
constexpr const char *expectedThrowMessage =
|
||||||
|
"group_edge_test intentional failure";
|
||||||
|
|
||||||
|
using CallerDriver =
|
||||||
|
sscl::tests::RoleNonViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLER>;
|
||||||
|
|
||||||
|
using CalleeIntInvoker =
|
||||||
|
sscl::tests::RoleViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLEE,
|
||||||
|
int>;
|
||||||
|
|
||||||
|
using CalleeVoidInvoker =
|
||||||
|
sscl::tests::RoleViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLEE,
|
||||||
|
void>;
|
||||||
|
|
||||||
|
CalleeIntInvoker waitAndReturnLabel(int timerLabelMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
timerLabelMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
co_return timerLabelMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeIntInvoker waitThenThrowAfterDelay(int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
delayMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
throw std::runtime_error(expectedThrowMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeIntInvoker waitThenThrowIntAfterDelay(int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
delayMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
throw expectedNonStdThrowValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeIntInvoker returnLabelImmediately(int label)
|
||||||
|
{
|
||||||
|
co_return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeVoidInvoker voidMemberAfterDelay(int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
delayMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeIntInvoker waitRecordThreadAndReturnLabel(
|
||||||
|
int timerLabelMilliseconds,
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
timerLabelMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
trace.recordCalleeExecutionThread();
|
||||||
|
co_return timerLabelMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::co::ViralNonPostingInvoker<void> waitOnCallerThread(int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
delayMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver mixedSuccessAndFailureAwaitFirstThenAll(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker successInvoker = waitAndReturnLabel(1);
|
||||||
|
CalleeIntInvoker failureInvoker = waitThenThrowAfterDelay(delayShortMs);
|
||||||
|
|
||||||
|
group.add(successInvoker);
|
||||||
|
group.add(failureInvoker);
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptor, allAfterFirst] = co_await awaitFirst;
|
||||||
|
|
||||||
|
if (firstDescriptor.type
|
||||||
|
== sscl::co::Group::SettlementDescriptor::TypeE::COMPLETED) {
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptor,
|
||||||
|
1);
|
||||||
|
}
|
||||||
|
else if (firstDescriptor.type
|
||||||
|
== sscl::co::Group::SettlementDescriptor::TypeE::EXCEPTION_THROWN) {
|
||||||
|
sscl::tests::requireRuntimeErrorSettlement(
|
||||||
|
firstDescriptor,
|
||||||
|
expectedThrowMessage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error("first settlement has unexpected type");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
auto &allDescriptors = co_await awaitAll;
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 2 || allAfterFirst.size() != 2) {
|
||||||
|
throw std::runtime_error("mixed settlement count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t completedCount = 0;
|
||||||
|
std::size_t exceptionCount = 0;
|
||||||
|
|
||||||
|
for (auto &descriptor : allDescriptors) {
|
||||||
|
if (descriptor.type
|
||||||
|
== sscl::co::Group::SettlementDescriptor::TypeE::COMPLETED) {
|
||||||
|
++completedCount;
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
descriptor,
|
||||||
|
1);
|
||||||
|
}
|
||||||
|
else if (descriptor.type
|
||||||
|
== sscl::co::Group::SettlementDescriptor::TypeE::EXCEPTION_THROWN) {
|
||||||
|
++exceptionCount;
|
||||||
|
sscl::tests::requireRuntimeErrorSettlement(
|
||||||
|
descriptor,
|
||||||
|
expectedThrowMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completedCount != 1 || exceptionCount != 1) {
|
||||||
|
throw std::runtime_error("mixed settlement type counts mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver singleMemberAwaitFirstThenAll(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker onlyInvoker = waitAndReturnLabel(delayShortMs);
|
||||||
|
group.add(onlyInvoker);
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptor, allAfterFirst] = co_await awaitFirst;
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptor,
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
if (!group.allInvokersSettled() || allAfterFirst.size() != 1) {
|
||||||
|
throw std::runtime_error("single member state mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
auto &allDescriptors = co_await awaitAll;
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 1) {
|
||||||
|
throw std::runtime_error("single member await-all count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allDescriptors[0],
|
||||||
|
delayShortMs);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver allCompleteBeforeCoAwait(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker invokerTen = returnLabelImmediately(10);
|
||||||
|
CalleeIntInvoker invokerTwenty = returnLabelImmediately(20);
|
||||||
|
CalleeIntInvoker invokerThirty = returnLabelImmediately(30);
|
||||||
|
|
||||||
|
group.add(invokerTen);
|
||||||
|
group.add(invokerTwenty);
|
||||||
|
group.add(invokerThirty);
|
||||||
|
|
||||||
|
co_await waitOnCallerThread(delayShortMs);
|
||||||
|
|
||||||
|
if (!group.allInvokersSettled() || !group.firstInvokerSettled()) {
|
||||||
|
throw std::runtime_error("immediate group did not settle before await");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptor, allAfterFirst] = co_await awaitFirst;
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptor,
|
||||||
|
10);
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
auto &allDescriptors = co_await awaitAll;
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 3 || allAfterFirst.size() != 3) {
|
||||||
|
throw std::runtime_error("immediate settlement count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::jthread startAddWhileGroupAwaiterSuspendedProbe(
|
||||||
|
sscl::co::Group &group,
|
||||||
|
CalleeIntInvoker &lateInvoker,
|
||||||
|
std::atomic<bool> &groupIsAwaitingAll,
|
||||||
|
std::atomic<bool> &addWasRejected)
|
||||||
|
{
|
||||||
|
return std::jthread(
|
||||||
|
[&]()
|
||||||
|
{
|
||||||
|
while (!groupIsAwaitingAll.load(std::memory_order_acquire)) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(
|
||||||
|
std::chrono::milliseconds(delayAddWhileSuspendedProbeMs));
|
||||||
|
|
||||||
|
boost::asio::post(
|
||||||
|
sscl::tests::ThreadRegistry::ioContext(
|
||||||
|
sscl::tests::PostingThreadRole::CALLER),
|
||||||
|
[&]()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
group.add(lateInvoker);
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &) {
|
||||||
|
addWasRejected.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver addWhileAwaitAllSuspended(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
std::atomic<bool> groupIsAwaitingAll{false};
|
||||||
|
std::atomic<bool> addWasRejected{false};
|
||||||
|
|
||||||
|
CalleeIntInvoker slowInvokerA = waitAndReturnLabel(delayLongMs);
|
||||||
|
CalleeIntInvoker slowInvokerB = waitAndReturnLabel(delayLongMs);
|
||||||
|
CalleeIntInvoker lateInvoker = waitAndReturnLabel(99);
|
||||||
|
|
||||||
|
group.add(slowInvokerA);
|
||||||
|
group.add(slowInvokerB);
|
||||||
|
|
||||||
|
std::jthread addProbeThread = startAddWhileGroupAwaiterSuspendedProbe(
|
||||||
|
group,
|
||||||
|
lateInvoker,
|
||||||
|
groupIsAwaitingAll,
|
||||||
|
addWasRejected);
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
groupIsAwaitingAll.store(true, std::memory_order_release);
|
||||||
|
co_await awaitAll;
|
||||||
|
|
||||||
|
addProbeThread.join();
|
||||||
|
|
||||||
|
if (!addWasRejected.load(std::memory_order_acquire)) {
|
||||||
|
throw std::runtime_error("expected add while suspended to throw");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver awaitAllOnlyMixedOutcomes(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker successInvoker = returnLabelImmediately(7);
|
||||||
|
CalleeIntInvoker failureInvoker = waitThenThrowAfterDelay(delayShortMs);
|
||||||
|
|
||||||
|
group.add(successInvoker);
|
||||||
|
group.add(failureInvoker);
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
auto &allDescriptors = co_await awaitAll;
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 2) {
|
||||||
|
throw std::runtime_error("await-all-only count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t completedCount = 0;
|
||||||
|
std::size_t exceptionCount = 0;
|
||||||
|
|
||||||
|
for (auto &descriptor : allDescriptors) {
|
||||||
|
if (descriptor.type
|
||||||
|
== sscl::co::Group::SettlementDescriptor::TypeE::COMPLETED) {
|
||||||
|
++completedCount;
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
descriptor,
|
||||||
|
7);
|
||||||
|
}
|
||||||
|
else if (descriptor.type
|
||||||
|
== sscl::co::Group::SettlementDescriptor::TypeE::EXCEPTION_THROWN) {
|
||||||
|
++exceptionCount;
|
||||||
|
sscl::tests::requireRuntimeErrorSettlement(
|
||||||
|
descriptor,
|
||||||
|
expectedThrowMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completedCount != 1 || exceptionCount != 1) {
|
||||||
|
throw std::runtime_error("await-all-only mixed counts mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver checkForAndReThrowGroupExceptions(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker failureInvoker = waitThenThrowAfterDelay(delayShortMs);
|
||||||
|
group.add(failureInvoker);
|
||||||
|
|
||||||
|
(void)co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
try {
|
||||||
|
group.checkForAndReThrowGroupExceptions();
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &aggregateError) {
|
||||||
|
if (std::string(aggregateError.what()).find(expectedThrowMessage)
|
||||||
|
== std::string::npos) {
|
||||||
|
throw std::runtime_error("aggregate message missing callee text");
|
||||||
|
}
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error("expected aggregate group exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver emptyGroupAwaitAllThrows(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
|
||||||
|
try {
|
||||||
|
(void)co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &runtimeError) {
|
||||||
|
sscl::tests::requireEmptyGroupError(runtimeError);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error("expected empty group await-all to throw");
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver emptyGroupAwaitFirstThrows(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
|
||||||
|
try {
|
||||||
|
(void)co_await group.getAwaitFirstSettlementInvoker();
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &runtimeError) {
|
||||||
|
sscl::tests::requireEmptyGroupError(runtimeError);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error("expected empty group await-first to throw");
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver wrongAwaitInvokerOrder(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker shortInvoker = waitAndReturnLabel(delayShortMs);
|
||||||
|
CalleeIntInvoker mediumInvoker = waitAndReturnLabel(delayMediumMs);
|
||||||
|
|
||||||
|
group.add(shortInvoker);
|
||||||
|
group.add(mediumInvoker);
|
||||||
|
|
||||||
|
auto awaitFirstHandle = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto awaitAllHandle = group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
auto &allDescriptors = co_await awaitAllHandle;
|
||||||
|
if (allDescriptors.size() != 2) {
|
||||||
|
throw std::runtime_error("wrong-order await-all count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [firstDescriptor, allAfterFirst] = co_await awaitFirstHandle;
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptor,
|
||||||
|
sscl::tests::completedIntValue(
|
||||||
|
firstDescriptor.invokerAs<CalleeIntInvoker>()));
|
||||||
|
|
||||||
|
if (!group.firstInvokerSettled() || allAfterFirst.size() != 2) {
|
||||||
|
throw std::runtime_error("wrong-order await-first state mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver doubleCoAwaitSameAwaitFirst(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker memberInvoker = returnLabelImmediately(delayShortMs);
|
||||||
|
group.add(memberInvoker);
|
||||||
|
|
||||||
|
co_await waitOnCallerThread(delayShortMs);
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptorA, allAfterFirstA] = co_await awaitFirst;
|
||||||
|
auto [firstDescriptorB, allAfterFirstB] = co_await awaitFirst;
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptorA,
|
||||||
|
delayShortMs);
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptorB,
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
if (&firstDescriptorA.invokerAs<CalleeIntInvoker>()
|
||||||
|
!= &firstDescriptorB.invokerAs<CalleeIntInvoker>()) {
|
||||||
|
throw std::runtime_error("double await-first descriptor mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allAfterFirstA.size() != allAfterFirstB.size()) {
|
||||||
|
throw std::runtime_error("double await-first snapshot mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver doubleCoAwaitSameAwaitAll(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker memberInvoker = waitAndReturnLabel(delayShortMs);
|
||||||
|
group.add(memberInvoker);
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
auto &allDescriptorsA = co_await awaitAll;
|
||||||
|
auto &allDescriptorsB = co_await awaitAll;
|
||||||
|
|
||||||
|
if (allDescriptorsA.size() != 1 || allDescriptorsB.size() != 1) {
|
||||||
|
throw std::runtime_error("double await-all count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allDescriptorsA[0],
|
||||||
|
delayShortMs);
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allDescriptorsB[0],
|
||||||
|
delayShortMs);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver twoAwaitFirstHandlesSequentially(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker shortInvoker = waitAndReturnLabel(delayShortMs);
|
||||||
|
CalleeIntInvoker mediumInvoker = waitAndReturnLabel(delayMediumMs);
|
||||||
|
|
||||||
|
group.add(shortInvoker);
|
||||||
|
group.add(mediumInvoker);
|
||||||
|
|
||||||
|
auto awaitFirstA = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptorA, allAfterFirstA] = co_await awaitFirstA;
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptorA,
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
auto awaitFirstB = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptorB, allAfterFirstB] = co_await awaitFirstB;
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptorB,
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
if (&firstDescriptorA.invokerAs<CalleeIntInvoker>()
|
||||||
|
!= &firstDescriptorB.invokerAs<CalleeIntInvoker>()) {
|
||||||
|
throw std::runtime_error("sticky first settlement mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
(void)allAfterFirstA;
|
||||||
|
(void)allAfterFirstB;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver addSecondWaveAfterAwaitAll(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker wave1MemberA = waitAndReturnLabel(delayLongMs);
|
||||||
|
CalleeIntInvoker wave1MemberB = waitAndReturnLabel(delayLongMs);
|
||||||
|
|
||||||
|
group.add(wave1MemberA);
|
||||||
|
group.add(wave1MemberB);
|
||||||
|
(void)co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
CalleeIntInvoker wave2Immediate =
|
||||||
|
returnLabelImmediately(wave2ImmediateSettlementLabel);
|
||||||
|
CalleeIntInvoker wave2Slow = waitAndReturnLabel(delayMediumMs);
|
||||||
|
|
||||||
|
group.add(wave2Immediate);
|
||||||
|
group.add(wave2Slow);
|
||||||
|
|
||||||
|
co_await waitOnCallerThread(delayShortMs);
|
||||||
|
|
||||||
|
if (sscl::tests::completedIntValue(wave2Immediate)
|
||||||
|
!= wave2ImmediateSettlementLabel) {
|
||||||
|
throw std::runtime_error("wave-2 immediate member did not complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.allInvokersSettled()) {
|
||||||
|
throw std::runtime_error("wave-2 slow member should still be in flight");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &allDescriptors =
|
||||||
|
co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 4) {
|
||||||
|
throw std::runtime_error("expected four settlements after second wave");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver shortTimerAddedAfterLongStillWinsRace(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker longInvoker = waitAndReturnLabel(delayLongMs);
|
||||||
|
CalleeIntInvoker shortInvoker = waitAndReturnLabel(delayShortMs);
|
||||||
|
|
||||||
|
group.add(longInvoker);
|
||||||
|
group.add(shortInvoker);
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptor, allAfterFirst] = co_await awaitFirst;
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptor,
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
if (&firstDescriptor.invokerAs<CalleeIntInvoker>() != &shortInvoker) {
|
||||||
|
throw std::runtime_error("short timer should win first settlement");
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
(void)allAfterFirst;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver nonStdExceptionSettlement(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker failureInvoker = waitThenThrowIntAfterDelay(delayShortMs);
|
||||||
|
group.add(failureInvoker);
|
||||||
|
|
||||||
|
auto &allDescriptors = co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 1) {
|
||||||
|
throw std::runtime_error("non-std exception count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::requireIntExceptionSettlement(
|
||||||
|
allDescriptors[0],
|
||||||
|
expectedNonStdThrowValue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
group.checkForAndReThrowGroupExceptions();
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error("expected aggregate for non-std exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver voidViralMemberInGroup(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeVoidInvoker voidInvoker = voidMemberAfterDelay(delayShortMs);
|
||||||
|
group.add(voidInvoker);
|
||||||
|
|
||||||
|
auto &allDescriptors = co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 1) {
|
||||||
|
throw std::runtime_error("void group count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allDescriptors[0].type
|
||||||
|
!= sscl::co::Group::SettlementDescriptor::TypeE::COMPLETED) {
|
||||||
|
throw std::runtime_error("void member did not complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver returnValuesRemainReadableAfterAwaitFirst(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker slowInvoker = waitAndReturnLabel(delayLongMs);
|
||||||
|
CalleeIntInvoker fastInvoker = waitAndReturnLabel(delayShortMs);
|
||||||
|
|
||||||
|
group.add(slowInvoker);
|
||||||
|
group.add(fastInvoker);
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptor, allAfterFirst] = co_await awaitFirst;
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptor,
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
const int fastLabelFromDescriptor = sscl::tests::completedIntValue(
|
||||||
|
firstDescriptor.invokerAs<CalleeIntInvoker>());
|
||||||
|
const int fastLabelFromLocal =
|
||||||
|
sscl::tests::completedIntValue(fastInvoker);
|
||||||
|
|
||||||
|
if (fastLabelFromDescriptor != fastLabelFromLocal) {
|
||||||
|
throw std::runtime_error("descriptor/local return value mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allAfterFirst.size() != 2) {
|
||||||
|
throw std::runtime_error("expected two settlement slots");
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver groupMemberRunsOnCalleeAndAwaitResumesOnCaller(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker memberInvoker = waitRecordThreadAndReturnLabel(
|
||||||
|
delayShortMs,
|
||||||
|
trace);
|
||||||
|
group.add(memberInvoker);
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstDescriptor, allAfterFirst] = co_await awaitFirst;
|
||||||
|
trace.recordAwaitResumeThread();
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstDescriptor,
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
if (allAfterFirst.size() != 1) {
|
||||||
|
throw std::runtime_error("cross-thread group trace count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupEdgeTest
|
||||||
|
: public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
template <typename Factory>
|
||||||
|
void runScenario(Factory &&factory)
|
||||||
|
{
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
std::forward<Factory>(factory)));
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::PostingThreadSet threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#define RUN_GROUP_EDGE_SCENARIO(testName, functionName) \
|
||||||
|
TEST_F(GroupEdgeTest, testName) \
|
||||||
|
{ \
|
||||||
|
runScenario( \
|
||||||
|
[]( \
|
||||||
|
std::exception_ptr &exceptionPtr, \
|
||||||
|
std::function<void()> completion) \
|
||||||
|
{ \
|
||||||
|
return functionName(exceptionPtr, std::move(completion)); \
|
||||||
|
}); \
|
||||||
|
}
|
||||||
|
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(
|
||||||
|
MixedSuccessAndFailureAwaitFirstThenAll,
|
||||||
|
mixedSuccessAndFailureAwaitFirstThenAll)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(
|
||||||
|
SingleMemberAwaitFirstThenAll,
|
||||||
|
singleMemberAwaitFirstThenAll)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(AllCompleteBeforeCoAwait, allCompleteBeforeCoAwait)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(AddWhileAwaitAllSuspended, addWhileAwaitAllSuspended)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(AwaitAllOnlyMixedOutcomes, awaitAllOnlyMixedOutcomes)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(
|
||||||
|
CheckForAndReThrowGroupExceptions,
|
||||||
|
checkForAndReThrowGroupExceptions)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(EmptyGroupAwaitAllThrows, emptyGroupAwaitAllThrows)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(EmptyGroupAwaitFirstThrows, emptyGroupAwaitFirstThrows)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(WrongAwaitInvokerOrder, wrongAwaitInvokerOrder)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(DoubleCoAwaitSameAwaitFirst, doubleCoAwaitSameAwaitFirst)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(DoubleCoAwaitSameAwaitAll, doubleCoAwaitSameAwaitAll)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(
|
||||||
|
TwoAwaitFirstHandlesSequentially,
|
||||||
|
twoAwaitFirstHandlesSequentially)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(AddSecondWaveAfterAwaitAll, addSecondWaveAfterAwaitAll)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(
|
||||||
|
ShortTimerAddedAfterLongStillWinsRace,
|
||||||
|
shortTimerAddedAfterLongStillWinsRace)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(NonStdExceptionSettlement, nonStdExceptionSettlement)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(VoidViralMemberInGroup, voidViralMemberInGroup)
|
||||||
|
RUN_GROUP_EDGE_SCENARIO(
|
||||||
|
ReturnValuesRemainReadableAfterAwaitFirst,
|
||||||
|
returnValuesRemainReadableAfterAwaitFirst)
|
||||||
|
|
||||||
|
TEST_F(GroupEdgeTest, SuspendingMemberRunsOnCalleeAndAwaitResumesOnCaller)
|
||||||
|
{
|
||||||
|
sscl::tests::CrossThreadTrace trace;
|
||||||
|
|
||||||
|
runScenario(
|
||||||
|
[&trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
return groupMemberRunsOnCalleeAndAwaitResumesOnCaller(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion),
|
||||||
|
trace);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_EQ(trace.calleeExecutionThread(), threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(trace.awaitResumeThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_NE(trace.calleeExecutionThread(), trace.awaitResumeThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(GroupEdgeTest, NonViralVoidGroupTemplateInstantiates)
|
||||||
|
{
|
||||||
|
GTEST_SKIP()
|
||||||
|
<< "NonViralPostingInvoker does not satisfy Group's awaitable concept.";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(GroupEdgeTest, EarlyInvokerDestructionIsUnsupported)
|
||||||
|
{
|
||||||
|
GTEST_SKIP()
|
||||||
|
<< "Destroying a member invoker before group settlement completes is undefined.";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(GroupEdgeTest, OverlappingGroupWaitsAssertInDebug)
|
||||||
|
{
|
||||||
|
GTEST_SKIP()
|
||||||
|
<< "Overlapping group co_await is debug-assert behavior.";
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
#include <chrono>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <boost/asio/error.hpp>
|
||||||
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/co/group.h>
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
|
#include <support/groupAssertions.h>
|
||||||
|
#include <support/threadHarness.h>
|
||||||
|
#include <support/timerAwaiters.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int timerDelayShortMs = 50;
|
||||||
|
constexpr int timerDelayMediumMs = 200;
|
||||||
|
constexpr int timerDelayLongMs = 500;
|
||||||
|
constexpr int awaitAllTimingSlackMs = 25;
|
||||||
|
constexpr int awaitAllLongCancelTimingMarginMs = 50;
|
||||||
|
|
||||||
|
using CallerDriver =
|
||||||
|
sscl::tests::RoleNonViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLER>;
|
||||||
|
|
||||||
|
using CalleeIntInvoker =
|
||||||
|
sscl::tests::RoleViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLEE,
|
||||||
|
int>;
|
||||||
|
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
using Ms = std::chrono::milliseconds;
|
||||||
|
|
||||||
|
class GroupTimerThreadTrace
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void recordTimerCompletionThread(int timerLabelMilliseconds)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
timerCompletionThreads[timerLabelMilliseconds] =
|
||||||
|
std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordAwaitFirstResumeThread()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
awaitFirstResumeThread = std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordAwaitAllResumeThread()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
awaitAllResumeThread = std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id timerCompletionThread(int timerLabelMilliseconds) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
const auto iterator =
|
||||||
|
timerCompletionThreads.find(timerLabelMilliseconds);
|
||||||
|
|
||||||
|
if (iterator == timerCompletionThreads.end()) {
|
||||||
|
throw std::runtime_error("Missing timer completion thread trace");
|
||||||
|
}
|
||||||
|
|
||||||
|
return iterator->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id awaitFirstThread() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return awaitFirstResumeThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id awaitAllThread() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return awaitAllResumeThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::mutex mutex;
|
||||||
|
std::map<int, std::thread::id> timerCompletionThreads;
|
||||||
|
std::thread::id awaitFirstResumeThread;
|
||||||
|
std::thread::id awaitAllResumeThread;
|
||||||
|
};
|
||||||
|
|
||||||
|
CalleeIntInvoker waitDeadlineTimer(
|
||||||
|
int timerLabelMilliseconds,
|
||||||
|
GroupTimerThreadTrace &trace)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
timerLabelMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
trace.recordTimerCompletionThread(timerLabelMilliseconds);
|
||||||
|
co_return timerLabelMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeIntInvoker waitCancelableDeadlineTimer(
|
||||||
|
int timerLabelMilliseconds,
|
||||||
|
sscl::tests::CancelableDeadlineTimerRegistry ®istry,
|
||||||
|
GroupTimerThreadTrace &trace)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::RegisteredDeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
timerLabelMilliseconds,
|
||||||
|
timerLabelMilliseconds,
|
||||||
|
registry};
|
||||||
|
|
||||||
|
if (sscl::tests::timerWasCanceled(waitError)) {
|
||||||
|
trace.recordTimerCompletionThread(timerLabelMilliseconds);
|
||||||
|
co_return timerLabelMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
trace.recordTimerCompletionThread(timerLabelMilliseconds);
|
||||||
|
co_return timerLabelMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
void throwIfElapsedTooLong(
|
||||||
|
const Ms &elapsed,
|
||||||
|
const Ms &limit,
|
||||||
|
const char *message)
|
||||||
|
{
|
||||||
|
if (elapsed > limit) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(message) + ": " + std::to_string(elapsed.count()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void throwIfElapsedTooShort(
|
||||||
|
const Ms &elapsed,
|
||||||
|
const Ms &limit,
|
||||||
|
const char *message)
|
||||||
|
{
|
||||||
|
if (elapsed < limit) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::string(message) + ": " + std::to_string(elapsed.count()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver runGroupTimerRace(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
GroupTimerThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker invokerShort =
|
||||||
|
waitDeadlineTimer(timerDelayShortMs, trace);
|
||||||
|
CalleeIntInvoker invokerMedium =
|
||||||
|
waitDeadlineTimer(timerDelayMediumMs, trace);
|
||||||
|
CalleeIntInvoker invokerLong =
|
||||||
|
waitDeadlineTimer(timerDelayLongMs, trace);
|
||||||
|
|
||||||
|
group.add(invokerShort);
|
||||||
|
group.add(invokerMedium);
|
||||||
|
group.add(invokerLong);
|
||||||
|
|
||||||
|
const auto testStart = Clock::now();
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstSettlement, allSettlementsAfterFirst] = co_await awaitFirst;
|
||||||
|
trace.recordAwaitFirstResumeThread();
|
||||||
|
|
||||||
|
const auto firstElapsedMs =
|
||||||
|
std::chrono::duration_cast<Ms>(Clock::now() - testStart);
|
||||||
|
throwIfElapsedTooLong(
|
||||||
|
firstElapsedMs,
|
||||||
|
Ms(timerDelayMediumMs - awaitAllTimingSlackMs),
|
||||||
|
"await-first took too long");
|
||||||
|
|
||||||
|
if (&firstSettlement.invokerAs<CalleeIntInvoker>() != &invokerShort) {
|
||||||
|
throw std::runtime_error("first settlement was not shortest timer");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.allInvokersSettled()) {
|
||||||
|
throw std::runtime_error("await-first returned after all settled");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
auto &allSettlements = co_await awaitAll;
|
||||||
|
trace.recordAwaitAllResumeThread();
|
||||||
|
|
||||||
|
const auto allElapsedMs =
|
||||||
|
std::chrono::duration_cast<Ms>(Clock::now() - testStart);
|
||||||
|
throwIfElapsedTooShort(
|
||||||
|
allElapsedMs,
|
||||||
|
Ms(timerDelayLongMs - awaitAllLongCancelTimingMarginMs),
|
||||||
|
"await-all finished too soon");
|
||||||
|
|
||||||
|
if (allSettlements.size() != 3) {
|
||||||
|
throw std::runtime_error("expected three settlements");
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
firstSettlement,
|
||||||
|
timerDelayShortMs);
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allSettlementsAfterFirst[0],
|
||||||
|
timerDelayShortMs);
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allSettlementsAfterFirst[1],
|
||||||
|
timerDelayMediumMs);
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allSettlementsAfterFirst[2],
|
||||||
|
timerDelayLongMs);
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerDriver runGroupTimerCancelLongAfterAwaitFirst(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
sscl::tests::CancelableDeadlineTimerRegistry ®istry,
|
||||||
|
GroupTimerThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
CalleeIntInvoker invokerShort =
|
||||||
|
waitCancelableDeadlineTimer(timerDelayShortMs, registry, trace);
|
||||||
|
CalleeIntInvoker invokerMedium =
|
||||||
|
waitCancelableDeadlineTimer(timerDelayMediumMs, registry, trace);
|
||||||
|
CalleeIntInvoker invokerLong =
|
||||||
|
waitCancelableDeadlineTimer(timerDelayLongMs, registry, trace);
|
||||||
|
|
||||||
|
group.add(invokerShort);
|
||||||
|
group.add(invokerMedium);
|
||||||
|
group.add(invokerLong);
|
||||||
|
|
||||||
|
const auto testStart = Clock::now();
|
||||||
|
|
||||||
|
auto awaitFirst = group.getAwaitFirstSettlementInvoker();
|
||||||
|
auto [firstSettlement, allSettlementsAfterFirst] = co_await awaitFirst;
|
||||||
|
trace.recordAwaitFirstResumeThread();
|
||||||
|
|
||||||
|
if (&firstSettlement.invokerAs<CalleeIntInvoker>() != &invokerShort) {
|
||||||
|
throw std::runtime_error("cancel test first settlement mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.allInvokersSettled()) {
|
||||||
|
throw std::runtime_error("cancel test all settled after await-first");
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.cancel(timerDelayLongMs);
|
||||||
|
|
||||||
|
auto awaitAll = group.getAwaitAllSettlementsInvoker();
|
||||||
|
auto &allSettlements = co_await awaitAll;
|
||||||
|
trace.recordAwaitAllResumeThread();
|
||||||
|
|
||||||
|
const auto allElapsedMs =
|
||||||
|
std::chrono::duration_cast<Ms>(Clock::now() - testStart);
|
||||||
|
|
||||||
|
if (allElapsedMs >= Ms(timerDelayLongMs - awaitAllLongCancelTimingMarginMs)) {
|
||||||
|
throw std::runtime_error("await-all waited for canceled long timer");
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfElapsedTooShort(
|
||||||
|
allElapsedMs,
|
||||||
|
Ms(timerDelayMediumMs - awaitAllTimingSlackMs),
|
||||||
|
"await-all finished before medium timer");
|
||||||
|
|
||||||
|
if (allSettlements.size() != 3) {
|
||||||
|
throw std::runtime_error("cancel test expected three settlements");
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allSettlements[0],
|
||||||
|
timerDelayShortMs);
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allSettlements[1],
|
||||||
|
timerDelayMediumMs);
|
||||||
|
sscl::tests::requireCompletedIntSettlement<CalleeIntInvoker>(
|
||||||
|
allSettlements[2],
|
||||||
|
timerDelayLongMs);
|
||||||
|
|
||||||
|
if (&allSettlements[2].invokerAs<CalleeIntInvoker>() != &invokerLong) {
|
||||||
|
throw std::runtime_error("cancel test long invoker mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)allSettlementsAfterFirst;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupTimerTest
|
||||||
|
: public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void assertTimerTraceCrossedThreads(
|
||||||
|
const GroupTimerThreadTrace &trace)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(
|
||||||
|
trace.timerCompletionThread(timerDelayShortMs),
|
||||||
|
threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(
|
||||||
|
trace.timerCompletionThread(timerDelayMediumMs),
|
||||||
|
threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(
|
||||||
|
trace.timerCompletionThread(timerDelayLongMs),
|
||||||
|
threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(trace.awaitFirstThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_EQ(trace.awaitAllThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_NE(
|
||||||
|
trace.timerCompletionThread(timerDelayShortMs),
|
||||||
|
trace.awaitFirstThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::PostingThreadSet threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(GroupTimerTest, AwaitFirstReturnsShortestTimerAndAwaitAllWaitsForLongest)
|
||||||
|
{
|
||||||
|
GroupTimerThreadTrace trace;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[&trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
return runGroupTimerRace(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion),
|
||||||
|
trace);
|
||||||
|
}));
|
||||||
|
|
||||||
|
assertTimerTraceCrossedThreads(trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(GroupTimerTest, CancelLongTimerAfterAwaitFirst)
|
||||||
|
{
|
||||||
|
sscl::tests::CancelableDeadlineTimerRegistry registry;
|
||||||
|
GroupTimerThreadTrace trace;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[®istry, &trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
return runGroupTimerCancelLongAfterAwaitFirst(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion),
|
||||||
|
registry,
|
||||||
|
trace);
|
||||||
|
}));
|
||||||
|
|
||||||
|
assertTimerTraceCrossedThreads(trace);
|
||||||
|
}
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
|
#include <spinscale/co/nonViralTaskNursery.h>
|
||||||
|
#include <spinscale/syncCancelerForAsyncWork.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ResumeGate
|
||||||
|
{
|
||||||
|
std::coroutine_handle<> waitingHandle;
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{ return false; }
|
||||||
|
|
||||||
|
bool await_suspend(std::coroutine_handle<> callerHandle) noexcept
|
||||||
|
{
|
||||||
|
waitingHandle = callerHandle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void await_resume() const noexcept
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
sscl::co::NonViralNonPostingInvoker immediateCompleteCReq(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::co::NonViralNonPostingInvoker throwingCompleteCReq(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
throw std::runtime_error("nursery test failure");
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::co::NonViralNonPostingInvoker suspendUntilResumeCReq(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
ResumeGate &gate)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
co_await gate;
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::co::NonViralNonPostingInvoker cancelAwareSuspendCReq(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
sscl::SyncCancelerForAsyncWork &canceler,
|
||||||
|
ResumeGate &gate)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
while (!canceler.isCancellationRequested())
|
||||||
|
{
|
||||||
|
co_await gate;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class NonViralTaskNurseryTest : public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
nursery.openAdmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::co::NonViralTaskNursery nursery;
|
||||||
|
ResumeGate gate;
|
||||||
|
ResumeGate gate2;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, GetNewSlotLeaseFillCommitRetires)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease]()
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, UncommittedLeaseReleasesReservation)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
(void)lease;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, CloseAdmissionRejectsNewLeases)
|
||||||
|
{
|
||||||
|
nursery.closeAdmission();
|
||||||
|
|
||||||
|
EXPECT_THROW(nursery.getNewSlotLease(), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, SetOnSettledHookRejectsAfterFillSlot)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease]()
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_THROW(
|
||||||
|
lease.setOnSettledHook([](std::exception_ptr &) {}),
|
||||||
|
std::runtime_error);
|
||||||
|
lease.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, AsyncAwaitFiresOnDrain)
|
||||||
|
{
|
||||||
|
std::atomic<bool> drained{false};
|
||||||
|
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease]()
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
nursery.closeAdmission();
|
||||||
|
nursery.asyncAwaitAllSettlements(
|
||||||
|
[&drained]()
|
||||||
|
{
|
||||||
|
drained.store(true, std::memory_order_release);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_TRUE(drained.load(std::memory_order_acquire));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, AsyncAwaitRejectsWhenAdmissionOpen)
|
||||||
|
{
|
||||||
|
EXPECT_THROW(nursery.asyncAwaitAllSettlements([]() {}), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, SecondDrainWaiterThrows)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
nursery.closeAdmission();
|
||||||
|
|
||||||
|
bool firstWaiterRegistered = false;
|
||||||
|
nursery.asyncAwaitAllSettlements(
|
||||||
|
[&firstWaiterRegistered]()
|
||||||
|
{
|
||||||
|
firstWaiterRegistered = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_FALSE(firstWaiterRegistered);
|
||||||
|
EXPECT_THROW(
|
||||||
|
nursery.asyncAwaitAllSettlements([]() {}),
|
||||||
|
std::runtime_error);
|
||||||
|
|
||||||
|
if (gate.waitingHandle)
|
||||||
|
{
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, SyncAwaitNestedRun)
|
||||||
|
{
|
||||||
|
boost::asio::io_context ioContext;
|
||||||
|
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
std::thread awaitThread(
|
||||||
|
[this, &ioContext]()
|
||||||
|
{
|
||||||
|
nursery.closeAdmission();
|
||||||
|
nursery.syncAwaitAllSettlements(ioContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
|
||||||
|
ASSERT_TRUE(static_cast<bool>(gate.waitingHandle));
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
|
||||||
|
awaitThread.join();
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, RequestCancelOnAllDoesNotDestroyInvokers)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.getSyncCanceler().startAcceptingWork();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 1U);
|
||||||
|
nursery.requestCancelOnAll();
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 1U);
|
||||||
|
|
||||||
|
ASSERT_TRUE(static_cast<bool>(gate.waitingHandle));
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, RequestCancelOnAllStopsCanceler)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.getSyncCanceler().startAcceptingWork();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease, this]()
|
||||||
|
{
|
||||||
|
return cancelAwareSuspendCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
lease.getSyncCanceler(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
nursery.requestCancelOnAll();
|
||||||
|
EXPECT_TRUE(lease.getSyncCanceler().isCancellationRequested());
|
||||||
|
|
||||||
|
ASSERT_TRUE(static_cast<bool>(gate.waitingHandle));
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, ExceptionPtrRecorded)
|
||||||
|
{
|
||||||
|
std::exception_ptr captured;
|
||||||
|
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&captured, &lease]()
|
||||||
|
{
|
||||||
|
std::exception_ptr &exceptionStorage =
|
||||||
|
lease.getExceptionStorage();
|
||||||
|
auto invoker = throwingCompleteCReq(
|
||||||
|
exceptionStorage,
|
||||||
|
lease.getCallerLambda());
|
||||||
|
captured = exceptionStorage;
|
||||||
|
return invoker;
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
EXPECT_TRUE(captured != nullptr);
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, LaunchSugar)
|
||||||
|
{
|
||||||
|
auto handle = nursery.launch(
|
||||||
|
[](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_TRUE(handle == handle);
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, LaunchWithOnSettledHook)
|
||||||
|
{
|
||||||
|
std::atomic<bool> hookRan{false};
|
||||||
|
|
||||||
|
nursery.launch(
|
||||||
|
[](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
},
|
||||||
|
[&hookRan](std::exception_ptr &)
|
||||||
|
{
|
||||||
|
hookRan.store(true, std::memory_order_release);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_TRUE(hookRan.load(std::memory_order_acquire));
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, HandleStability)
|
||||||
|
{
|
||||||
|
auto handle = nursery.launch(
|
||||||
|
[](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
|
||||||
|
sscl::co::NonViralTaskNursery::Slot::Handle copy = handle;
|
||||||
|
EXPECT_TRUE(handle == copy);
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, CommitWithoutFillSlotThrows)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
|
||||||
|
EXPECT_THROW(lease.commit(), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, DoubleCommitThrows)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease]()
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
EXPECT_THROW(lease.commit(), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, FillSlotTwiceThrows)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_THROW(
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease]()
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
}),
|
||||||
|
std::runtime_error);
|
||||||
|
|
||||||
|
if (gate.waitingHandle) {
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
lease.commit();
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, SyncAwaitRejectsWhenAdmissionOpen)
|
||||||
|
{
|
||||||
|
boost::asio::io_context ioContext;
|
||||||
|
|
||||||
|
EXPECT_THROW(
|
||||||
|
nursery.syncAwaitAllSettlements(ioContext),
|
||||||
|
std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, SyncAwaitRejectsStoppedIoContext)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
nursery.closeAdmission();
|
||||||
|
|
||||||
|
boost::asio::io_context ioContext;
|
||||||
|
ioContext.stop();
|
||||||
|
|
||||||
|
EXPECT_THROW(
|
||||||
|
nursery.syncAwaitAllSettlements(ioContext),
|
||||||
|
std::runtime_error);
|
||||||
|
|
||||||
|
if (gate.waitingHandle) {
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, SyncAwaitReturnsImmediatelyWhenDrained)
|
||||||
|
{
|
||||||
|
boost::asio::io_context ioContext;
|
||||||
|
|
||||||
|
nursery.closeAdmission();
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
|
||||||
|
nursery.syncAwaitAllSettlements(ioContext);
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, UnsettledCountTracksInFlightTasks)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 1U);
|
||||||
|
EXPECT_FALSE(nursery.allSettled());
|
||||||
|
|
||||||
|
if (gate.waitingHandle) {
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, MultipleTasksDrainTogether)
|
||||||
|
{
|
||||||
|
std::atomic<bool> drained{false};
|
||||||
|
|
||||||
|
auto lease1 = nursery.getNewSlotLease();
|
||||||
|
lease1.fillSlot(
|
||||||
|
[&lease1, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease1.getExceptionStorage(),
|
||||||
|
lease1.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
lease1.commit();
|
||||||
|
|
||||||
|
auto lease2 = nursery.getNewSlotLease();
|
||||||
|
lease2.fillSlot(
|
||||||
|
[&lease2, this]()
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease2.getExceptionStorage(),
|
||||||
|
lease2.getCallerLambda(),
|
||||||
|
gate2);
|
||||||
|
});
|
||||||
|
lease2.commit();
|
||||||
|
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 2U);
|
||||||
|
|
||||||
|
nursery.closeAdmission();
|
||||||
|
nursery.asyncAwaitAllSettlements(
|
||||||
|
[&drained]()
|
||||||
|
{
|
||||||
|
drained.store(true, std::memory_order_release);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_FALSE(drained.load(std::memory_order_acquire));
|
||||||
|
|
||||||
|
if (gate.waitingHandle) {
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
EXPECT_FALSE(drained.load(std::memory_order_acquire));
|
||||||
|
|
||||||
|
if (gate2.waitingHandle) {
|
||||||
|
gate2.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
EXPECT_TRUE(drained.load(std::memory_order_acquire));
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, OnSettledHookRunsAtRetirement)
|
||||||
|
{
|
||||||
|
std::atomic<bool> hookRan{false};
|
||||||
|
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.setOnSettledHook(
|
||||||
|
[&hookRan](std::exception_ptr &)
|
||||||
|
{
|
||||||
|
hookRan.store(true, std::memory_order_release);
|
||||||
|
});
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease]()
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
EXPECT_TRUE(hookRan.load(std::memory_order_acquire));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, OnSettledHookSeesRetiredSlot)
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.setOnSettledHook(
|
||||||
|
[this](std::exception_ptr &)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||||
|
});
|
||||||
|
lease.fillSlot(
|
||||||
|
[&lease]()
|
||||||
|
{
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda());
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, DuplicateRetireThrows)
|
||||||
|
{
|
||||||
|
std::function<void()> completion;
|
||||||
|
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
lease.fillSlot(
|
||||||
|
[&completion, &lease]()
|
||||||
|
{
|
||||||
|
completion = lease.getCallerLambda();
|
||||||
|
return immediateCompleteCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
completion);
|
||||||
|
});
|
||||||
|
lease.commit();
|
||||||
|
|
||||||
|
ASSERT_TRUE(static_cast<bool>(completion));
|
||||||
|
EXPECT_THROW(completion(), std::runtime_error);
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, MovedLeaseTransfersReleaseObligation)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||||
|
{
|
||||||
|
auto lease = nursery.getNewSlotLease();
|
||||||
|
auto movedLease = std::move(lease);
|
||||||
|
(void)movedLease;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, LaunchAssignsDistinctHandles)
|
||||||
|
{
|
||||||
|
auto handle1 = nursery.launch(
|
||||||
|
[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto handle2 = nursery.launch(
|
||||||
|
[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||||
|
{
|
||||||
|
return suspendUntilResumeCReq(
|
||||||
|
lease.getExceptionStorage(),
|
||||||
|
lease.getCallerLambda(),
|
||||||
|
gate2);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_NE(handle1, handle2);
|
||||||
|
EXPECT_EQ(nursery.unsettledCount(), 2U);
|
||||||
|
|
||||||
|
if (gate.waitingHandle) {
|
||||||
|
gate.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gate2.waitingHandle) {
|
||||||
|
gate2.waitingHandle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
EXPECT_TRUE(nursery.allSettled());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(NonViralTaskNurseryTest, AdmissionIsOpenReflectsCloseAndOpen)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(nursery.admissionIsOpen());
|
||||||
|
|
||||||
|
nursery.closeAdmission();
|
||||||
|
EXPECT_FALSE(nursery.admissionIsOpen());
|
||||||
|
|
||||||
|
nursery.openAdmission();
|
||||||
|
EXPECT_TRUE(nursery.admissionIsOpen());
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <spinscale/co/postTarget.h>
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
|
#include <support/threadHarness.h>
|
||||||
|
#include <support/timerAwaiters.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int expectedReturnValue = 42;
|
||||||
|
constexpr int explicitTargetReturnValue = 77;
|
||||||
|
constexpr const char *expectedThrowMessage =
|
||||||
|
"posting cross-thread intentional failure";
|
||||||
|
|
||||||
|
using CallerNonViralInvoker =
|
||||||
|
sscl::tests::RoleNonViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLER>;
|
||||||
|
using CalleeNonViralInvoker =
|
||||||
|
sscl::tests::RoleNonViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLEE>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using CalleeViralInvoker =
|
||||||
|
sscl::tests::RoleViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLEE,
|
||||||
|
T>;
|
||||||
|
|
||||||
|
CalleeViralInvoker<int> returnFromCalleeThread(
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
trace.recordCalleeExecutionThread();
|
||||||
|
trace.recordFinalSuspendThread();
|
||||||
|
co_return expectedReturnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeViralInvoker<int> returnFromExplicitTargetThread(
|
||||||
|
sscl::co::ExplicitPostTarget postTarget,
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)postTarget;
|
||||||
|
trace.recordCalleeExecutionThread();
|
||||||
|
trace.recordFinalSuspendThread();
|
||||||
|
co_return explicitTargetReturnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeViralInvoker<int> throwFromCalleeThread(
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
constexpr int throwDelayMs = 1;
|
||||||
|
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
throwDelayMs};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
trace.recordCalleeExecutionThread();
|
||||||
|
trace.recordFinalSuspendThread();
|
||||||
|
throw std::runtime_error(expectedThrowMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerNonViralInvoker awaitCalleeDriver(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
const int value = co_await returnFromCalleeThread(trace);
|
||||||
|
trace.recordAwaitResumeThread();
|
||||||
|
|
||||||
|
if (value != expectedReturnValue) {
|
||||||
|
throw std::runtime_error("Unexpected callee return value");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerNonViralInvoker awaitExplicitTargetDriver(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::ExplicitPostTarget postTarget{
|
||||||
|
sscl::tests::ThreadRegistry::ioContext(
|
||||||
|
sscl::tests::PostingThreadRole::ALTERNATE)};
|
||||||
|
const int value = co_await returnFromExplicitTargetThread(
|
||||||
|
postTarget,
|
||||||
|
trace);
|
||||||
|
trace.recordAwaitResumeThread();
|
||||||
|
|
||||||
|
if (value != explicitTargetReturnValue) {
|
||||||
|
throw std::runtime_error("Unexpected explicit-target return value");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerNonViralInvoker awaitThrowingCalleeDriver(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
try {
|
||||||
|
(void)co_await throwFromCalleeThread(trace);
|
||||||
|
throw std::runtime_error("Expected callee exception");
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &runtimeError) {
|
||||||
|
trace.recordAwaitResumeThread();
|
||||||
|
if (std::string(runtimeError.what()) != expectedThrowMessage) {
|
||||||
|
throw std::runtime_error("Unexpected callee exception message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalleeNonViralInvoker nonViralCalleeCompletesToCaller(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion,
|
||||||
|
sscl::tests::CrossThreadTrace &trace)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
trace.recordCalleeExecutionThread();
|
||||||
|
trace.recordFinalSuspendThread();
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostingCrossThreadTest
|
||||||
|
: public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
sscl::tests::PostingThreadSet threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(PostingCrossThreadTest, ViralAwaitPostsCalleeAndResumesCaller)
|
||||||
|
{
|
||||||
|
sscl::tests::CrossThreadTrace trace;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[&trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
trace.recordConstructionThread();
|
||||||
|
return awaitCalleeDriver(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion),
|
||||||
|
trace);
|
||||||
|
}));
|
||||||
|
|
||||||
|
EXPECT_EQ(trace.constructionThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_EQ(trace.calleeExecutionThread(), threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(trace.finalSuspendThread(), threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(trace.awaitResumeThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_NE(trace.calleeExecutionThread(), trace.awaitResumeThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PostingCrossThreadTest, NonViralCompletionPostsBackToCaller)
|
||||||
|
{
|
||||||
|
sscl::tests::CrossThreadTrace trace;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[&trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
trace.recordConstructionThread();
|
||||||
|
return nonViralCalleeCompletesToCaller(
|
||||||
|
exceptionPtr,
|
||||||
|
[&trace, completion = std::move(completion)]() mutable
|
||||||
|
{
|
||||||
|
trace.recordCompletionCallbackThread();
|
||||||
|
completion();
|
||||||
|
},
|
||||||
|
trace);
|
||||||
|
}));
|
||||||
|
|
||||||
|
EXPECT_EQ(trace.constructionThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_EQ(trace.calleeExecutionThread(), threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(trace.finalSuspendThread(), threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(trace.completionCallbackThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_NE(trace.calleeExecutionThread(), trace.completionCallbackThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PostingCrossThreadTest, ExplicitPostTargetRoutesCalleeExecution)
|
||||||
|
{
|
||||||
|
sscl::tests::CrossThreadTrace trace;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[&trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
trace.recordConstructionThread();
|
||||||
|
return awaitExplicitTargetDriver(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion),
|
||||||
|
trace);
|
||||||
|
}));
|
||||||
|
|
||||||
|
EXPECT_EQ(trace.constructionThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_EQ(trace.calleeExecutionThread(), threads.alternate().osThreadId());
|
||||||
|
EXPECT_EQ(trace.awaitResumeThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_NE(trace.calleeExecutionThread(), threads.callee().osThreadId());
|
||||||
|
EXPECT_NE(trace.calleeExecutionThread(), trace.awaitResumeThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PostingCrossThreadTest, CalleeExceptionIsObservedOnCallerThread)
|
||||||
|
{
|
||||||
|
sscl::tests::CrossThreadTrace trace;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[&trace](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
trace.recordConstructionThread();
|
||||||
|
return awaitThrowingCalleeDriver(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion),
|
||||||
|
trace);
|
||||||
|
}));
|
||||||
|
|
||||||
|
EXPECT_EQ(trace.constructionThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_EQ(trace.calleeExecutionThread(), threads.callee().osThreadId());
|
||||||
|
EXPECT_EQ(trace.awaitResumeThread(), threads.caller().osThreadId());
|
||||||
|
EXPECT_NE(trace.calleeExecutionThread(), trace.awaitResumeThread());
|
||||||
|
}
|
||||||
@@ -0,0 +1,633 @@
|
|||||||
|
#include <chrono>
|
||||||
|
#include <exception>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
|
#include <spinscale/co/group.h>
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
|
#include <support/coroutineDriver.h>
|
||||||
|
#include <support/groupAssertions.h>
|
||||||
|
#include <support/threadHarness.h>
|
||||||
|
#include <support/timerAwaiters.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int delayShortMs = 50;
|
||||||
|
constexpr int expectedNonStdThrowValue = 42;
|
||||||
|
constexpr const char *expectedThrowMessage =
|
||||||
|
"viral_non_posting_test intentional failure";
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using TestInvoker = sscl::co::ViralNonPostingInvoker<T>;
|
||||||
|
|
||||||
|
using TestDriver = TestInvoker<int>;
|
||||||
|
using TestVoidDriver = TestInvoker<void>;
|
||||||
|
using CallerPostingDriver =
|
||||||
|
sscl::tests::RoleNonViralPostingInvoker<
|
||||||
|
sscl::tests::PostingThreadRole::CALLER>;
|
||||||
|
|
||||||
|
struct ThreadIdPair
|
||||||
|
{
|
||||||
|
std::thread::id callerIdAtCoAwait;
|
||||||
|
std::thread::id calleeId;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MoveCountedInt
|
||||||
|
{
|
||||||
|
std::shared_ptr<std::size_t> moveCount;
|
||||||
|
int value = 0;
|
||||||
|
|
||||||
|
MoveCountedInt() = default;
|
||||||
|
|
||||||
|
MoveCountedInt(
|
||||||
|
std::shared_ptr<std::size_t> moveCountIn,
|
||||||
|
int valueIn)
|
||||||
|
: moveCount(std::move(moveCountIn)),
|
||||||
|
value(valueIn)
|
||||||
|
{}
|
||||||
|
|
||||||
|
MoveCountedInt(const MoveCountedInt &) = delete;
|
||||||
|
MoveCountedInt &operator=(const MoveCountedInt &) = delete;
|
||||||
|
|
||||||
|
MoveCountedInt(MoveCountedInt &&other) noexcept
|
||||||
|
: moveCount(std::exchange(other.moveCount, {})),
|
||||||
|
value(other.value)
|
||||||
|
{
|
||||||
|
if (moveCount) {
|
||||||
|
++(*moveCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveCountedInt &operator=(MoveCountedInt &&other) noexcept
|
||||||
|
{
|
||||||
|
moveCount = std::exchange(other.moveCount, {});
|
||||||
|
value = other.value;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct CountingAwaiter
|
||||||
|
{
|
||||||
|
TestInvoker<T> &invoker;
|
||||||
|
std::size_t &awaitResumeCallCount;
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{ return invoker.await_ready(); }
|
||||||
|
|
||||||
|
template <typename CallerPromise>
|
||||||
|
bool await_suspend(
|
||||||
|
std::coroutine_handle<CallerPromise> callerSchedHandle) noexcept
|
||||||
|
{ return invoker.await_suspend(callerSchedHandle); }
|
||||||
|
|
||||||
|
auto await_resume()
|
||||||
|
{
|
||||||
|
++awaitResumeCallCount;
|
||||||
|
return invoker.await_resume();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ViralNonPostingTest
|
||||||
|
: public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void TearDown() override
|
||||||
|
{
|
||||||
|
ioContext.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
int runDriver(TestDriver &driver)
|
||||||
|
{
|
||||||
|
return sscl::tests::CoroutineDriver::pumpUntilIdleAndReturnValue(
|
||||||
|
ioContext,
|
||||||
|
driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
int finishDriver(TestDriver &driver)
|
||||||
|
{
|
||||||
|
return sscl::tests::CoroutineDriver::completedReturnValue(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::io_context ioContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
TestInvoker<int> returnLabelImmediately(int label)
|
||||||
|
{
|
||||||
|
co_return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<int> waitAndReturnLabel(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
ioContext,
|
||||||
|
delayMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
co_return delayMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestVoidDriver voidReturnImmediately()
|
||||||
|
{
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestVoidDriver voidMemberAfterDelay(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
ioContext,
|
||||||
|
delayMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<int> throwRuntimeErrorImmediately()
|
||||||
|
{
|
||||||
|
throw std::runtime_error(expectedThrowMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<int> throwIntImmediately()
|
||||||
|
{
|
||||||
|
throw expectedNonStdThrowValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<ThreadIdPair> recordThreadIdsAtReturn()
|
||||||
|
{
|
||||||
|
ThreadIdPair pair;
|
||||||
|
pair.calleeId = std::this_thread::get_id();
|
||||||
|
co_return pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<ThreadIdPair> recordThreadIdsAfterDelay(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const boost::system::error_code waitError =
|
||||||
|
co_await sscl::tests::DeadlineTimerAwaiter{
|
||||||
|
ioContext,
|
||||||
|
delayMilliseconds};
|
||||||
|
sscl::tests::throwIfTimerWaitFailed(waitError);
|
||||||
|
|
||||||
|
ThreadIdPair pair;
|
||||||
|
pair.calleeId = std::this_thread::get_id();
|
||||||
|
co_return pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<MoveCountedInt> returnMoveCountedInt(
|
||||||
|
std::shared_ptr<std::size_t> moveCount,
|
||||||
|
int value)
|
||||||
|
{
|
||||||
|
co_return MoveCountedInt{std::move(moveCount), value};
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<int> innerDelayedCoAwait(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const int label = co_await waitAndReturnLabel(
|
||||||
|
ioContext,
|
||||||
|
delayMilliseconds);
|
||||||
|
co_return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<int> nestedNonPostingSum(int left, int right)
|
||||||
|
{
|
||||||
|
const int leftSum = co_await returnLabelImmediately(left);
|
||||||
|
const int rightSum = co_await returnLabelImmediately(right);
|
||||||
|
co_return leftSum + rightSum;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestInvoker<int> outerCoAwaitingDelayedInner(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
int delayMilliseconds)
|
||||||
|
{
|
||||||
|
const int innerLabel = co_await innerDelayedCoAwait(
|
||||||
|
ioContext,
|
||||||
|
delayMilliseconds);
|
||||||
|
co_return innerLabel + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testImmediateReturnFastPath()
|
||||||
|
{
|
||||||
|
const int value = co_await returnLabelImmediately(42);
|
||||||
|
if (value != 42) {
|
||||||
|
throw std::runtime_error("immediateReturnFastPath value mismatch");
|
||||||
|
}
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testAllCompleteBeforeCoAwait()
|
||||||
|
{
|
||||||
|
TestInvoker<int> invokerTen = returnLabelImmediately(10);
|
||||||
|
TestInvoker<int> invokerTwenty = returnLabelImmediately(20);
|
||||||
|
TestInvoker<int> invokerThirty = returnLabelImmediately(30);
|
||||||
|
|
||||||
|
const int valueTen = co_await invokerTen;
|
||||||
|
const int valueTwenty = co_await invokerTwenty;
|
||||||
|
const int valueThirty = co_await invokerThirty;
|
||||||
|
|
||||||
|
if (valueTen != 10 || valueTwenty != 20 || valueThirty != 30) {
|
||||||
|
throw std::runtime_error("allCompleteBeforeCoAwait label mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testCallerSuspendsThenResumes(boost::asio::io_context &ioContext)
|
||||||
|
{
|
||||||
|
const int value = co_await waitAndReturnLabel(ioContext, delayShortMs);
|
||||||
|
if (value != delayShortMs) {
|
||||||
|
throw std::runtime_error("callerSuspendsThenResumes label mismatch");
|
||||||
|
}
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testMixedImmediateAndDelayedInSequence(
|
||||||
|
boost::asio::io_context &ioContext)
|
||||||
|
{
|
||||||
|
const int immediate = co_await returnLabelImmediately(7);
|
||||||
|
const int delayed = co_await waitAndReturnLabel(ioContext, delayShortMs);
|
||||||
|
|
||||||
|
if (immediate != 7 || delayed != delayShortMs) {
|
||||||
|
throw std::runtime_error("mixedImmediateAndDelayed label mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testAwaitResumeCalledOnceFastPath()
|
||||||
|
{
|
||||||
|
std::size_t awaitResumeCallCount = 0;
|
||||||
|
TestInvoker<int> invoker = returnLabelImmediately(42);
|
||||||
|
const int value = co_await CountingAwaiter<int>{
|
||||||
|
invoker,
|
||||||
|
awaitResumeCallCount};
|
||||||
|
|
||||||
|
if (value != 42 || awaitResumeCallCount != 1) {
|
||||||
|
throw std::runtime_error("fast path await_resume count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testAwaitResumeCalledOnceSlowPath(
|
||||||
|
boost::asio::io_context &ioContext)
|
||||||
|
{
|
||||||
|
std::size_t awaitResumeCallCount = 0;
|
||||||
|
TestInvoker<int> invoker = waitAndReturnLabel(ioContext, delayShortMs);
|
||||||
|
const int value = co_await CountingAwaiter<int>{
|
||||||
|
invoker,
|
||||||
|
awaitResumeCallCount};
|
||||||
|
|
||||||
|
if (value != delayShortMs || awaitResumeCallCount != 1) {
|
||||||
|
throw std::runtime_error("slow path await_resume count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testAwaitResumeCalledOnceNested(
|
||||||
|
boost::asio::io_context &ioContext)
|
||||||
|
{
|
||||||
|
std::size_t awaitResumeCallCount = 0;
|
||||||
|
TestInvoker<int> inner = innerDelayedCoAwait(ioContext, delayShortMs);
|
||||||
|
const int value = co_await CountingAwaiter<int>{
|
||||||
|
inner,
|
||||||
|
awaitResumeCallCount};
|
||||||
|
|
||||||
|
if (value != delayShortMs || awaitResumeCallCount != 1) {
|
||||||
|
throw std::runtime_error("nested await_resume count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testMoveCountedReturnNotDoubleMoved()
|
||||||
|
{
|
||||||
|
auto moveCount = std::make_shared<std::size_t>(0);
|
||||||
|
TestInvoker<MoveCountedInt> invoker =
|
||||||
|
returnMoveCountedInt(moveCount, 99);
|
||||||
|
MoveCountedInt result = co_await invoker;
|
||||||
|
|
||||||
|
if (result.value != 99) {
|
||||||
|
throw std::runtime_error("move counted value mismatch");
|
||||||
|
}
|
||||||
|
if (*moveCount > 2 || *moveCount < 1) {
|
||||||
|
throw std::runtime_error("move counted return move-count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testVoidReturnCompletes()
|
||||||
|
{
|
||||||
|
co_await voidReturnImmediately();
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testReturnValuesReadableBeforeDestroy()
|
||||||
|
{
|
||||||
|
TestInvoker<int> invoker = returnLabelImmediately(55);
|
||||||
|
(void)co_await invoker;
|
||||||
|
|
||||||
|
if (invoker.completedReturnValues().myReturnValue != 55) {
|
||||||
|
throw std::runtime_error("completed return value not readable");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testExceptionRethrowsOnCoAwait()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(void)co_await throwRuntimeErrorImmediately();
|
||||||
|
throw std::runtime_error("expected runtime_error");
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &runtimeError) {
|
||||||
|
if (std::string(runtimeError.what()) != expectedThrowMessage) {
|
||||||
|
throw std::runtime_error("unexpected runtime_error message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testNonStdExceptionRethrows()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(void)co_await throwIntImmediately();
|
||||||
|
throw std::runtime_error("expected int exception");
|
||||||
|
}
|
||||||
|
catch (int caughtValue) {
|
||||||
|
if (caughtValue != expectedNonStdThrowValue) {
|
||||||
|
throw std::runtime_error("unexpected int exception value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testCalleeRunsOnCallerThread()
|
||||||
|
{
|
||||||
|
const std::thread::id callerThreadId = std::this_thread::get_id();
|
||||||
|
const ThreadIdPair pair = co_await recordThreadIdsAtReturn();
|
||||||
|
|
||||||
|
if (pair.calleeId != callerThreadId) {
|
||||||
|
throw std::runtime_error("callee thread mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testDelayedCalleeStillOnCallerThread(
|
||||||
|
boost::asio::io_context &ioContext)
|
||||||
|
{
|
||||||
|
const std::thread::id callerThreadId = std::this_thread::get_id();
|
||||||
|
const ThreadIdPair pair =
|
||||||
|
co_await recordThreadIdsAfterDelay(ioContext, delayShortMs);
|
||||||
|
|
||||||
|
if (pair.calleeId != callerThreadId) {
|
||||||
|
throw std::runtime_error("delayed callee thread mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testNestedNonPostingCoAwait()
|
||||||
|
{
|
||||||
|
const int sum = co_await nestedNonPostingSum(10, 32);
|
||||||
|
if (sum != 42) {
|
||||||
|
throw std::runtime_error("nested sum mismatch");
|
||||||
|
}
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDriver testNestedInnerSuspension(boost::asio::io_context &ioContext)
|
||||||
|
{
|
||||||
|
const int value = co_await outerCoAwaitingDelayedInner(
|
||||||
|
ioContext,
|
||||||
|
delayShortMs);
|
||||||
|
if (value != delayShortMs + 1) {
|
||||||
|
throw std::runtime_error("nested inner suspension value mismatch");
|
||||||
|
}
|
||||||
|
co_return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerPostingDriver nonPostingVoidMemberInGroupDriver(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
TestVoidDriver voidInvoker = voidMemberAfterDelay(
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
delayShortMs);
|
||||||
|
group.add(voidInvoker);
|
||||||
|
|
||||||
|
auto &allDescriptors = co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 1) {
|
||||||
|
throw std::runtime_error("voidMemberInGroup count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::tests::requireCompletedSettlement(allDescriptors[0]);
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallerPostingDriver nonPostingGroupMixedImmediateAndDelayedDriver(
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
(void)exceptionPtr;
|
||||||
|
(void)completion;
|
||||||
|
|
||||||
|
sscl::co::Group group;
|
||||||
|
TestInvoker<int> immediateInvoker = returnLabelImmediately(11);
|
||||||
|
TestInvoker<int> delayedInvoker = waitAndReturnLabel(
|
||||||
|
sscl::ComponentThread::getSelf()->getIoContext(),
|
||||||
|
delayShortMs);
|
||||||
|
|
||||||
|
group.add(immediateInvoker);
|
||||||
|
group.add(delayedInvoker);
|
||||||
|
|
||||||
|
auto &allDescriptors = co_await group.getAwaitAllSettlementsInvoker();
|
||||||
|
|
||||||
|
if (allDescriptors.size() != 2) {
|
||||||
|
throw std::runtime_error("groupMixedImmediateAndDelayed count mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sawImmediate = false;
|
||||||
|
bool sawDelayed = false;
|
||||||
|
|
||||||
|
for (auto &descriptor : allDescriptors) {
|
||||||
|
sscl::tests::requireCompletedSettlement(descriptor);
|
||||||
|
const int label = sscl::tests::completedIntValue(
|
||||||
|
descriptor.invokerAs<TestInvoker<int>>());
|
||||||
|
if (label == 11) {
|
||||||
|
sawImmediate = true;
|
||||||
|
}
|
||||||
|
else if (label == delayShortMs) {
|
||||||
|
sawDelayed = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"groupMixedImmediateAndDelayed unexpected label");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sawImmediate || !sawDelayed) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"groupMixedImmediateAndDelayed missing expected label");
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, ImmediateReturnFastPath)
|
||||||
|
{
|
||||||
|
TestDriver driver = testImmediateReturnFastPath();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, AllCompleteBeforeCoAwait)
|
||||||
|
{
|
||||||
|
TestDriver driver = testAllCompleteBeforeCoAwait();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, CallerSuspendsThenResumes)
|
||||||
|
{
|
||||||
|
TestDriver driver = testCallerSuspendsThenResumes(ioContext);
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(runDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, MixedImmediateAndDelayedInSequence)
|
||||||
|
{
|
||||||
|
TestDriver driver = testMixedImmediateAndDelayedInSequence(ioContext);
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(runDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, AwaitResumeCalledOnceFastPath)
|
||||||
|
{
|
||||||
|
TestDriver driver = testAwaitResumeCalledOnceFastPath();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, AwaitResumeCalledOnceSlowPath)
|
||||||
|
{
|
||||||
|
TestDriver driver = testAwaitResumeCalledOnceSlowPath(ioContext);
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(runDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, AwaitResumeCalledOnceNested)
|
||||||
|
{
|
||||||
|
TestDriver driver = testAwaitResumeCalledOnceNested(ioContext);
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(runDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, MoveCountedReturnNotDoubleMoved)
|
||||||
|
{
|
||||||
|
TestDriver driver = testMoveCountedReturnNotDoubleMoved();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, VoidReturnCompletes)
|
||||||
|
{
|
||||||
|
TestDriver driver = testVoidReturnCompletes();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, ReturnValuesReadableBeforeDestroy)
|
||||||
|
{
|
||||||
|
TestDriver driver = testReturnValuesReadableBeforeDestroy();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, ExceptionRethrowsOnCoAwait)
|
||||||
|
{
|
||||||
|
TestDriver driver = testExceptionRethrowsOnCoAwait();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, NonStdExceptionRethrows)
|
||||||
|
{
|
||||||
|
TestDriver driver = testNonStdExceptionRethrows();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, CalleeRunsOnCallerThread)
|
||||||
|
{
|
||||||
|
TestDriver driver = testCalleeRunsOnCallerThread();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, DelayedCalleeStillOnCallerThread)
|
||||||
|
{
|
||||||
|
TestDriver driver = testDelayedCalleeStillOnCallerThread(ioContext);
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(runDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, NestedNonPostingCoAwait)
|
||||||
|
{
|
||||||
|
TestDriver driver = testNestedNonPostingCoAwait();
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(finishDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ViralNonPostingTest, NestedInnerSuspension)
|
||||||
|
{
|
||||||
|
TestDriver driver = testNestedInnerSuspension(ioContext);
|
||||||
|
EXPECT_NO_THROW({ EXPECT_EQ(runDriver(driver), 0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ViralNonPostingGroupIntegrationTest, VoidMemberInGroup)
|
||||||
|
{
|
||||||
|
sscl::tests::PostingThreadSet threads;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
return nonPostingVoidMemberInGroupDriver(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ViralNonPostingGroupIntegrationTest, MixedImmediateAndDelayedInGroup)
|
||||||
|
{
|
||||||
|
sscl::tests::PostingThreadSet threads;
|
||||||
|
|
||||||
|
ASSERT_NO_THROW(
|
||||||
|
sscl::tests::runNonViralPostingTask(
|
||||||
|
threads.caller(),
|
||||||
|
[](
|
||||||
|
std::exception_ptr &exceptionPtr,
|
||||||
|
std::function<void()> completion)
|
||||||
|
{
|
||||||
|
return nonPostingGroupMixedImmediateAndDelayedDriver(
|
||||||
|
exceptionPtr,
|
||||||
|
std::move(completion));
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <spinscale/cps/qutex.h>
|
||||||
|
#include <spinscale/cps/lockerAndInvokerBase.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace smo {
|
||||||
|
|
||||||
|
// Mock implementation of LockerAndInvokerBase for testing
|
||||||
|
class MockLockerAndInvoker : public sscl::cps::LockerAndInvokerBase {
|
||||||
|
public:
|
||||||
|
explicit MockLockerAndInvoker(const void* addr)
|
||||||
|
: sscl::cps::LockerAndInvokerBase(addr), awakened(false) {}
|
||||||
|
|
||||||
|
bool awakened;
|
||||||
|
mutable sscl::cps::Qutex* registeredQutex = nullptr;
|
||||||
|
mutable sscl::cps::LockerAndInvokerBase::List::iterator queueIterator;
|
||||||
|
|
||||||
|
sscl::cps::LockerAndInvokerBase::List::iterator
|
||||||
|
getLockvokerIteratorForQutex(sscl::cps::Qutex& qutex) const override
|
||||||
|
{
|
||||||
|
registeredQutex = &qutex;
|
||||||
|
|
||||||
|
for (auto it = qutex.queue.begin(); it != qutex.queue.end(); ++it)
|
||||||
|
{
|
||||||
|
if ((**it) == *this)
|
||||||
|
{
|
||||||
|
queueIterator = it;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error(
|
||||||
|
"MockLockerAndInvoker: not registered in qutex queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
void awaken(bool forceAwaken = false) override
|
||||||
|
{
|
||||||
|
(void)forceAwaken;
|
||||||
|
awakened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t getLockSetSize() const override
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::cps::Qutex& getLockAt(size_t index) const override
|
||||||
|
{
|
||||||
|
if (index != 0 || registeredQutex == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
"MockLockerAndInvoker: invalid lock index or no registered qutex");
|
||||||
|
}
|
||||||
|
|
||||||
|
return *registeredQutex;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class QutexTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
// Create mock lockvokers with unique addresses
|
||||||
|
mock1 = std::make_shared<MockLockerAndInvoker>(&addr1);
|
||||||
|
mock2 = std::make_shared<MockLockerAndInvoker>(&addr2);
|
||||||
|
mock3 = std::make_shared<MockLockerAndInvoker>(&addr3);
|
||||||
|
mock4 = std::make_shared<MockLockerAndInvoker>(&addr4);
|
||||||
|
mock5 = std::make_shared<MockLockerAndInvoker>(&addr5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
// Clean up
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::cps::Qutex qutex{"test-qutex"};
|
||||||
|
std::shared_ptr<MockLockerAndInvoker> mock1, mock2, mock3, mock4, mock5;
|
||||||
|
|
||||||
|
// Unique addresses for testing
|
||||||
|
int addr1 = 1;
|
||||||
|
int addr2 = 2;
|
||||||
|
int addr3 = 3;
|
||||||
|
int addr4 = 4;
|
||||||
|
int addr5 = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test basic queue registration and unregistration
|
||||||
|
TEST_F(QutexTest, QueueRegistrationAndUnregistration) {
|
||||||
|
// Register mock1 in queue
|
||||||
|
auto it1 = qutex.registerInQueue(mock1);
|
||||||
|
EXPECT_EQ(qutex.queue.size(), 1);
|
||||||
|
EXPECT_FALSE(qutex.isOwned);
|
||||||
|
|
||||||
|
// Register mock2 in queue
|
||||||
|
auto it2 = qutex.registerInQueue(mock2);
|
||||||
|
EXPECT_EQ(qutex.queue.size(), 2);
|
||||||
|
|
||||||
|
// Unregister mock1
|
||||||
|
qutex.unregisterFromQueue(it1);
|
||||||
|
EXPECT_EQ(qutex.queue.size(), 1);
|
||||||
|
|
||||||
|
// Unregister mock2
|
||||||
|
qutex.unregisterFromQueue(it2);
|
||||||
|
EXPECT_EQ(qutex.queue.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test single lock acquisition when queue is empty
|
||||||
|
TEST_F(QutexTest, SingleLockAcquisitionEmptyQueue) {
|
||||||
|
// Register mock1
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
|
||||||
|
// Try to acquire with nRequiredLocks = 1
|
||||||
|
bool acquired = qutex.tryAcquire(*mock1, 1);
|
||||||
|
EXPECT_TRUE(acquired);
|
||||||
|
EXPECT_TRUE(qutex.isOwned);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test single lock acquisition when at front of queue
|
||||||
|
TEST_F(QutexTest, SingleLockAcquisitionAtFront) {
|
||||||
|
// Register multiple lockvokers
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
(void)qutex.registerInQueue(mock2);
|
||||||
|
(void)qutex.registerInQueue(mock3);
|
||||||
|
|
||||||
|
// mock1 should be at front, mock3 at back
|
||||||
|
EXPECT_EQ(qutex.queue.front().get(), mock1.get());
|
||||||
|
EXPECT_EQ(qutex.queue.back().get(), mock3.get());
|
||||||
|
|
||||||
|
// mock1 (at front) should succeed
|
||||||
|
bool acquired = qutex.tryAcquire(*mock1, 1);
|
||||||
|
EXPECT_TRUE(acquired);
|
||||||
|
EXPECT_TRUE(qutex.isOwned);
|
||||||
|
|
||||||
|
// mock2 (not at front) should fail
|
||||||
|
qutex.isOwned = false; // Reset for testing
|
||||||
|
bool acquired2 = qutex.tryAcquire(*mock2, 1);
|
||||||
|
EXPECT_FALSE(acquired2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test single lock acquisition failure when not at front
|
||||||
|
TEST_F(QutexTest, SingleLockAcquisitionNotAtFront) {
|
||||||
|
// Register multiple lockvokers
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
(void)qutex.registerInQueue(mock2);
|
||||||
|
|
||||||
|
// mock2 (not at front) should fail
|
||||||
|
bool acquired = qutex.tryAcquire(*mock2, 1);
|
||||||
|
EXPECT_FALSE(acquired);
|
||||||
|
EXPECT_FALSE(qutex.isOwned);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test multi-lock acquisition (nRequiredLocks > 1)
|
||||||
|
TEST_F(QutexTest, MultiLockAcquisition) {
|
||||||
|
// Register 4 lockvokers
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
(void)qutex.registerInQueue(mock2);
|
||||||
|
(void)qutex.registerInQueue(mock3);
|
||||||
|
(void)qutex.registerInQueue(mock4);
|
||||||
|
|
||||||
|
// For nRequiredLocks = 2, need to be in top 50% (top 2 out of 4)
|
||||||
|
// mock1 (position 1) should succeed
|
||||||
|
bool acquired1 = qutex.tryAcquire(*mock1, 2);
|
||||||
|
EXPECT_TRUE(acquired1);
|
||||||
|
|
||||||
|
// Reset for next test
|
||||||
|
qutex.isOwned = false;
|
||||||
|
|
||||||
|
// mock2 (position 2) should succeed
|
||||||
|
bool acquired2 = qutex.tryAcquire(*mock2, 2);
|
||||||
|
EXPECT_TRUE(acquired2);
|
||||||
|
|
||||||
|
// Reset for next test
|
||||||
|
qutex.isOwned = false;
|
||||||
|
|
||||||
|
// mock3 (position 3) should fail (in bottom 50%)
|
||||||
|
bool acquired3 = qutex.tryAcquire(*mock3, 2);
|
||||||
|
EXPECT_FALSE(acquired3);
|
||||||
|
|
||||||
|
// Reset for next test
|
||||||
|
qutex.isOwned = false;
|
||||||
|
|
||||||
|
// mock4 (position 4) should fail (in bottom 50%)
|
||||||
|
bool acquired4 = qutex.tryAcquire(*mock4, 2);
|
||||||
|
EXPECT_FALSE(acquired4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test multi-lock acquisition with 3 required locks
|
||||||
|
TEST_F(QutexTest, MultiLockAcquisitionThreeLocks) {
|
||||||
|
// Register 6 lockvokers
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
(void)qutex.registerInQueue(mock2);
|
||||||
|
(void)qutex.registerInQueue(mock3);
|
||||||
|
(void)qutex.registerInQueue(mock4);
|
||||||
|
(void)qutex.registerInQueue(mock5);
|
||||||
|
|
||||||
|
// Create one more mock
|
||||||
|
int addr6 = 6;
|
||||||
|
auto mock6 = std::make_shared<MockLockerAndInvoker>(&addr6);
|
||||||
|
(void)qutex.registerInQueue(mock6);
|
||||||
|
|
||||||
|
// For nRequiredLocks = 3, need to be in top 66% (top 4 out of 6)
|
||||||
|
// Positions 1, 2, 3, 4 should succeed
|
||||||
|
// Positions 5, 6 should fail
|
||||||
|
|
||||||
|
bool acquired1 = qutex.tryAcquire(*mock1, 3);
|
||||||
|
EXPECT_TRUE(acquired1);
|
||||||
|
qutex.isOwned = false;
|
||||||
|
|
||||||
|
bool acquired2 = qutex.tryAcquire(*mock2, 3);
|
||||||
|
EXPECT_TRUE(acquired2);
|
||||||
|
qutex.isOwned = false;
|
||||||
|
|
||||||
|
bool acquired3 = qutex.tryAcquire(*mock3, 3);
|
||||||
|
EXPECT_TRUE(acquired3);
|
||||||
|
qutex.isOwned = false;
|
||||||
|
|
||||||
|
bool acquired4 = qutex.tryAcquire(*mock4, 3);
|
||||||
|
EXPECT_TRUE(acquired4);
|
||||||
|
qutex.isOwned = false;
|
||||||
|
|
||||||
|
bool acquired5 = qutex.tryAcquire(*mock5, 3);
|
||||||
|
EXPECT_FALSE(acquired5);
|
||||||
|
|
||||||
|
qutex.isOwned = false;
|
||||||
|
bool acquired6 = qutex.tryAcquire(*mock6, 3);
|
||||||
|
EXPECT_FALSE(acquired6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test acquisition failure when already owned
|
||||||
|
TEST_F(QutexTest, AcquisitionFailureWhenOwned) {
|
||||||
|
// Register mock1
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
|
||||||
|
// Manually set as owned
|
||||||
|
qutex.isOwned = true;
|
||||||
|
|
||||||
|
// Try to acquire should fail
|
||||||
|
bool acquired = qutex.tryAcquire(*mock1, 1);
|
||||||
|
EXPECT_FALSE(acquired);
|
||||||
|
EXPECT_TRUE(qutex.isOwned);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test backoff with single item (should not rotate)
|
||||||
|
TEST_F(QutexTest, BackoffSingleItem) {
|
||||||
|
// Register only one lockvoker
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
|
||||||
|
// Set as owned first
|
||||||
|
qutex.isOwned = true;
|
||||||
|
|
||||||
|
// nRequiredLocks > 1 avoids the "front item with nRequiredLocks==1" guard
|
||||||
|
mock1->awakened = false;
|
||||||
|
qutex.backoff(*mock1, 2);
|
||||||
|
|
||||||
|
EXPECT_FALSE(qutex.isOwned);
|
||||||
|
EXPECT_EQ(qutex.queue.size(), 1u);
|
||||||
|
// Should not awaken since there's only one item
|
||||||
|
EXPECT_FALSE(mock1->awakened);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test backoff with multiple items and rotation
|
||||||
|
TEST_F(QutexTest, BackoffWithRotation) {
|
||||||
|
// Register multiple lockvokers
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
(void)qutex.registerInQueue(mock2);
|
||||||
|
(void)qutex.registerInQueue(mock3);
|
||||||
|
|
||||||
|
// Set as owned first
|
||||||
|
qutex.isOwned = true;
|
||||||
|
|
||||||
|
// mock1 should be at front initially
|
||||||
|
EXPECT_EQ(qutex.queue.front().get(), mock1.get());
|
||||||
|
|
||||||
|
// Backoff from mock1 (at front) with nRequiredLocks = 2
|
||||||
|
mock2->awakened = false;
|
||||||
|
qutex.backoff(*mock1, 2);
|
||||||
|
|
||||||
|
// mock1 should have been rotated to position 2
|
||||||
|
// mock2 should now be at front
|
||||||
|
EXPECT_EQ(qutex.queue.front().get(), mock2.get());
|
||||||
|
EXPECT_FALSE(qutex.isOwned);
|
||||||
|
|
||||||
|
// mock2 should have been awakened
|
||||||
|
EXPECT_TRUE(mock2->awakened);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test backoff with rotation to back when queue smaller than nRequiredLocks
|
||||||
|
TEST_F(QutexTest, BackoffRotationToBack) {
|
||||||
|
// Register only 2 lockvokers
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
(void)qutex.registerInQueue(mock2);
|
||||||
|
|
||||||
|
// Set as owned first
|
||||||
|
qutex.isOwned = true;
|
||||||
|
|
||||||
|
// mock1 should be at front initially
|
||||||
|
EXPECT_EQ(qutex.queue.front().get(), mock1.get());
|
||||||
|
EXPECT_EQ(qutex.queue.back().get(), mock2.get());
|
||||||
|
|
||||||
|
// Backoff from mock1 with nRequiredLocks = 5 (larger than queue size)
|
||||||
|
mock2->awakened = false;
|
||||||
|
qutex.backoff(*mock1, 5);
|
||||||
|
|
||||||
|
// mock1 should have been moved to the back
|
||||||
|
EXPECT_EQ(qutex.queue.front().get(), mock2.get());
|
||||||
|
EXPECT_EQ(qutex.queue.back().get(), mock1.get());
|
||||||
|
EXPECT_FALSE(qutex.isOwned);
|
||||||
|
|
||||||
|
// mock2 should have been awakened
|
||||||
|
EXPECT_TRUE(mock2->awakened);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test release functionality
|
||||||
|
TEST_F(QutexTest, Release) {
|
||||||
|
// Register multiple lockvokers
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
(void)qutex.registerInQueue(mock2);
|
||||||
|
|
||||||
|
ASSERT_TRUE(qutex.tryAcquire(*mock1, 1));
|
||||||
|
|
||||||
|
// Release should set isOwned to false and awaken front item
|
||||||
|
mock1->awakened = false;
|
||||||
|
qutex.release();
|
||||||
|
|
||||||
|
EXPECT_FALSE(qutex.isOwned);
|
||||||
|
EXPECT_TRUE(mock1->awakened);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test release without a prior acquire is rejected
|
||||||
|
TEST_F(QutexTest, ReleaseWithoutAcquireThrows) {
|
||||||
|
EXPECT_THROW(qutex.release(), std::runtime_error);
|
||||||
|
EXPECT_TRUE(qutex.queue.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test exception when trying to acquire from empty queue
|
||||||
|
TEST_F(QutexTest, ExceptionOnEmptyQueueAcquisition) {
|
||||||
|
// Don't register any lockvokers
|
||||||
|
EXPECT_THROW(qutex.tryAcquire(*mock1, 1), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test exception when backoff called on empty queue
|
||||||
|
TEST_F(QutexTest, ExceptionOnEmptyQueueBackoff) {
|
||||||
|
// Don't register any lockvokers
|
||||||
|
EXPECT_THROW(qutex.backoff(*mock1, 1), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test edge case: single lockvoker with multiple required locks
|
||||||
|
TEST_F(QutexTest, SingleLockvokerMultipleRequiredLocks) {
|
||||||
|
// Register only one lockvoker
|
||||||
|
(void)qutex.registerInQueue(mock1);
|
||||||
|
|
||||||
|
// Should succeed regardless of nRequiredLocks when only one item
|
||||||
|
bool acquired = qutex.tryAcquire(*mock1, 5);
|
||||||
|
EXPECT_TRUE(acquired);
|
||||||
|
EXPECT_TRUE(qutex.isOwned);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test unregistration without locking
|
||||||
|
TEST_F(QutexTest, UnregistrationWithoutLocking) {
|
||||||
|
// Register lockvoker
|
||||||
|
auto it1 = qutex.registerInQueue(mock1);
|
||||||
|
EXPECT_EQ(qutex.queue.size(), 1);
|
||||||
|
|
||||||
|
// Unregister without locking
|
||||||
|
qutex.unregisterFromQueue(it1, false);
|
||||||
|
EXPECT_EQ(qutex.queue.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace smo
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <spinscale/envKvStore.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char *kTestEnvName = "SSCL_ENV_TEST_VALUE";
|
||||||
|
constexpr const char *kPositiveIntMsEnvName = "SSCL_POSITIVE_INT_MS";
|
||||||
|
constexpr int kPositiveIntMsDefault = 33;
|
||||||
|
|
||||||
|
void unsetTestEnvVars()
|
||||||
|
{
|
||||||
|
unsetenv(kTestEnvName);
|
||||||
|
unsetenv(kPositiveIntMsEnvName);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvKvStoreTest
|
||||||
|
: public testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
root = std::filesystem::temp_directory_path()
|
||||||
|
/ ("spinscale-env-test-"
|
||||||
|
+ std::to_string(std::chrono::steady_clock::now()
|
||||||
|
.time_since_epoch().count())
|
||||||
|
+ "-" + std::to_string(testCounter++));
|
||||||
|
std::filesystem::create_directories(root);
|
||||||
|
unsetTestEnvVars();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override
|
||||||
|
{
|
||||||
|
unsetTestEnvVars();
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path writeFile(
|
||||||
|
const std::string &filename,
|
||||||
|
const std::string &contents)
|
||||||
|
{
|
||||||
|
std::filesystem::path path = root / filename;
|
||||||
|
std::ofstream file(path);
|
||||||
|
file << contents;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectParseErrorContaining(
|
||||||
|
const std::filesystem::path &envFile,
|
||||||
|
const std::string &expectedFragment)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
FAIL() << "Expected parse of " << envFile << " to throw.";
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &e)
|
||||||
|
{
|
||||||
|
std::string message = e.what();
|
||||||
|
EXPECT_NE(message.find(envFile.string()), std::string::npos)
|
||||||
|
<< message;
|
||||||
|
EXPECT_NE(message.find(expectedFragment), std::string::npos)
|
||||||
|
<< message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path root;
|
||||||
|
static inline int testCounter = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, ParsesSupportedDotenvForms)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"one.env",
|
||||||
|
"\n"
|
||||||
|
"# comment\n"
|
||||||
|
"PLAIN=value\n"
|
||||||
|
" TRIMMED = value with spaces \n"
|
||||||
|
"SINGLE=' preserved value '\n"
|
||||||
|
"DOUBLE=\"another preserved value\"\n"
|
||||||
|
"ESCAPED=\"quote: \\\" slash: \\\\ tab: \\t\"\n"
|
||||||
|
"COMMENTED=value # comment\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("PLAIN"), "value");
|
||||||
|
EXPECT_EQ(store.find("TRIMMED"), "value with spaces");
|
||||||
|
EXPECT_EQ(store.find("SINGLE"), " preserved value ");
|
||||||
|
EXPECT_EQ(store.find("DOUBLE"), "another preserved value");
|
||||||
|
EXPECT_EQ(store.find("ESCAPED"), "quote: \" slash: \\ tab: \t");
|
||||||
|
EXPECT_EQ(store.find("COMMENTED"), "value");
|
||||||
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, EmptyPathListYieldsEmptyStore)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("ANY"), std::nullopt);
|
||||||
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, EmptyFileYieldsEmptyStore)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile("empty.env", "");
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("ANY"), std::nullopt);
|
||||||
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, CommentOnlyAndWhitespaceOnlyLinesAreIgnored)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"comments.env",
|
||||||
|
" \n"
|
||||||
|
"\t\n"
|
||||||
|
"# only comment\n"
|
||||||
|
" # indented comment\n"
|
||||||
|
"KEEP=yes\n"
|
||||||
|
"\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("KEEP"), "yes");
|
||||||
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, EmptyUnquotedValueIsAccepted)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile("empty-value.env", "EMPTY=\n");
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("EMPTY"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, UnquotedHashStartsInlineComment)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"hash.env",
|
||||||
|
"A=before#after\n"
|
||||||
|
"B= # leading comment after equals\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("A"), "before");
|
||||||
|
EXPECT_EQ(store.find("B"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, HashInsideQuotesIsLiteral)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"hash-quoted.env",
|
||||||
|
"SINGLE='#not-comment'\n"
|
||||||
|
"DOUBLE=\"#not-comment\"\n"
|
||||||
|
"DOUBLE_TRAIL=\"kept\" # trailing comment ok\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("SINGLE"), "#not-comment");
|
||||||
|
EXPECT_EQ(store.find("DOUBLE"), "#not-comment");
|
||||||
|
EXPECT_EQ(store.find("DOUBLE_TRAIL"), "kept");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, DoubleQuotedEscapeSequences)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"escapes.env",
|
||||||
|
"NL=\"line\\nbreak\"\n"
|
||||||
|
"CR=\"ret\\rurn\"\n"
|
||||||
|
"TAB=\"a\\tb\"\n"
|
||||||
|
"UNKNOWN=\"\\q\"\n"
|
||||||
|
"TRAILING=\"end\\\\\"\n"
|
||||||
|
"ESCAPED_QUOTE_MID=\"a\\\"b\"\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("NL"), "line\nbreak");
|
||||||
|
EXPECT_EQ(store.find("CR"), "ret\rurn");
|
||||||
|
EXPECT_EQ(store.find("TAB"), "a\tb");
|
||||||
|
EXPECT_EQ(store.find("UNKNOWN"), "q");
|
||||||
|
EXPECT_EQ(store.find("TRAILING"), "end\\");
|
||||||
|
EXPECT_EQ(store.find("ESCAPED_QUOTE_MID"), "a\"b");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, SingleQuotedValuesDoNotDecodeEscapes)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"single-escapes.env",
|
||||||
|
"LITERAL='\\n\\t\\\"'\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("LITERAL"), "\\n\\t\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, ValidNamesAcceptUnderscoreAndAlnum)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"names.env",
|
||||||
|
"_LEADING=1\n"
|
||||||
|
"A1B2=2\n"
|
||||||
|
"mixed_Case99=3\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("_LEADING"), "1");
|
||||||
|
EXPECT_EQ(store.find("A1B2"), "2");
|
||||||
|
EXPECT_EQ(store.find("mixed_Case99"), "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, InvalidNamesThrow)
|
||||||
|
{
|
||||||
|
expectParseErrorContaining(
|
||||||
|
writeFile("digit.env", "1BAD=x\n"),
|
||||||
|
"Invalid variable name.");
|
||||||
|
expectParseErrorContaining(
|
||||||
|
writeFile("hyphen.env", "BAD-NAME=x\n"),
|
||||||
|
"Invalid variable name.");
|
||||||
|
expectParseErrorContaining(
|
||||||
|
writeFile("dot.env", "BAD.NAME=x\n"),
|
||||||
|
"Invalid variable name.");
|
||||||
|
expectParseErrorContaining(
|
||||||
|
writeFile("empty-name.env", "=value\n"),
|
||||||
|
"Invalid variable name.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, UnterminatedQuotedValueThrows)
|
||||||
|
{
|
||||||
|
expectParseErrorContaining(
|
||||||
|
writeFile("unterminated-double.env", "X=\"no close\n"),
|
||||||
|
"Unterminated quoted value.");
|
||||||
|
expectParseErrorContaining(
|
||||||
|
writeFile("unterminated-single.env", "X='no close\n"),
|
||||||
|
"Unterminated quoted value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, UnexpectedTextAfterQuotedValueThrows)
|
||||||
|
{
|
||||||
|
expectParseErrorContaining(
|
||||||
|
writeFile("trail.env", "X=\"ok\" trailing\n"),
|
||||||
|
"Unexpected text after quoted value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, MalformedLineThrowsWithLineNumber)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"bad.env",
|
||||||
|
"# header\n"
|
||||||
|
"GOOD=1\n"
|
||||||
|
"NOT AN ASSIGNMENT\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
FAIL() << "Expected malformed env file to throw.";
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &e)
|
||||||
|
{
|
||||||
|
std::string message = e.what();
|
||||||
|
EXPECT_NE(message.find(envFile.string()), std::string::npos);
|
||||||
|
EXPECT_NE(message.find(":3:"), std::string::npos) << message;
|
||||||
|
EXPECT_NE(message.find("Expected KEY=value."), std::string::npos)
|
||||||
|
<< message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, MissingFileThrows)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
EXPECT_THROW(
|
||||||
|
sscl::EnvKvStore({root / "missing.env"}, warnings),
|
||||||
|
std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, UnreadableFileThrowsOpenFailure)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile("noread.env", "X=1\n");
|
||||||
|
std::filesystem::permissions(envFile, std::filesystem::perms::none);
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
FAIL() << "Expected unreadable file open to throw.";
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &e)
|
||||||
|
{
|
||||||
|
EXPECT_NE(
|
||||||
|
std::string(e.what()).find("Failed to open env file:"),
|
||||||
|
std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::permissions(
|
||||||
|
envFile,
|
||||||
|
std::filesystem::perms::owner_read
|
||||||
|
| std::filesystem::perms::owner_write);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, LaterFilesOverwriteEarlierFilesAndWarn)
|
||||||
|
{
|
||||||
|
std::filesystem::path first = writeFile("first.env", "VALUE=first\n");
|
||||||
|
std::filesystem::path second = writeFile("second.env", "VALUE=second\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({first, second}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("VALUE"), "second");
|
||||||
|
EXPECT_NE(warnings.str().find("VALUE"), std::string::npos);
|
||||||
|
EXPECT_NE(warnings.str().find("first"), std::string::npos);
|
||||||
|
EXPECT_NE(warnings.str().find("second"), std::string::npos);
|
||||||
|
EXPECT_NE(warnings.str().find(second.string()), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, ThreeFileOverwriteKeepsLastValue)
|
||||||
|
{
|
||||||
|
std::filesystem::path a = writeFile("a.env", "K=a\nSHARED=1\n");
|
||||||
|
std::filesystem::path b = writeFile("b.env", "SHARED=2\n");
|
||||||
|
std::filesystem::path c = writeFile("c.env", "SHARED=3\nK=c\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({a, b, c}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("K"), "c");
|
||||||
|
EXPECT_EQ(store.find("SHARED"), "3");
|
||||||
|
const std::string warningText = warnings.str();
|
||||||
|
EXPECT_GE(
|
||||||
|
std::count(warningText.begin(), warningText.end(), '\n'),
|
||||||
|
2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, DuplicateKeysInsideSameFileOverwriteAndWarn)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile =
|
||||||
|
writeFile("one.env", "VALUE=first\nVALUE=second\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("VALUE"), "second");
|
||||||
|
EXPECT_NE(warnings.str().find("VALUE"), std::string::npos);
|
||||||
|
EXPECT_NE(warnings.str().find("first"), std::string::npos);
|
||||||
|
EXPECT_NE(warnings.str().find("second"), std::string::npos);
|
||||||
|
EXPECT_NE(warnings.str().find(envFile.string()), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, ProcessEnvironmentOverridesStoreSilently)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile =
|
||||||
|
writeFile("one.env", "SSCL_ENV_TEST_VALUE=file\n");
|
||||||
|
setenv(kTestEnvName, "process", 1);
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find(kTestEnvName), "process");
|
||||||
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, EmptyProcessEnvironmentValueOverridesStoreSilently)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile =
|
||||||
|
writeFile("one.env", "SSCL_ENV_TEST_VALUE=file\n");
|
||||||
|
setenv(kTestEnvName, "", 1);
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find(kTestEnvName), "");
|
||||||
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, ProcessEnvironmentReturnedWhenKeyAbsentFromStore)
|
||||||
|
{
|
||||||
|
setenv(kTestEnvName, "only-process", 1);
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find(kTestEnvName), "only-process");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, BypassProcessEnvironmentUsesFileStoreOnly)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile =
|
||||||
|
writeFile("one.env", "SSCL_ENV_TEST_VALUE=file\n");
|
||||||
|
setenv(kTestEnvName, "process", 1);
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find(kTestEnvName, true), "file");
|
||||||
|
EXPECT_EQ(store.find(kTestEnvName, false), "process");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, BypassIgnoresProcessEnvWhenKeyOnlyInProcess)
|
||||||
|
{
|
||||||
|
setenv(kTestEnvName, "process-only", 1);
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find(kTestEnvName, true), std::nullopt);
|
||||||
|
EXPECT_EQ(store.find(kTestEnvName, false), "process-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, MissingKeyReturnsNullopt)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile("one.env", "PRESENT=1\n");
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.find("PRESENT"), "1");
|
||||||
|
EXPECT_EQ(store.find("ABSENT"), std::nullopt);
|
||||||
|
EXPECT_EQ(store.find("ABSENT", true), std::nullopt);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, DefaultWarningCtorLoadsWithoutThrowing)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile("one.env", "OK=1\n");
|
||||||
|
sscl::EnvKvStore store({envFile});
|
||||||
|
EXPECT_EQ(store.find("OK"), "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetThrowsWhenKeyMissing)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({}, warnings);
|
||||||
|
EXPECT_THROW(store.get("ABSENT"), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetReturnsPresentValue)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store(
|
||||||
|
{writeFile("one.env", "PRESENT=1\n")},
|
||||||
|
warnings);
|
||||||
|
EXPECT_EQ(store.get("PRESENT"), "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetIntUsesDefaultWhenUnset)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.getInt(kPositiveIntMsEnvName, kPositiveIntMsDefault), kPositiveIntMsDefault);
|
||||||
|
EXPECT_EQ(store.getInt("CUSTOM", 42), 42);
|
||||||
|
EXPECT_THROW(store.getInt("MISSING"), std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetIntParsesSignedValues)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store(
|
||||||
|
{writeFile("one.env", "NEG=-7\nZERO=0\n")},
|
||||||
|
warnings);
|
||||||
|
EXPECT_EQ(store.getInt("NEG"), -7);
|
||||||
|
EXPECT_EQ(store.getInt("ZERO"), 0);
|
||||||
|
EXPECT_EQ(store.getPositiveInt("ZERO"), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetPositiveNonZeroIntUsesDefaultWhenUnset)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
store.getPositiveNonZeroInt(kPositiveIntMsEnvName, kPositiveIntMsDefault),
|
||||||
|
kPositiveIntMsDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetPositiveNonZeroIntParsesFileValue)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"one.env",
|
||||||
|
std::string(kPositiveIntMsEnvName) + "=50\n");
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
store.getPositiveNonZeroInt(kPositiveIntMsEnvName, kPositiveIntMsDefault),
|
||||||
|
50);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetPositiveNonZeroIntHonorsProcessEnvironmentOverFile)
|
||||||
|
{
|
||||||
|
std::filesystem::path envFile = writeFile(
|
||||||
|
"one.env",
|
||||||
|
std::string(kPositiveIntMsEnvName) + "=50\n");
|
||||||
|
setenv(kPositiveIntMsEnvName, "77", 1);
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
store.getPositiveNonZeroInt(kPositiveIntMsEnvName, kPositiveIntMsDefault),
|
||||||
|
77);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetPositiveNonZeroIntRejectsNonPositive)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore zeroStore(
|
||||||
|
{writeFile(
|
||||||
|
"zero.env",
|
||||||
|
std::string(kPositiveIntMsEnvName) + "=0\n")},
|
||||||
|
warnings);
|
||||||
|
EXPECT_THROW(
|
||||||
|
zeroStore.getPositiveNonZeroInt(
|
||||||
|
kPositiveIntMsEnvName, kPositiveIntMsDefault),
|
||||||
|
std::runtime_error);
|
||||||
|
|
||||||
|
sscl::EnvKvStore negativeStore(
|
||||||
|
{writeFile(
|
||||||
|
"neg.env",
|
||||||
|
std::string(kPositiveIntMsEnvName) + "=-3\n")},
|
||||||
|
warnings);
|
||||||
|
EXPECT_THROW(
|
||||||
|
negativeStore.getPositiveNonZeroInt(
|
||||||
|
kPositiveIntMsEnvName, kPositiveIntMsDefault),
|
||||||
|
std::runtime_error);
|
||||||
|
EXPECT_THROW(
|
||||||
|
negativeStore.getPositiveInt(
|
||||||
|
kPositiveIntMsEnvName, kPositiveIntMsDefault),
|
||||||
|
std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetPositiveNonZeroIntRejectsNonNumericAndTrailingJunk)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
|
||||||
|
sscl::EnvKvStore letters(
|
||||||
|
{writeFile(
|
||||||
|
"letters.env",
|
||||||
|
std::string(kPositiveIntMsEnvName) + "=abc\n")},
|
||||||
|
warnings);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(void)letters.getPositiveNonZeroInt(
|
||||||
|
kPositiveIntMsEnvName, kPositiveIntMsDefault);
|
||||||
|
FAIL() << "Expected non-numeric parse to throw.";
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &e)
|
||||||
|
{
|
||||||
|
EXPECT_NE(
|
||||||
|
std::string(e.what()).find("failed to parse"),
|
||||||
|
std::string::npos)
|
||||||
|
<< e.what();
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::EnvKvStore trailing(
|
||||||
|
{writeFile(
|
||||||
|
"trailing.env",
|
||||||
|
std::string(kPositiveIntMsEnvName) + "=50ms\n")},
|
||||||
|
warnings);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(void)trailing.getPositiveNonZeroInt(
|
||||||
|
kPositiveIntMsEnvName, kPositiveIntMsDefault);
|
||||||
|
FAIL() << "Expected trailing junk to throw.";
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &e)
|
||||||
|
{
|
||||||
|
EXPECT_NE(
|
||||||
|
std::string(e.what()).find("must be an integer"),
|
||||||
|
std::string::npos)
|
||||||
|
<< e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetPositiveNonZeroIntRejectsEmptyStringValue)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store(
|
||||||
|
{writeFile(
|
||||||
|
"empty.env",
|
||||||
|
std::string(kPositiveIntMsEnvName) + "=\n")},
|
||||||
|
warnings);
|
||||||
|
|
||||||
|
EXPECT_THROW(
|
||||||
|
store.getPositiveNonZeroInt(kPositiveIntMsEnvName, kPositiveIntMsDefault),
|
||||||
|
std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(EnvKvStoreTest, GetPositiveNonZeroIntAcceptsOne)
|
||||||
|
{
|
||||||
|
std::ostringstream warnings;
|
||||||
|
sscl::EnvKvStore store(
|
||||||
|
{writeFile("one.env", "CUSTOM=1\n")},
|
||||||
|
warnings);
|
||||||
|
|
||||||
|
EXPECT_EQ(store.getPositiveNonZeroInt("CUSTOM", 99), 1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#ifndef SPINSCALE_TEST_SUPPORT_BAKED_DEVICE_CATALOG_H
|
||||||
|
#define SPINSCALE_TEST_SUPPORT_BAKED_DEVICE_CATALOG_H
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <bakedCameraProfiles.h>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
inline std::vector<const test_fixtures::BakedCameraProfile *>
|
||||||
|
profilesForMachine(const char *machineTag)
|
||||||
|
{
|
||||||
|
std::vector<const test_fixtures::BakedCameraProfile *> matches;
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
|
||||||
|
{
|
||||||
|
const test_fixtures::BakedCameraProfile& profile =
|
||||||
|
test_fixtures::bakedCameraProfiles[i];
|
||||||
|
|
||||||
|
if (std::string(profile.machineTag) == machineTag) {
|
||||||
|
matches.push_back(&profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::optional<const test_fixtures::BakedCameraProfile *>
|
||||||
|
findProfileByTag(const char *machineTag, const char *profileTag)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
|
||||||
|
{
|
||||||
|
const test_fixtures::BakedCameraProfile& profile =
|
||||||
|
test_fixtures::bakedCameraProfiles[i];
|
||||||
|
|
||||||
|
if (std::string(profile.machineTag) == machineTag
|
||||||
|
&& std::string(profile.profileTag) == profileTag)
|
||||||
|
{
|
||||||
|
return &profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<const test_fixtures::BakedCameraProfile *>
|
||||||
|
requiredProfilesForMachine(const char *machineTag)
|
||||||
|
{
|
||||||
|
std::vector<const test_fixtures::BakedCameraProfile *> matches;
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
|
||||||
|
{
|
||||||
|
const test_fixtures::BakedCameraProfile& profile =
|
||||||
|
test_fixtures::bakedCameraProfiles[i];
|
||||||
|
|
||||||
|
if (std::string(profile.machineTag) == machineTag
|
||||||
|
&& profile.requiredOnMachine)
|
||||||
|
{
|
||||||
|
matches.push_back(&profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
#endif // SPINSCALE_TEST_SUPPORT_BAKED_DEVICE_CATALOG_H
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef SPINSCALE_TEST_SUPPORT_COROUTINE_DRIVER_H
|
||||||
|
#define SPINSCALE_TEST_SUPPORT_COROUTINE_DRIVER_H
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
|
#include <support/threadHarness.h>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
class CoroutineDriver
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
template <typename Invoker>
|
||||||
|
static auto completedReturnValue(Invoker &invoker)
|
||||||
|
{
|
||||||
|
if (invoker.completedReturnValues().myExceptionPtr) {
|
||||||
|
std::rethrow_exception(
|
||||||
|
invoker.completedReturnValues().myExceptionPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoker.completedReturnValues().myReturnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Invoker>
|
||||||
|
static auto pumpUntilIdleAndReturnValue(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
Invoker &invoker)
|
||||||
|
{
|
||||||
|
IoContextPump::pumpUntilIdle(ioContext);
|
||||||
|
return completedReturnValue(invoker);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
#endif // SPINSCALE_TEST_SUPPORT_COROUTINE_DRIVER_H
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#ifndef SPINSCALE_TEST_SUPPORT_EXCEPTION_ASSERTIONS_H
|
||||||
|
#define SPINSCALE_TEST_SUPPORT_EXCEPTION_ASSERTIONS_H
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
inline void requireExceptionMessageContains(
|
||||||
|
const std::exception &exception,
|
||||||
|
const std::string &expectedSubstring)
|
||||||
|
{
|
||||||
|
const std::string message = exception.what();
|
||||||
|
if (message.find(expectedSubstring) == std::string::npos) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Expected exception message to contain \""
|
||||||
|
+ expectedSubstring
|
||||||
|
+ "\", got \""
|
||||||
|
+ message
|
||||||
|
+ "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void expectExceptionMessageContains(
|
||||||
|
const std::exception &exception,
|
||||||
|
const std::string &expectedSubstring)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(
|
||||||
|
requireExceptionMessageContains(exception, expectedSubstring));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void requireExceptionPtrMessageContains(
|
||||||
|
const std::exception_ptr &exceptionPtr,
|
||||||
|
const std::string &expectedSubstring)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
std::rethrow_exception(exceptionPtr);
|
||||||
|
}
|
||||||
|
catch (const std::exception &exception) {
|
||||||
|
requireExceptionMessageContains(exception, expectedSubstring);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
throw std::runtime_error("Expected std::exception in exception_ptr");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void expectExceptionPtrMessageContains(
|
||||||
|
const std::exception_ptr &exceptionPtr,
|
||||||
|
const std::string &expectedSubstring)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(
|
||||||
|
requireExceptionPtrMessageContains(
|
||||||
|
exceptionPtr,
|
||||||
|
expectedSubstring));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
#endif // SPINSCALE_TEST_SUPPORT_EXCEPTION_ASSERTIONS_H
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#ifndef SPINSCALE_TEST_SUPPORT_GROUP_ASSERTIONS_H
|
||||||
|
#define SPINSCALE_TEST_SUPPORT_GROUP_ASSERTIONS_H
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <spinscale/co/group.h>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
template <typename Invoker>
|
||||||
|
int completedIntValue(Invoker &invoker)
|
||||||
|
{
|
||||||
|
if (invoker.completedReturnValues().myExceptionPtr) {
|
||||||
|
std::rethrow_exception(
|
||||||
|
invoker.completedReturnValues().myExceptionPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoker.completedReturnValues().myReturnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void requireCompletedSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor)
|
||||||
|
{
|
||||||
|
if (descriptor.type !=
|
||||||
|
sscl::co::Group::SettlementDescriptor::TypeE::COMPLETED)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Expected completed settlement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Invoker>
|
||||||
|
void requireCompletedIntSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||||
|
int expectedValue)
|
||||||
|
{
|
||||||
|
requireCompletedSettlement(descriptor);
|
||||||
|
|
||||||
|
const int actualValue = completedIntValue(descriptor.invokerAs<Invoker>());
|
||||||
|
if (actualValue != expectedValue) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Expected completed settlement value "
|
||||||
|
+ std::to_string(expectedValue)
|
||||||
|
+ ", got "
|
||||||
|
+ std::to_string(actualValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Invoker>
|
||||||
|
void expectCompletedIntSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||||
|
int expectedValue)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(
|
||||||
|
requireCompletedIntSettlement<Invoker>(
|
||||||
|
descriptor,
|
||||||
|
expectedValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void expectCompletedSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(requireCompletedSettlement(descriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void requireExceptionSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor)
|
||||||
|
{
|
||||||
|
if (descriptor.type !=
|
||||||
|
sscl::co::Group::SettlementDescriptor::TypeE::EXCEPTION_THROWN)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Expected exception settlement");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!descriptor.calleeException) {
|
||||||
|
throw std::runtime_error("Expected exception pointer in settlement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void expectExceptionSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(requireExceptionSettlement(descriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void requireRuntimeErrorSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||||
|
const std::string &expectedMessage)
|
||||||
|
{
|
||||||
|
requireExceptionSettlement(descriptor);
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::rethrow_exception(descriptor.calleeException);
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error &runtimeError) {
|
||||||
|
const std::string actualMessage = runtimeError.what();
|
||||||
|
if (actualMessage != expectedMessage) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Expected runtime_error settlement message \""
|
||||||
|
+ expectedMessage
|
||||||
|
+ "\", got \""
|
||||||
|
+ actualMessage
|
||||||
|
+ "\"");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
throw std::runtime_error("Expected std::runtime_error settlement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void requireIntExceptionSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||||
|
int expectedValue)
|
||||||
|
{
|
||||||
|
requireExceptionSettlement(descriptor);
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::rethrow_exception(descriptor.calleeException);
|
||||||
|
}
|
||||||
|
catch (int caughtValue) {
|
||||||
|
if (caughtValue != expectedValue) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Expected int exception settlement value "
|
||||||
|
+ std::to_string(expectedValue)
|
||||||
|
+ ", got "
|
||||||
|
+ std::to_string(caughtValue));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
throw std::runtime_error("Expected int exception settlement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void expectIntExceptionSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||||
|
int expectedValue)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(
|
||||||
|
requireIntExceptionSettlement(
|
||||||
|
descriptor,
|
||||||
|
expectedValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void expectRuntimeErrorSettlement(
|
||||||
|
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||||
|
const std::string &expectedMessage)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(
|
||||||
|
requireRuntimeErrorSettlement(
|
||||||
|
descriptor,
|
||||||
|
expectedMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void requireEmptyGroupError(
|
||||||
|
const std::runtime_error &runtimeError)
|
||||||
|
{
|
||||||
|
constexpr const char *expectedMessage =
|
||||||
|
"co_await: Group has no member invokers; call add() before awaiting";
|
||||||
|
if (std::string(runtimeError.what()) != expectedMessage) {
|
||||||
|
throw std::runtime_error("Unexpected empty group error message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void expectEmptyGroupError(
|
||||||
|
const std::runtime_error &runtimeError)
|
||||||
|
{
|
||||||
|
EXPECT_NO_THROW(requireEmptyGroupError(runtimeError));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
#endif // SPINSCALE_TEST_SUPPORT_GROUP_ASSERTIONS_H
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#ifndef SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
|
||||||
|
#define SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
|
||||||
|
|
||||||
|
/** EXPLANATION:
|
||||||
|
* Compatibility shim: probe harness lives in spinscale_probe_support under
|
||||||
|
* sscl::probe. Test code may keep including this path and using sscl::tests
|
||||||
|
* names; tools should include <probe/probeComponentThread.h> directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <probe/probeComponentThread.h>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
using sscl::probe::defaultProbeTaskTimeout;
|
||||||
|
using sscl::probe::runNonViralNurseryOnComponentThread;
|
||||||
|
using sscl::probe::ProbeComponentThreadHarness;
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
#endif // SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
#include <support/threadHarness.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
struct DedicatedIoThread::StartupState
|
||||||
|
{
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable condition;
|
||||||
|
std::thread::id osThreadId;
|
||||||
|
std::exception_ptr startupException;
|
||||||
|
bool allowInitialization = false;
|
||||||
|
bool initialized = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char *callerThreadName = "test:caller";
|
||||||
|
constexpr const char *calleeThreadName = "test:callee";
|
||||||
|
constexpr const char *alternateThreadName = "test:alternate";
|
||||||
|
constexpr const char *bodyThreadName = "test:body";
|
||||||
|
constexpr const char *worldThreadName = "test:world";
|
||||||
|
constexpr const char *legThreadName = "test:leg";
|
||||||
|
|
||||||
|
void runDedicatedThread(
|
||||||
|
const std::shared_ptr<DedicatedIoThread::StartupState> &state,
|
||||||
|
const sscl::PuppeteerThread::EntryFnArguments &args)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(state->mutex);
|
||||||
|
state->condition.wait(
|
||||||
|
lock,
|
||||||
|
[&state]() { return state->allowInitialization; });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
args.usableBeforeJolt.initializeTls();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(state->mutex);
|
||||||
|
state->osThreadId = std::this_thread::get_id();
|
||||||
|
state->initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->condition.notify_all();
|
||||||
|
|
||||||
|
args.usableBeforeJolt.getIoContext().restart();
|
||||||
|
args.usableBeforeJolt.getIoContext().run();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(state->mutex);
|
||||||
|
state->startupException = std::current_exception();
|
||||||
|
state->initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->condition.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string threadRoleName(PostingThreadRole role)
|
||||||
|
{
|
||||||
|
switch (role)
|
||||||
|
{
|
||||||
|
case PostingThreadRole::CALLER:
|
||||||
|
return callerThreadName;
|
||||||
|
case PostingThreadRole::CALLEE:
|
||||||
|
return calleeThreadName;
|
||||||
|
case PostingThreadRole::ALTERNATE:
|
||||||
|
return alternateThreadName;
|
||||||
|
case PostingThreadRole::BODY:
|
||||||
|
return bodyThreadName;
|
||||||
|
case PostingThreadRole::WORLD:
|
||||||
|
return worldThreadName;
|
||||||
|
case PostingThreadRole::LEG:
|
||||||
|
return legThreadName;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error("Unknown PostingThreadRole");
|
||||||
|
}
|
||||||
|
|
||||||
|
void IoContextPump::pumpUntilIdle(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
std::chrono::milliseconds idleTimeout,
|
||||||
|
std::chrono::milliseconds totalTimeout)
|
||||||
|
{
|
||||||
|
const auto totalDeadline =
|
||||||
|
std::chrono::steady_clock::now() + totalTimeout;
|
||||||
|
auto lastProgress = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
while (std::chrono::steady_clock::now() < totalDeadline)
|
||||||
|
{
|
||||||
|
if (ioContext.poll_one() > 0)
|
||||||
|
{
|
||||||
|
lastProgress = std::chrono::steady_clock::now();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::chrono::steady_clock::now() - lastProgress >= idleTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadBoundComponent::ThreadBoundComponent()
|
||||||
|
: sscl::pptr::PuppeteerComponent(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadBoundComponent::handleLoopExceptionHook()
|
||||||
|
{
|
||||||
|
loopException = std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread::DedicatedIoThread(PostingThreadRole roleIn)
|
||||||
|
: role(roleIn),
|
||||||
|
startupState(std::make_shared<StartupState>()),
|
||||||
|
component(),
|
||||||
|
thread(std::make_shared<sscl::PuppeteerThread>(
|
||||||
|
static_cast<sscl::ThreadId>(roleIn),
|
||||||
|
threadRoleName(roleIn),
|
||||||
|
[state = startupState](
|
||||||
|
const sscl::PuppeteerThread::EntryFnArguments &args)
|
||||||
|
{
|
||||||
|
runDedicatedThread(state, args);
|
||||||
|
},
|
||||||
|
component,
|
||||||
|
nullptr))
|
||||||
|
{
|
||||||
|
component.thread = thread;
|
||||||
|
releaseStartupBarrier();
|
||||||
|
waitUntilInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread::~DedicatedIoThread()
|
||||||
|
{
|
||||||
|
stopAndJoin();
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::io_context &DedicatedIoThread::ioContext()
|
||||||
|
{
|
||||||
|
return thread->getIoContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
sscl::ThreadId DedicatedIoThread::threadId() const noexcept
|
||||||
|
{
|
||||||
|
return static_cast<sscl::ThreadId>(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id DedicatedIoThread::osThreadId() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(startupState->mutex);
|
||||||
|
return startupState->osThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<sscl::PuppeteerThread> DedicatedIoThread::componentThread() const
|
||||||
|
{
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DedicatedIoThread::stopAndJoin()
|
||||||
|
{
|
||||||
|
if (!thread) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseStartupBarrier();
|
||||||
|
thread->getIoContext().stop();
|
||||||
|
|
||||||
|
if (thread->thread.joinable()) {
|
||||||
|
thread->thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
thread.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DedicatedIoThread::releaseStartupBarrier()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(startupState->mutex);
|
||||||
|
startupState->allowInitialization = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
startupState->condition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DedicatedIoThread::waitUntilInitialized()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(startupState->mutex);
|
||||||
|
const bool initialized = startupState->condition.wait_for(
|
||||||
|
lock,
|
||||||
|
defaultPostingTaskTimeout,
|
||||||
|
[this]() { return startupState->initialized; });
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
throw std::runtime_error("Timed out waiting for test thread startup");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::exception_ptr startupException = startupState->startupException;
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
if (startupException) {
|
||||||
|
std::rethrow_exception(startupException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadRegistry::registerThread(
|
||||||
|
PostingThreadRole role,
|
||||||
|
DedicatedIoThread &thread)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(registryMutex());
|
||||||
|
auto [iterator, inserted] = threadsByRole().emplace(role, &thread);
|
||||||
|
|
||||||
|
if (!inserted) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Test thread role already registered for " + threadRoleName(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadRegistry::unregisterThread(
|
||||||
|
PostingThreadRole role,
|
||||||
|
DedicatedIoThread &expectedThread)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(registryMutex());
|
||||||
|
auto iterator = threadsByRole().find(role);
|
||||||
|
|
||||||
|
if (iterator == threadsByRole().end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iterator->second != &expectedThread) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Test thread role registered to a different thread for "
|
||||||
|
+ threadRoleName(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
threadsByRole().erase(iterator);
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::io_context &ThreadRegistry::ioContext(PostingThreadRole role)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(registryMutex());
|
||||||
|
auto iterator = threadsByRole().find(role);
|
||||||
|
|
||||||
|
if (iterator == threadsByRole().end()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"No test thread registered for " + threadRoleName(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
return iterator->second->ioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id ThreadRegistry::osThreadId(PostingThreadRole role)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(registryMutex());
|
||||||
|
auto iterator = threadsByRole().find(role);
|
||||||
|
|
||||||
|
if (iterator == threadsByRole().end()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"No test thread registered for " + threadRoleName(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
return iterator->second->osThreadId();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex &ThreadRegistry::registryMutex()
|
||||||
|
{
|
||||||
|
static std::mutex mutex;
|
||||||
|
return mutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<PostingThreadRole, DedicatedIoThread *> &
|
||||||
|
ThreadRegistry::threadsByRole()
|
||||||
|
{
|
||||||
|
static std::map<PostingThreadRole, DedicatedIoThread *> threads;
|
||||||
|
return threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
PostingThreadSet::PostingThreadSet()
|
||||||
|
: callerThread(PostingThreadRole::CALLER),
|
||||||
|
calleeThread(PostingThreadRole::CALLEE),
|
||||||
|
alternateThread(PostingThreadRole::ALTERNATE),
|
||||||
|
bodyThread(PostingThreadRole::BODY),
|
||||||
|
worldThread(PostingThreadRole::WORLD),
|
||||||
|
legThread(PostingThreadRole::LEG)
|
||||||
|
{
|
||||||
|
previousPuppeteerThread = sscl::ComponentThread::getPptr();
|
||||||
|
previousPuppeteerThreadId = sscl::pptr::puppeteerThreadId;
|
||||||
|
registerAllThreads();
|
||||||
|
installCallerAsPuppeteer();
|
||||||
|
}
|
||||||
|
|
||||||
|
PostingThreadSet::~PostingThreadSet()
|
||||||
|
{
|
||||||
|
restorePreviousPuppeteer();
|
||||||
|
unregisterAllThreads();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostingThreadSet::registerAllThreads()
|
||||||
|
{
|
||||||
|
ThreadRegistry::registerThread(PostingThreadRole::CALLER, callerThread);
|
||||||
|
ThreadRegistry::registerThread(PostingThreadRole::CALLEE, calleeThread);
|
||||||
|
ThreadRegistry::registerThread(PostingThreadRole::ALTERNATE, alternateThread);
|
||||||
|
ThreadRegistry::registerThread(PostingThreadRole::BODY, bodyThread);
|
||||||
|
ThreadRegistry::registerThread(PostingThreadRole::WORLD, worldThread);
|
||||||
|
ThreadRegistry::registerThread(PostingThreadRole::LEG, legThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostingThreadSet::unregisterAllThreads()
|
||||||
|
{
|
||||||
|
ThreadRegistry::unregisterThread(PostingThreadRole::CALLER, callerThread);
|
||||||
|
ThreadRegistry::unregisterThread(PostingThreadRole::CALLEE, calleeThread);
|
||||||
|
ThreadRegistry::unregisterThread(
|
||||||
|
PostingThreadRole::ALTERNATE,
|
||||||
|
alternateThread);
|
||||||
|
ThreadRegistry::unregisterThread(PostingThreadRole::BODY, bodyThread);
|
||||||
|
ThreadRegistry::unregisterThread(PostingThreadRole::WORLD, worldThread);
|
||||||
|
ThreadRegistry::unregisterThread(PostingThreadRole::LEG, legThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostingThreadSet::installCallerAsPuppeteer()
|
||||||
|
{
|
||||||
|
sscl::ComponentThread::setPuppeteerThreadId(
|
||||||
|
static_cast<sscl::ThreadId>(PostingThreadRole::CALLER));
|
||||||
|
sscl::ComponentThread::setPuppeteerThread(callerThread.componentThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostingThreadSet::restorePreviousPuppeteer()
|
||||||
|
{
|
||||||
|
sscl::ComponentThread::setPuppeteerThreadId(previousPuppeteerThreadId);
|
||||||
|
sscl::ComponentThread::setPuppeteerThread(previousPuppeteerThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread &PostingThreadSet::thread(PostingThreadRole role)
|
||||||
|
{
|
||||||
|
switch (role)
|
||||||
|
{
|
||||||
|
case PostingThreadRole::CALLER:
|
||||||
|
return callerThread;
|
||||||
|
case PostingThreadRole::CALLEE:
|
||||||
|
return calleeThread;
|
||||||
|
case PostingThreadRole::ALTERNATE:
|
||||||
|
return alternateThread;
|
||||||
|
case PostingThreadRole::BODY:
|
||||||
|
return bodyThread;
|
||||||
|
case PostingThreadRole::WORLD:
|
||||||
|
return worldThread;
|
||||||
|
case PostingThreadRole::LEG:
|
||||||
|
return legThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error("Unknown PostingThreadRole");
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread &PostingThreadSet::caller()
|
||||||
|
{
|
||||||
|
return callerThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread &PostingThreadSet::callee()
|
||||||
|
{
|
||||||
|
return calleeThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread &PostingThreadSet::alternate()
|
||||||
|
{
|
||||||
|
return alternateThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread &PostingThreadSet::body()
|
||||||
|
{
|
||||||
|
return bodyThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread &PostingThreadSet::world()
|
||||||
|
{
|
||||||
|
return worldThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
DedicatedIoThread &PostingThreadSet::leg()
|
||||||
|
{
|
||||||
|
return legThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossThreadTrace::recordConstructionThread()
|
||||||
|
{
|
||||||
|
record(constructionThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossThreadTrace::recordCalleeExecutionThread()
|
||||||
|
{
|
||||||
|
record(calleeExecutionThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossThreadTrace::recordFinalSuspendThread()
|
||||||
|
{
|
||||||
|
record(finalSuspendThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossThreadTrace::recordAwaitResumeThread()
|
||||||
|
{
|
||||||
|
record(awaitResumeThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossThreadTrace::recordCompletionCallbackThread()
|
||||||
|
{
|
||||||
|
record(completionCallbackThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id CrossThreadTrace::constructionThread() const
|
||||||
|
{
|
||||||
|
return read(constructionThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id CrossThreadTrace::calleeExecutionThread() const
|
||||||
|
{
|
||||||
|
return read(calleeExecutionThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id CrossThreadTrace::finalSuspendThread() const
|
||||||
|
{
|
||||||
|
return read(finalSuspendThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id CrossThreadTrace::awaitResumeThread() const
|
||||||
|
{
|
||||||
|
return read(awaitResumeThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id CrossThreadTrace::completionCallbackThread() const
|
||||||
|
{
|
||||||
|
return read(completionCallbackThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossThreadTrace::record(std::thread::id &slot)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
slot = std::this_thread::get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::id CrossThreadTrace::read(const std::thread::id &slot) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
#ifndef SPINSCALE_TEST_SUPPORT_THREAD_HARNESS_H
|
||||||
|
#define SPINSCALE_TEST_SUPPORT_THREAD_HARNESS_H
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
|
#include <spinscale/co/postingPromise.h>
|
||||||
|
#include <spinscale/component.h>
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
constexpr std::chrono::milliseconds defaultIdleTimeout{800};
|
||||||
|
constexpr std::chrono::milliseconds defaultTotalTimeout{10000};
|
||||||
|
constexpr std::chrono::milliseconds defaultPostingTaskTimeout{10000};
|
||||||
|
|
||||||
|
enum class PostingThreadRole : sscl::ThreadId
|
||||||
|
{
|
||||||
|
CALLER = 70,
|
||||||
|
CALLEE = 71,
|
||||||
|
ALTERNATE = 72,
|
||||||
|
BODY = 73,
|
||||||
|
WORLD = 74,
|
||||||
|
LEG = 75,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string threadRoleName(PostingThreadRole role);
|
||||||
|
|
||||||
|
class IoContextPump
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void pumpUntilIdle(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
std::chrono::milliseconds idleTimeout = defaultIdleTimeout,
|
||||||
|
std::chrono::milliseconds totalTimeout = defaultTotalTimeout);
|
||||||
|
|
||||||
|
template <typename Predicate>
|
||||||
|
static bool pumpUntil(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
Predicate &&predicate,
|
||||||
|
std::chrono::milliseconds idleTimeout = defaultIdleTimeout,
|
||||||
|
std::chrono::milliseconds totalTimeout = defaultTotalTimeout)
|
||||||
|
{
|
||||||
|
const auto totalDeadline =
|
||||||
|
std::chrono::steady_clock::now() + totalTimeout;
|
||||||
|
auto lastProgress = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
while (std::chrono::steady_clock::now() < totalDeadline)
|
||||||
|
{
|
||||||
|
if (std::invoke(predicate)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ioContext.poll_one() > 0)
|
||||||
|
{
|
||||||
|
lastProgress = std::chrono::steady_clock::now();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::chrono::steady_clock::now() - lastProgress >= idleTimeout) {
|
||||||
|
return std::invoke(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::invoke(predicate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThreadBoundComponent final
|
||||||
|
: public sscl::pptr::PuppeteerComponent
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ThreadBoundComponent();
|
||||||
|
void handleLoopExceptionHook() override;
|
||||||
|
|
||||||
|
std::exception_ptr loopException;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DedicatedIoThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit DedicatedIoThread(PostingThreadRole role);
|
||||||
|
~DedicatedIoThread();
|
||||||
|
|
||||||
|
DedicatedIoThread(const DedicatedIoThread &) = delete;
|
||||||
|
DedicatedIoThread &operator=(const DedicatedIoThread &) = delete;
|
||||||
|
DedicatedIoThread(DedicatedIoThread &&) = delete;
|
||||||
|
DedicatedIoThread &operator=(DedicatedIoThread &&) = delete;
|
||||||
|
|
||||||
|
boost::asio::io_context &ioContext();
|
||||||
|
sscl::ThreadId threadId() const noexcept;
|
||||||
|
std::thread::id osThreadId() const;
|
||||||
|
std::shared_ptr<sscl::PuppeteerThread> componentThread() const;
|
||||||
|
|
||||||
|
void stopAndJoin();
|
||||||
|
|
||||||
|
struct StartupState;
|
||||||
|
|
||||||
|
template <typename Function>
|
||||||
|
void post(Function &&function)
|
||||||
|
{
|
||||||
|
boost::asio::post(
|
||||||
|
ioContext(),
|
||||||
|
std::forward<Function>(function));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Function>
|
||||||
|
auto runSync(Function &&function)
|
||||||
|
-> std::invoke_result_t<Function &>
|
||||||
|
{
|
||||||
|
using Result = std::invoke_result_t<Function &>;
|
||||||
|
|
||||||
|
if (std::this_thread::get_id() == osThreadId()) {
|
||||||
|
if constexpr (std::is_void_v<Result>) {
|
||||||
|
std::invoke(function);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return std::invoke(function);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto promise = std::make_shared<std::promise<Result>>();
|
||||||
|
auto future = promise->get_future();
|
||||||
|
|
||||||
|
post(
|
||||||
|
[promise, function = std::forward<Function>(function)]() mutable
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if constexpr (std::is_void_v<Result>)
|
||||||
|
{
|
||||||
|
std::invoke(function);
|
||||||
|
promise->set_value();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
promise->set_value(std::invoke(function));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
promise->set_exception(std::current_exception());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void releaseStartupBarrier();
|
||||||
|
void waitUntilInitialized();
|
||||||
|
|
||||||
|
PostingThreadRole role;
|
||||||
|
std::shared_ptr<StartupState> startupState;
|
||||||
|
ThreadBoundComponent component;
|
||||||
|
std::shared_ptr<sscl::PuppeteerThread> thread;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThreadRegistry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void registerThread(
|
||||||
|
PostingThreadRole role,
|
||||||
|
DedicatedIoThread &thread);
|
||||||
|
static void unregisterThread(
|
||||||
|
PostingThreadRole role,
|
||||||
|
DedicatedIoThread &expectedThread);
|
||||||
|
static boost::asio::io_context &ioContext(PostingThreadRole role);
|
||||||
|
static std::thread::id osThreadId(PostingThreadRole role);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::mutex ®istryMutex();
|
||||||
|
static std::map<PostingThreadRole, DedicatedIoThread *> &threadsByRole();
|
||||||
|
};
|
||||||
|
|
||||||
|
template <PostingThreadRole role>
|
||||||
|
struct PostingThreadTag
|
||||||
|
{
|
||||||
|
static boost::asio::io_context &io_context()
|
||||||
|
{
|
||||||
|
return ThreadRegistry::ioContext(role);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <PostingThreadRole role, typename T>
|
||||||
|
using RolePostingPromise =
|
||||||
|
sscl::co::TaggedPostingPromise<T, PostingThreadTag<role>>;
|
||||||
|
|
||||||
|
template <PostingThreadRole role>
|
||||||
|
struct RolePostingPromiseTemplate
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
using Type = RolePostingPromise<role, T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <PostingThreadRole role, typename T>
|
||||||
|
using RoleViralPostingInvoker =
|
||||||
|
sscl::co::ViralPostingInvoker<
|
||||||
|
RolePostingPromiseTemplate<role>::template Type,
|
||||||
|
T>;
|
||||||
|
|
||||||
|
template <PostingThreadRole role>
|
||||||
|
using RoleNonViralPostingInvoker =
|
||||||
|
sscl::co::NonViralPostingInvoker<
|
||||||
|
RolePostingPromiseTemplate<role>::template Type>;
|
||||||
|
|
||||||
|
class PostingThreadSet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PostingThreadSet();
|
||||||
|
~PostingThreadSet();
|
||||||
|
|
||||||
|
PostingThreadSet(const PostingThreadSet &) = delete;
|
||||||
|
PostingThreadSet &operator=(const PostingThreadSet &) = delete;
|
||||||
|
PostingThreadSet(PostingThreadSet &&) = delete;
|
||||||
|
PostingThreadSet &operator=(PostingThreadSet &&) = delete;
|
||||||
|
|
||||||
|
DedicatedIoThread &thread(PostingThreadRole role);
|
||||||
|
DedicatedIoThread &caller();
|
||||||
|
DedicatedIoThread &callee();
|
||||||
|
DedicatedIoThread &alternate();
|
||||||
|
DedicatedIoThread &body();
|
||||||
|
DedicatedIoThread &world();
|
||||||
|
DedicatedIoThread &leg();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void registerAllThreads();
|
||||||
|
void unregisterAllThreads();
|
||||||
|
void installCallerAsPuppeteer();
|
||||||
|
void restorePreviousPuppeteer();
|
||||||
|
|
||||||
|
DedicatedIoThread callerThread;
|
||||||
|
DedicatedIoThread calleeThread;
|
||||||
|
DedicatedIoThread alternateThread;
|
||||||
|
DedicatedIoThread bodyThread;
|
||||||
|
DedicatedIoThread worldThread;
|
||||||
|
DedicatedIoThread legThread;
|
||||||
|
std::shared_ptr<sscl::PuppeteerThread> previousPuppeteerThread;
|
||||||
|
sscl::ThreadId previousPuppeteerThreadId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Function>
|
||||||
|
auto RunOnThread(DedicatedIoThread &thread, Function &&function)
|
||||||
|
-> std::invoke_result_t<Function &>
|
||||||
|
{
|
||||||
|
return thread.runSync(std::forward<Function>(function));
|
||||||
|
}
|
||||||
|
|
||||||
|
class CrossThreadTrace
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void recordConstructionThread();
|
||||||
|
void recordCalleeExecutionThread();
|
||||||
|
void recordFinalSuspendThread();
|
||||||
|
void recordAwaitResumeThread();
|
||||||
|
void recordCompletionCallbackThread();
|
||||||
|
|
||||||
|
std::thread::id constructionThread() const;
|
||||||
|
std::thread::id calleeExecutionThread() const;
|
||||||
|
std::thread::id finalSuspendThread() const;
|
||||||
|
std::thread::id awaitResumeThread() const;
|
||||||
|
std::thread::id completionCallbackThread() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void record(std::thread::id &slot);
|
||||||
|
std::thread::id read(const std::thread::id &slot) const;
|
||||||
|
|
||||||
|
mutable std::mutex mutex;
|
||||||
|
std::thread::id constructionThreadId;
|
||||||
|
std::thread::id calleeExecutionThreadId;
|
||||||
|
std::thread::id finalSuspendThreadId;
|
||||||
|
std::thread::id awaitResumeThreadId;
|
||||||
|
std::thread::id completionCallbackThreadId;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename InvokerFactory>
|
||||||
|
void runNonViralPostingTask(
|
||||||
|
DedicatedIoThread &callerThread,
|
||||||
|
InvokerFactory &&invokerFactory,
|
||||||
|
std::chrono::milliseconds timeout = defaultPostingTaskTimeout)
|
||||||
|
{
|
||||||
|
using Factory = std::decay_t<InvokerFactory>;
|
||||||
|
using Invoker = std::invoke_result_t<
|
||||||
|
Factory &, std::exception_ptr &, std::function<void()>>;
|
||||||
|
|
||||||
|
struct TaskState
|
||||||
|
{
|
||||||
|
explicit TaskState(Factory factoryIn)
|
||||||
|
: factory(std::move(factoryIn))
|
||||||
|
{}
|
||||||
|
|
||||||
|
Factory factory;
|
||||||
|
std::exception_ptr coroutineException;
|
||||||
|
std::exception_ptr taskException;
|
||||||
|
std::optional<Invoker> invoker;
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable condition;
|
||||||
|
bool completed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto taskState = std::make_shared<TaskState>(
|
||||||
|
std::forward<InvokerFactory>(invokerFactory));
|
||||||
|
|
||||||
|
callerThread.post(
|
||||||
|
[taskState]()
|
||||||
|
{
|
||||||
|
auto completeTask = [taskState]()
|
||||||
|
{
|
||||||
|
taskState->taskException = taskState->coroutineException;
|
||||||
|
taskState->invoker.reset();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(taskState->mutex);
|
||||||
|
taskState->completed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskState->condition.notify_one();
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
taskState->invoker.emplace(
|
||||||
|
std::invoke(
|
||||||
|
taskState->factory,
|
||||||
|
taskState->coroutineException,
|
||||||
|
std::move(completeTask)));
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(taskState->mutex);
|
||||||
|
taskState->taskException = std::current_exception();
|
||||||
|
taskState->completed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskState->condition.notify_one();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(taskState->mutex);
|
||||||
|
const bool completed = taskState->condition.wait_for(
|
||||||
|
lock,
|
||||||
|
timeout,
|
||||||
|
[&taskState]() { return taskState->completed; });
|
||||||
|
|
||||||
|
if (!completed) {
|
||||||
|
throw std::runtime_error("Timed out waiting for posting coroutine task");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::exception_ptr taskException = taskState->taskException;
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
if (taskException) {
|
||||||
|
std::rethrow_exception(taskException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
#endif // SPINSCALE_TEST_SUPPORT_THREAD_HARNESS_H
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
#ifndef SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||||
|
#define SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||||
|
|
||||||
|
#include <boostAsioLinkageFix.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <boost/asio/error.hpp>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/steady_timer.hpp>
|
||||||
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
|
namespace sscl::tests {
|
||||||
|
|
||||||
|
using SharedSteadyTimer = std::shared_ptr<boost::asio::steady_timer>;
|
||||||
|
|
||||||
|
/* Keep historical names as aliases so existing spinscale tests stay readable. */
|
||||||
|
using SharedDeadlineTimer = SharedSteadyTimer;
|
||||||
|
|
||||||
|
class CancelableDeadlineTimerRegistry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
timersByLabel.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerTimer(
|
||||||
|
int labelMilliseconds,
|
||||||
|
const SharedSteadyTimer &timer)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
timersByLabel[labelMilliseconds] = timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel(int labelMilliseconds)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
|
const auto iterator = timersByLabel.find(labelMilliseconds);
|
||||||
|
|
||||||
|
if (iterator == timersByLabel.end()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"No cancelable steady_timer registered for label "
|
||||||
|
+ std::to_string(labelMilliseconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedSteadyTimer timer = iterator->second.lock();
|
||||||
|
|
||||||
|
if (!timer) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Cancelable steady_timer expired before cancel for label "
|
||||||
|
+ std::to_string(labelMilliseconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
timer->cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mutex;
|
||||||
|
std::unordered_map<int, std::weak_ptr<boost::asio::steady_timer>>
|
||||||
|
timersByLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeadlineTimerAwaiter
|
||||||
|
{
|
||||||
|
DeadlineTimerAwaiter(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
int delayMilliseconds)
|
||||||
|
: timer(std::make_shared<boost::asio::steady_timer>(ioContext))
|
||||||
|
{
|
||||||
|
start(delayMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeadlineTimerAwaiter(
|
||||||
|
SharedSteadyTimer sharedTimer,
|
||||||
|
int delayMilliseconds)
|
||||||
|
: timer(std::move(sharedTimer))
|
||||||
|
{
|
||||||
|
start(delayMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{ return waitCompleted; }
|
||||||
|
|
||||||
|
bool await_suspend(std::coroutine_handle<> handle) noexcept
|
||||||
|
{
|
||||||
|
resumeHandle = handle;
|
||||||
|
return !waitCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::system::error_code await_resume() const noexcept
|
||||||
|
{ return completionErrorCode; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void start(int delayMilliseconds)
|
||||||
|
{
|
||||||
|
timer->expires_after(
|
||||||
|
std::chrono::milliseconds(delayMilliseconds));
|
||||||
|
timer->async_wait(
|
||||||
|
[this](const boost::system::error_code &errorCode)
|
||||||
|
{
|
||||||
|
completionErrorCode = errorCode;
|
||||||
|
waitCompleted = true;
|
||||||
|
if (resumeHandle) {
|
||||||
|
resumeHandle.resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedSteadyTimer timer;
|
||||||
|
boost::system::error_code completionErrorCode;
|
||||||
|
bool waitCompleted = false;
|
||||||
|
std::coroutine_handle<> resumeHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RegisteredDeadlineTimerAwaiter
|
||||||
|
{
|
||||||
|
RegisteredDeadlineTimerAwaiter(
|
||||||
|
boost::asio::io_context &ioContext,
|
||||||
|
int delayMilliseconds,
|
||||||
|
int registrationLabelMilliseconds,
|
||||||
|
CancelableDeadlineTimerRegistry ®istry)
|
||||||
|
: timer(std::make_shared<boost::asio::steady_timer>(ioContext))
|
||||||
|
{
|
||||||
|
registry.registerTimer(registrationLabelMilliseconds, timer);
|
||||||
|
waiter.emplace(timer, delayMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool await_ready() const noexcept
|
||||||
|
{ return waiter->await_ready(); }
|
||||||
|
|
||||||
|
bool await_suspend(std::coroutine_handle<> handle) noexcept
|
||||||
|
{ return waiter->await_suspend(handle); }
|
||||||
|
|
||||||
|
boost::system::error_code await_resume() const noexcept
|
||||||
|
{ return waiter->await_resume(); }
|
||||||
|
|
||||||
|
SharedSteadyTimer timer;
|
||||||
|
std::optional<DeadlineTimerAwaiter> waiter;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void throwIfTimerWaitFailed(
|
||||||
|
const boost::system::error_code &waitError)
|
||||||
|
{
|
||||||
|
if (waitError) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"steady_timer wait failed: " + waitError.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool timerWasCanceled(const boost::system::error_code &waitError)
|
||||||
|
{
|
||||||
|
return waitError == boost::asio::error::operation_aborted;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
#endif // SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||||
Reference in New Issue
Block a user