Compare commits

...
62 Commits
Author SHA1 Message Date
latentprionandCursor e6d9bfe30c SharedResourceGroup: add move constructor for initial resource.
Allows move-only ResourceType values (e.g. containers of unique_ptr) to initialize rsrc without copying.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 19:14:46 -04:00
latentprionandCursor fd2440e4ee EnvKvStore: find/get split, typed int accessors, nest DotenvParser.
Rename optional lookup to find(), add throwing get(), and provide
getInt/getPositiveInt/getPositiveNonZeroInt with optional defaults so
callers own missing-key policy without baking domain period semantics
into spinscale.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 19:26:14 -04:00
latentprionandCursor 21fa125d52 Summarize and merge MultiOperationResultSetWithException from settled Groups.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 05:06:18 -04:00
latentprionandCursor 3156652257 Document closeAdmission-before-cancel nursery stop ordering.
Explain the admit-after-cancel race and that daemons should disconnect at
the protocol layer before sealing admission (or handle closed-admission on enqueue).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 19:02:52 -04:00
latentprion 88d6367491 probe: barrier TLS init until make_shared arms shared_from_this.
PuppeteerThread starts its OS thread in the constructor, so initializeTls()
could race and throw bad_weak_ptr; wait for the harness barrier first.
2026-07-12 06:10:41 -04:00
latentprionandCursor 5e6108d396 Migrate test timer awaiters from deadline_timer to steady_timer.
Boost 1.90 marks deadline_timer deprecated; keep historical type aliases so existing spinscale tests need no renames.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 05:22:11 -04:00
latentprionandCursor 3d23ed21be cmake: omit Boost::system on Boost 1.89+ where the compiled stub is gone.
Keep linking Boost::log, and only require Boost::system on older package sets that still ship libboost_system.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 22:27:55 -04:00
latentprionandCursor 15ebf375ef spinscale: split probe harness from test support.
Move ProbeComponentThreadHarness into spinscale_probe_support (sscl::probe) so tools can link it without gtest; keep a sscl::tests compatibility shim.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 22:03:19 -04:00
latentprionandCursor 7c9bec7b9c spinscale: document slot-cancel-before-internal-op-cancel ordering.
Shutdown call sites must set slot cancelers before cancelling timers, I/O,
or hardware capture so callees observe stop intent when unblocked.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 18:26:54 -04:00
hayodeaandCursor 01efbd8c94 Add sscl::co::syncAwaitNonViralCoro for blocking non-viral coroutine launch.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 00:21:45 -04:00
hayodea 3d81ee92aa New test support harness primitives for testing stimbuffapis 2026-06-13 18:47:44 -04:00
hayodea 2f31e9a034 Adversarial review on test porting plan 2026-06-13 17:59:06 -04:00
hayodea a29c779f6e Tests: Add all tests from the coro creation repo
We went back and brought along all the tests we implemented while
we were building the new coro framework.
2026-06-13 17:17:57 -04:00
hayodea 1763685c0e Tests: Move qutex and nursery tests into Libspinscale repo 2026-06-13 16:17:40 -04:00
hayodea 016b2d26de SharedRsrcGrp: Allow construction of rsrc by copy 2026-06-13 11:46:16 -04:00
hayodea ffe86369e2 Add EnvKvStore for envvar parsing and interleaving 2026-06-11 19:16:46 -04:00
hayodea 00be517f30 Printing: print fewer blank newlines 2026-06-11 11:16:37 -04:00
hayodea ebf0fa2921 Nursery: Document intended usage form 2026-06-09 21:25:25 -04:00
hayodea d33e70f14a Nursery: document syncAwaitAll's caller io_context requirement for LLMs 2026-06-09 16:48:58 -04:00
hayodea 656aae37c8 Nursery: Capture onSettledCb before fillSlot in launch() 2026-06-09 10:55:33 -04:00
hayodea 5689ac3914 Nursery: Don't pass exceptions to main loop 2026-06-09 10:43:20 -04:00
hayodea 565e339a8b Nursery: Update examples to use slot and not lease in onSettled 2026-06-09 08:07:44 -04:00
hayodea b04b0db155 Add a Nursery class for dynamically managing nonviral coros 2026-06-09 05:46:51 -04:00
hayodea 44894299b4 Group: Enable aggregate exception to be passed as ptr 2026-06-07 19:33:42 -04:00
hayodea edde8f4a64 Don't auto-throw non-viral callee exceptions before callerLambda 2026-06-06 04:47:59 -04:00
hayodea 8a7d4272bd Add readme file 2026-06-01 09:40:03 -04:00
hayodea c60854845d New DynamicViralPostingInvoker: takes runtime post-to target 2026-05-31 07:13:10 -04:00
hayodea a53e0ca325 PostingPromise: reorder post-to target to be 1st arg 2026-05-30 21:45:51 -04:00
hayodea 42076d6c78 PostingPromise: Add dynamic post-to io_context targeting
This allows us to dynamically choose the target that a PostingPromise
coro will be posted to at runtime rather than only posting to the
statically configured ThreadTag::io_context() target. Big usability
improvement.
2026-05-30 20:44:53 -04:00
hayodea 2749d77d65 Remove boostAsioLinkageFix from headers 2026-05-30 12:10:38 -04:00
hayodea 3ea1475757 Boost.ASIO: potential fix for top_E shlib segfaults 2026-05-30 12:00:30 -04:00
hayodea 6df9407e65 Boost.ASIO: update io_service=>io_context 2026-05-30 11:57:57 -04:00
hayodea 0afa3e16b8 SyncCanceler: Fix visibility; reuse helper method 2026-05-29 12:05:36 -04:00
hayodea 4dbc066aac New class: SyncCancelerForAsyncWork
This class abstracts the pattern of running an async callee which
needs to be able to be canceled from a synchronous code path.

It really just lifts the logic that's regularly used in the
StimulusProducer path into a reusable abstraction.
2026-05-29 06:22:02 -04:00
hayodea ca2cccaa9c New multi-op result set class 2026-05-24 16:23:07 -04:00
hayodea a14d622eaf PuppetApp: Lifetime mgmt ops are now Viral
They no longer accept an exc_ptr and lambda for cb.
2026-05-24 16:11:08 -04:00
hayodea 16e0350245 CoQutex: Add instance name for debugging 2026-05-24 16:10:30 -04:00
hayodea 5f265567d1 Explain why CoQutex is superior to LockSet 2026-05-24 13:05:09 -04:00
hayodea e7707dacdf Unify [Non]PostingInvoker into Invoker 2026-05-24 04:32:44 -04:00
hayodea 5d139abff2 Split classes into neater header units 2026-05-24 04:28:30 -04:00
hayodea e29bee52cf Group now supports heterogeneous invokers for fanout 2026-05-24 04:01:34 -04:00
hayodea daad2a8c95 Impl ViralNonPostingInv; fix member coro thisptr; 2026-05-24 02:25:04 -04:00
hayodea abdb857e55 PuppetApp: Now use coros instead of CPS 2026-05-19 10:46:52 -04:00
hayodea 525530b567 Compilation fixups 2026-05-19 10:06:23 -04:00
hayodea 3f91cbf104 CompThr: make new changes compile 2026-05-19 10:01:15 -04:00
hayodea 6396cce7e0 Impl NonViralNonPostingInvoker; rename other invokers around posting behaviour 2026-05-19 09:57:24 -04:00
hayodea 15295ac05e Update reutrn type 2026-05-19 06:45:59 -04:00
hayodea dc58e5d521 CompThr: wrap lifetime mgmt ops in awaitable adapters 2026-05-19 06:18:23 -04:00
hayodea 1db3494d26 Remove superfluous file 2026-05-18 05:59:52 -04:00
hayodea e94aaf9323 Update include paths and namespacing 2026-05-17 17:26:21 -04:00
hayodea 83ad680c68 Move cps files into cps/ 2026-05-17 17:25:29 -04:00
hayodea 1d1cb099db Regress to io_service rather than io_context 2026-05-17 17:12:11 -04:00
hayodea 0dcfa754b6 Call std::terminate if await_* are called on NonViral*Invoker 2026-05-17 17:11:43 -04:00
hayodea ad4ea3ccac Add new Spinscale C++ Coroutine support 2026-05-17 16:52:04 -04:00
hayodea b6eb502e56 Fixing .deb package generation, I suppose 2026-03-06 01:08:34 -04:00
hayodea 596ad367e2 dbg:Gridlock detection: exclude locks where hasBeenReleased=true 2026-03-05 23:29:48 -04:00
hayodea e4332323f9 CompThr: Set keepLooping=false in ctor 2026-03-05 23:29:10 -04:00
hayodea 7eff7a6a9c Dbg:traceContinHistForDeadlock: consult LockUsageDesc::hasBeenReleased
Don't raise the alarm for a deadlock if the lock has already been
released at the moment of checking.
2026-03-05 23:10:05 -04:00
hayodea 85ac715772 SerializedAC:analyze all locksets in contin chain;
Not only those of type <OriginCbT>.
* Fix indentation too.
2026-03-05 22:41:02 -04:00
hayodea e98aae7e7e Qutex:backoff: Fix use after free 2026-03-05 20:02:55 -04:00
hayodea dab5477249 Qutex: fix use after free 2026-03-05 19:53:21 -04:00
hayodea 8af362c791 PuppetApp: exitAllThreads should call its own final segment
Previously it called the executeGenericOpOn which didn't call join()
on threads
2026-03-05 19:47:51 -04:00
72 changed files with 10403 additions and 469 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "googletest"]
path = googletest
url = https://github.com/google/googletest.git
+75 -21
View File
@@ -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
) )
+295
View File
@@ -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.
+59
View File
@@ -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
+3
View File
@@ -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 */
-58
View File
@@ -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
+214
View File
@@ -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
+217
View File
@@ -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
+652
View File
@@ -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
+119
View File
@@ -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
+263
View File
@@ -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
+228
View File
@@ -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
+40
View File
@@ -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
+554
View File
@@ -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
+29
View File
@@ -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
+377
View File
@@ -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
+72
View File
@@ -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
+49
View File
@@ -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
+38
View File
@@ -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
+61
View File
@@ -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
+1 -1
View File
@@ -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 {
+127 -16
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+101
View File
@@ -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
+21 -16
View File
@@ -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
+37
View File
@@ -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
+11
View File
@@ -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
)
+158
View File
@@ -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
+71
View File
@@ -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
+18
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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)));
+358
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
#include <spinscale/lockerAndInvokerBase.h>
namespace sscl {
} // namespace sscl
+35
View File
@@ -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
View File
@@ -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);
thread->joltThreadReq( co_await group.getAwaitAllSettlementsInvoker();
thread, group.checkForAndReThrowGroupExceptions();
{request, std::bind(
&PuppetThreadLifetimeMgmtOp::joltAllPuppetThreadsReq1, threadsHaveBeenJolted = true;
request.get(), request)}); co_return;
}
} }
void PuppetApplication::startAllPuppetThreadsReq( co::ViralNonPostingInvoker<void>
Callback<puppetThreadLifetimeMgmtOpCbFn> callback PuppetApplication::allPuppetThreadsLifetimeOpCReq(
) PuppetThread::ThreadOp threadOp,
std::string_view emptyThreadsLogMessage)
{ {
// If no threads, call callback immediately if (componentThreads.empty())
if (componentThreads.size() == 0 && callback.callbackFn)
{ {
callback.callbackFn(); std::cout << emptyThreadsLogMessage << "\n";
return; co_return;
} }
// Create a counter to track when all threads have started PuppetLifetimeMgmtGroup group;
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>( std::vector<PuppetLifetimeMgmtInvoker> invokers;
*this, componentThreads.size(), callback);
for (auto& thread : componentThreads) addAllPuppetLifetimeInvokersToGroup(group, invokers, threadOp);
{ co_await group.getAwaitAllSettlementsInvoker();
thread->startThreadReq( group.checkForAndReThrowGroupExceptions();
{request, std::bind(
&PuppetThreadLifetimeMgmtOp::executeGenericOpOnAllPuppetThreadsReq1, co_return;
request.get(), request)});
}
} }
void PuppetApplication::pauseAllPuppetThreadsReq( co::ViralNonPostingInvoker<void>
Callback<puppetThreadLifetimeMgmtOpCbFn> callback PuppetApplication::startAllPuppetThreadsCReq()
)
{ {
// If no threads, call callback immediately return allPuppetThreadsLifetimeOpCReq(
if (componentThreads.size() == 0 && callback.callbackFn) PuppetThread::ThreadOp::START,
{ noPuppetThreadsToStartLogMessage);
callback.callbackFn();
return;
}
// Create a counter to track when all threads have paused
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>(
*this, componentThreads.size(), callback);
for (auto& thread : componentThreads)
{
thread->pauseThreadReq(
{request, std::bind(
&PuppetThreadLifetimeMgmtOp::executeGenericOpOnAllPuppetThreadsReq1,
request.get(), request)});
}
} }
void PuppetApplication::resumeAllPuppetThreadsReq( 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 resumed
auto request = std::make_shared<PuppetThreadLifetimeMgmtOp>(
*this, componentThreads.size(), callback);
for (auto& thread : componentThreads)
{
thread->resumeThreadReq(
{request, std::bind(
&PuppetThreadLifetimeMgmtOp::executeGenericOpOnAllPuppetThreadsReq1,
request.get(), request)});
}
} }
void PuppetApplication::exitAllPuppetThreadsReq( co::ViralNonPostingInvoker<void>
Callback<puppetThreadLifetimeMgmtOpCbFn> callback PuppetApplication::resumeAllPuppetThreadsCReq()
)
{ {
// If no threads, call callback immediately return allPuppetThreadsLifetimeOpCReq(
if (componentThreads.size() == 0 && callback.callbackFn) PuppetThread::ThreadOp::RESUME,
noPuppetThreadsToResumeLogMessage);
}
co::ViralNonPostingInvoker<void>
PuppetApplication::exitAllPuppetThreadsCReq()
{
if (componentThreads.empty())
{ {
callback.callbackFn(); std::cout << noPuppetThreadsToExitLogMessage << "\n";
return; co_return;
} }
// Create a counter to track when all threads have exited 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->exitThreadReq(
{request, std::bind(
&PuppetThreadLifetimeMgmtOp::executeGenericOpOnAllPuppetThreadsReq1,
request.get(), request)});
} }
co_return;
} }
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)
{ {
+8 -8
View File
@@ -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)
{ {
+12 -12
View File
@@ -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
@@ -253,7 +253,7 @@ void Qutex::backoff(
#ifdef CONFIG_ENABLE_DEBUG_LOCKS #ifdef CONFIG_ENABLE_DEBUG_LOCKS
currOwner = nullptr; currOwner = nullptr;
#endif #endif
LockerAndInvokerBase &newFront = *queue.front(); std::shared_ptr<LockerAndInvokerBase> newFront = queue.front();
lock.release(); lock.release();
@@ -268,7 +268,7 @@ void Qutex::backoff(
* Hence there ought to be no way for the failedAcquirer to be at the front * Hence there ought to be no way for the failedAcquirer to be at the front
* of the queue at this point UNLESS the queue has only one item in it. * of the queue at this point UNLESS the queue has only one item in it.
*/ */
if (newFront == failedAcquirer && nQItems > 1) if (*newFront == failedAcquirer && nQItems > 1)
{ {
throw std::runtime_error( throw std::runtime_error(
std::string(__func__) + std::string(__func__) +
@@ -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.
* *
@@ -304,7 +304,7 @@ void Qutex::backoff(
* is backing off of a qutex within which it's the only waiter. * is backing off of a qutex within which it's the only waiter.
*/ */
if (nQItems > 1) { if (nQItems > 1) {
newFront.awaken(); newFront->awaken();
} }
} }
@@ -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
@@ -370,11 +370,11 @@ void Qutex::release()
* *
* Therefore we must always awaken the front item when releas()ing. * Therefore we must always awaken the front item when releas()ing.
*/ */
LockerAndInvokerBase &front = *queue.front(); std::shared_ptr<LockerAndInvokerBase> front = queue.front();
lock.release(); lock.release();
front.awaken(); front->awaken();
} }
} // namespace sscl } // namespace sscl::cps
+6 -6
View File
@@ -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
+56
View File
@@ -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
)
+250
View File
@@ -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);
}
+864
View File
@@ -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.";
}
+368
View File
@@ -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 &registry,
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 &registry,
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(),
[&registry, &trace](
std::exception_ptr &exceptionPtr,
std::function<void()> completion)
{
return runGroupTimerCancelLongAfterAwaitFirst(
exceptionPtr,
std::move(completion),
registry,
trace);
}));
assertTimerTraceCrossedThreads(trace);
}
+657
View File
@@ -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());
}
+252
View File
@@ -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());
}
+633
View File
@@ -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));
}));
}
+371
View File
@@ -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
+617
View File
@@ -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);
}
+71
View File
@@ -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
+38
View File
@@ -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
+63
View File
@@ -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
+177
View File
@@ -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
+20
View File
@@ -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
+455
View File
@@ -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
+378
View File
@@ -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 &registryMutex();
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
+166
View File
@@ -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 &registry)
: 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