mirror of
https://github.com/latentPrion/libspinscale.git
synced 2026-08-12 23:48:22 +00:00
Compare commits
10
Commits
3d81ee92aa
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6d9bfe30c | ||
|
|
fd2440e4ee | ||
|
|
21fa125d52 | ||
|
|
3156652257 | ||
|
|
88d6367491 | ||
|
|
5e6108d396 | ||
|
|
3d23ed21be | ||
|
|
15ebf375ef | ||
|
|
7c9bec7b9c | ||
|
|
01efbd8c94 |
+27
-11
@@ -66,13 +66,13 @@ 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)
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@ add_library(spinscale SHARED
|
|||||||
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
|
set_target_properties(spinscale PROPERTIES
|
||||||
@@ -110,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
|
||||||
@@ -162,6 +163,21 @@ endif()
|
|||||||
option(LIBSPINSCALE_BUILD_TESTS "Build libspinscale unit tests"
|
option(LIBSPINSCALE_BUILD_TESTS "Build libspinscale unit tests"
|
||||||
${_libspinscaleTestsDefault})
|
${_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(LIBSPINSCALE_BUILD_TESTS)
|
||||||
if(NOT TARGET gtest AND NOT TARGET gtest_main)
|
if(NOT TARGET gtest AND NOT TARGET gtest_main)
|
||||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||||
|
|||||||
@@ -202,17 +202,19 @@ nursery.launch(
|
|||||||
nvc.checkAndRethrowException();
|
nvc.checkAndRethrowException();
|
||||||
});
|
});
|
||||||
|
|
||||||
nursery.requestCancelOnAll();
|
|
||||||
nursery.closeAdmission();
|
nursery.closeAdmission();
|
||||||
|
nursery.requestCancelOnAll();
|
||||||
nursery.syncAwaitAllSettlements(
|
nursery.syncAwaitAllSettlements(
|
||||||
sscl::ComponentThread::getSelf()->getIoContext());
|
sscl::ComponentThread::getSelf()->getIoContext());
|
||||||
```
|
```
|
||||||
|
|
||||||
Each slot owns a `SyncCancelerForAsyncWork`. `requestCancelOnAll()` only signals
|
Each slot owns a `SyncCancelerForAsyncWork`. `requestCancelOnAll()` only signals
|
||||||
cooperative stop; it does not destroy invokers. Invokers are retired when their
|
cooperative stop; it does not destroy invokers. Invokers are retired when their
|
||||||
completion callbacks run. Call `closeAdmission()` explicitly before
|
completion callbacks run. Call `closeAdmission()` before `requestCancelOnAll()`
|
||||||
`asyncAwaitAllSettlements()` or `syncAwaitAllSettlements()`; those APIs wait until
|
so no new work can be admitted after cancel begins, and call `closeAdmission()`
|
||||||
all slots have retired naturally and throw if admission is still open.
|
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
|
`syncAwaitAllSettlements()` runs a nested `io_context` loop on the **calling
|
||||||
thread** (it blocks in `run_one()` until every slot has retired). Pass the
|
thread** (it blocks in `run_one()` until every slot has retired). Pass the
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -55,9 +55,6 @@ struct MemberInvoker : MemberInvokerBase
|
|||||||
* nursery member. The external submitter should add the complete flow to the
|
* 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.
|
* nursery and then return; the nursery owns that flow until the flow settles.
|
||||||
*
|
*
|
||||||
* Call closeAdmission() explicitly before asyncAwaitAllSettlements() or
|
|
||||||
* syncAwaitAllSettlements().
|
|
||||||
*
|
|
||||||
* syncAwaitAllSettlements() runs a nested io_context loop on the calling
|
* syncAwaitAllSettlements() runs a nested io_context loop on the calling
|
||||||
* thread (AsynchronousBridge). Pass the calling thread's io_context —
|
* thread (AsynchronousBridge). Pass the calling thread's io_context —
|
||||||
* typically
|
* typically
|
||||||
@@ -240,6 +237,36 @@ public:
|
|||||||
s.rsrc.admissionOpen = true;
|
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()
|
void closeAdmission()
|
||||||
{
|
{
|
||||||
sscl::SpinLock::Guard guard(s.lock);
|
sscl::SpinLock::Guard guard(s.lock);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -20,9 +20,66 @@ public:
|
|||||||
explicit EnvKvStore(
|
explicit EnvKvStore(
|
||||||
const std::vector<std::filesystem::path> &envFilePaths);
|
const std::vector<std::filesystem::path> &envFilePaths);
|
||||||
|
|
||||||
std::optional<std::string> get(std::string_view name) const;
|
/** 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:
|
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(
|
void loadFiles(
|
||||||
const std::vector<std::filesystem::path> &envFilePaths,
|
const std::vector<std::filesystem::path> &envFilePaths,
|
||||||
std::ostream &warningStream);
|
std::ostream &warningStream);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
#include <exception>
|
#include <exception>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl {
|
||||||
|
namespace co {
|
||||||
|
struct Group;
|
||||||
|
} // namespace co
|
||||||
|
|
||||||
/** Plain aggregate for fan-out / fan-in results returned from coroutines. */
|
/** Plain aggregate for fan-out / fan-in results returned from coroutines. */
|
||||||
struct MultiOperationResultSet
|
struct MultiOperationResultSet
|
||||||
@@ -38,9 +41,29 @@ struct MultiOperationResultSetWithException
|
|||||||
memberFailureException(memberFailureExceptionIn)
|
memberFailureException(memberFailureExceptionIn)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
/** Summarize a settled Group into counts + aggregated member failure. */
|
||||||
|
explicit MultiOperationResultSetWithException(const co::Group &group);
|
||||||
|
|
||||||
bool hasMemberFailure() const
|
bool hasMemberFailure() const
|
||||||
{ return memberFailureException != nullptr; }
|
{ 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;
|
MultiOperationResultSet results;
|
||||||
std::exception_ptr memberFailureException = nullptr;
|
std::exception_ptr memberFailureException = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define SHARED_RESOURCE_GROUP_H
|
#define SHARED_RESOURCE_GROUP_H
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl {
|
||||||
|
|
||||||
@@ -20,6 +21,11 @@ public:
|
|||||||
: lock(lockName), rsrc(initialRsrc)
|
: lock(lockName), rsrc(initialRsrc)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
SharedResourceGroup(
|
||||||
|
const std::string& lockName, ResourceType&& initialRsrc)
|
||||||
|
: lock(lockName), rsrc(std::move(initialRsrc))
|
||||||
|
{}
|
||||||
|
|
||||||
~SharedResourceGroup() = default;
|
~SharedResourceGroup() = default;
|
||||||
|
|
||||||
LockType lock;
|
LockType lock;
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ namespace sscl {
|
|||||||
* then flips shouldContinue to false. This guarantees shouldContinue is stable
|
* then flips shouldContinue to false. This guarantees shouldContinue is stable
|
||||||
* throughout each uncancelable segment.
|
* 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
|
* startAcceptingWork() is intentionally unlocked. Precondition: callers must
|
||||||
* only call startAcceptingWork() when no async callee is running yet (e.g. at
|
* 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
|
* the end of setup(), before posting/arming the first async work). If this
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
#include <support/probeComponentThread.h>
|
#include <probe/probeComponentThread.h>
|
||||||
|
|
||||||
|
#include <condition_variable>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
#include <spinscale/component.h>
|
#include <spinscale/component.h>
|
||||||
|
|
||||||
namespace sscl::tests {
|
namespace sscl::probe {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -25,12 +27,48 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 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(
|
void probePuppeteerMain(
|
||||||
const sscl::PuppeteerThread::EntryFnArguments& args,
|
const sscl::PuppeteerThread::EntryFnArguments& args,
|
||||||
const std::function<void(
|
const std::function<void(
|
||||||
const std::shared_ptr<sscl::ComponentThread>&)>& work,
|
const std::shared_ptr<sscl::ComponentThread>&)>& work,
|
||||||
std::promise<std::exception_ptr>& donePromise)
|
std::promise<std::exception_ptr>& donePromise,
|
||||||
|
const std::shared_ptr<ProbeThreadStartupState>& startupState)
|
||||||
{
|
{
|
||||||
|
waitForProbeThreadStartupPermission(startupState);
|
||||||
|
|
||||||
sscl::PuppeteerThread& thr = args.usableBeforeJolt;
|
sscl::PuppeteerThread& thr = args.usableBeforeJolt;
|
||||||
thr.initializeTls();
|
thr.initializeTls();
|
||||||
sscl::ComponentThread::setPuppeteerThreadId(PROBE_PUPPETEER_THREAD_ID);
|
sscl::ComponentThread::setPuppeteerThreadId(PROBE_PUPPETEER_THREAD_ID);
|
||||||
@@ -73,21 +111,23 @@ void ProbeComponentThreadHarness::runSync(
|
|||||||
{
|
{
|
||||||
std::promise<std::exception_ptr> donePromise;
|
std::promise<std::exception_ptr> donePromise;
|
||||||
std::future<std::exception_ptr> doneFuture = donePromise.get_future();
|
std::future<std::exception_ptr> doneFuture = donePromise.get_future();
|
||||||
|
auto startupState = std::make_shared<ProbeThreadStartupState>();
|
||||||
|
|
||||||
std::shared_ptr<sscl::PuppeteerThread> runThread =
|
std::shared_ptr<sscl::PuppeteerThread> runThread =
|
||||||
std::make_shared<sscl::PuppeteerThread>(
|
std::make_shared<sscl::PuppeteerThread>(
|
||||||
PROBE_PUPPETEER_THREAD_ID,
|
PROBE_PUPPETEER_THREAD_ID,
|
||||||
threadName,
|
threadName,
|
||||||
[&work, &donePromise](
|
[&work, &donePromise, startupState](
|
||||||
const sscl::PuppeteerThread::EntryFnArguments& args)
|
const sscl::PuppeteerThread::EntryFnArguments& args)
|
||||||
{
|
{
|
||||||
probePuppeteerMain(args, work, donePromise);
|
probePuppeteerMain(args, work, donePromise, startupState);
|
||||||
},
|
},
|
||||||
*dummyComponent,
|
*dummyComponent,
|
||||||
nullptr);
|
nullptr);
|
||||||
|
|
||||||
dummyComponent->thread = runThread;
|
dummyComponent->thread = runThread;
|
||||||
lastComponentThread = runThread;
|
lastComponentThread = runThread;
|
||||||
|
releaseProbeThreadStartupBarrier(startupState);
|
||||||
runThread->thread.join();
|
runThread->thread.join();
|
||||||
|
|
||||||
std::exception_ptr probeException = doneFuture.get();
|
std::exception_ptr probeException = doneFuture.get();
|
||||||
@@ -115,4 +155,4 @@ void runNonViralNurseryOnComponentThread(
|
|||||||
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
|
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sscl::tests
|
} // namespace sscl::probe
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#ifndef SPINSCALE_PROBE_COMPONENT_THREAD_H
|
||||||
|
#define SPINSCALE_PROBE_COMPONENT_THREAD_H
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <exception>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <spinscale/componentThread.h>
|
||||||
|
#include <spinscale/co/invokers.h>
|
||||||
|
#include <spinscale/co/nonViralTaskNursery.h>
|
||||||
|
|
||||||
|
namespace sscl::probe {
|
||||||
|
|
||||||
|
constexpr std::chrono::milliseconds defaultProbeTaskTimeout{10000};
|
||||||
|
|
||||||
|
void runNonViralNurseryOnComponentThread(
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>& componentThread,
|
||||||
|
std::function<sscl::co::NonViralNonPostingInvoker(
|
||||||
|
sscl::co::NonViralTaskNursery::Slot::Lease&)> invokerFactory,
|
||||||
|
std::chrono::milliseconds timeout = defaultProbeTaskTimeout);
|
||||||
|
|
||||||
|
/** Sync driver: run work on a temporary puppeteer ComponentThread.
|
||||||
|
*
|
||||||
|
* Shared by lcameraDev probe tools and HIL/unit tests. Not tied to gtest.
|
||||||
|
*/
|
||||||
|
class ProbeComponentThreadHarness
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ProbeComponentThreadHarness(
|
||||||
|
const char *threadName = "spinscale-probe");
|
||||||
|
~ProbeComponentThreadHarness();
|
||||||
|
|
||||||
|
ProbeComponentThreadHarness(const ProbeComponentThreadHarness &) = delete;
|
||||||
|
ProbeComponentThreadHarness &operator=(
|
||||||
|
const ProbeComponentThreadHarness &) = delete;
|
||||||
|
|
||||||
|
std::shared_ptr<sscl::ComponentThread> componentThread() const;
|
||||||
|
|
||||||
|
void runSync(
|
||||||
|
const std::function<void(
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>&)>& work);
|
||||||
|
|
||||||
|
template <typename InvokerFactory>
|
||||||
|
void runNonViralNurseryTask(
|
||||||
|
InvokerFactory &&invokerFactory,
|
||||||
|
std::chrono::milliseconds timeout = defaultProbeTaskTimeout)
|
||||||
|
{
|
||||||
|
runSync(
|
||||||
|
[this, &invokerFactory, timeout](
|
||||||
|
const std::shared_ptr<sscl::ComponentThread>& componentThread)
|
||||||
|
{
|
||||||
|
sscl::probe::runNonViralNurseryOnComponentThread(
|
||||||
|
componentThread,
|
||||||
|
std::forward<InvokerFactory>(invokerFactory),
|
||||||
|
timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string threadName;
|
||||||
|
std::shared_ptr<sscl::pptr::PuppeteerComponent> dummyComponent;
|
||||||
|
std::shared_ptr<sscl::ComponentThread> lastComponentThread;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sscl::probe
|
||||||
|
|
||||||
|
#endif // SPINSCALE_PROBE_COMPONENT_THREAD_H
|
||||||
+188
-108
@@ -3,70 +3,96 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <spinscale/envKvStore.h>
|
#include <spinscale/envKvStore.h>
|
||||||
|
|
||||||
namespace sscl {
|
namespace sscl {
|
||||||
namespace {
|
|
||||||
|
|
||||||
std::string trim(std::string_view value)
|
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(
|
auto begin = std::ranges::find_if_not(
|
||||||
value, [](unsigned char c) { return std::isspace(c); });
|
value, [](unsigned char c) { return std::isspace(c); });
|
||||||
auto rbegin = std::ranges::find_if_not(
|
auto rbegin = std::ranges::find_if_not(
|
||||||
value | std::views::reverse,
|
value | std::views::reverse,
|
||||||
[](unsigned char c) { return std::isspace(c); });
|
[](unsigned char c) { return std::isspace(c); });
|
||||||
auto end = rbegin.base();
|
auto end = rbegin.base();
|
||||||
if (begin >= end)
|
if (begin >= end) { return {}; }
|
||||||
{
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return std::string(begin, end);
|
return std::string(begin, end);
|
||||||
}
|
|
||||||
|
|
||||||
bool characterIsValidNameStart(char c)
|
|
||||||
{
|
|
||||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
|
||||||
}
|
|
||||||
|
|
||||||
bool characterIsValidNameBody(char c)
|
|
||||||
{
|
|
||||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
|
||||||
}
|
|
||||||
|
|
||||||
bool nameIsValid(std::string_view name)
|
|
||||||
{
|
|
||||||
if (name.empty() || !characterIsValidNameStart(name.front()))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return std::ranges::all_of(name.substr(1), characterIsValidNameBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::runtime_error makeParseError(
|
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,
|
const std::filesystem::path &envFilePath,
|
||||||
std::size_t lineNumber,
|
std::size_t lineNumber,
|
||||||
const std::string &message)
|
const std::string &message)
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << envFilePath << ":" << lineNumber << ": " << message;
|
stream << envFilePath << ":" << lineNumber << ": " << message;
|
||||||
return std::runtime_error(stream.str());
|
return std::runtime_error(stream.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool lineIsBlankOrComment(const std::string &line)
|
static std::size_t findClosingQuote(
|
||||||
{
|
|
||||||
std::string trimmed = trim(line);
|
|
||||||
return trimmed.empty() || trimmed.front() == '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t findClosingQuote(
|
|
||||||
const std::filesystem::path &envFilePath,
|
const std::filesystem::path &envFilePath,
|
||||||
std::size_t lineNumber,
|
std::size_t lineNumber,
|
||||||
std::string_view value)
|
std::string_view value)
|
||||||
{
|
{
|
||||||
char quote = value.front();
|
char quote = value.front();
|
||||||
bool escapeNext = false;
|
bool escapeNext = false;
|
||||||
for (std::size_t i = 1; i < value.size(); ++i)
|
for (std::size_t i = 1; i < value.size(); ++i)
|
||||||
@@ -81,16 +107,14 @@ std::size_t findClosingQuote(
|
|||||||
escapeNext = true;
|
escapeNext = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (value[i] == quote)
|
if (value[i] == quote) { return i; }
|
||||||
{
|
|
||||||
return i;
|
|
||||||
}
|
}
|
||||||
|
throw makeParseError(
|
||||||
|
envFilePath, lineNumber, "Unterminated quoted value.");
|
||||||
}
|
}
|
||||||
throw makeParseError(envFilePath, lineNumber, "Unterminated quoted value.");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string decodeDoubleQuotedValue(std::string_view value)
|
static std::string decodeDoubleQuotedValue(std::string_view value)
|
||||||
{
|
{
|
||||||
std::string decoded;
|
std::string decoded;
|
||||||
decoded.reserve(value.size());
|
decoded.reserve(value.size());
|
||||||
bool escapeNext = false;
|
bool escapeNext = false;
|
||||||
@@ -123,77 +147,42 @@ std::string decodeDoubleQuotedValue(std::string_view value)
|
|||||||
}
|
}
|
||||||
decoded.push_back(c);
|
decoded.push_back(c);
|
||||||
}
|
}
|
||||||
if (escapeNext)
|
if (escapeNext) { decoded.push_back('\\'); }
|
||||||
{
|
|
||||||
decoded.push_back('\\');
|
|
||||||
}
|
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string parseQuotedValue(
|
static std::string parseQuotedValue(
|
||||||
const std::filesystem::path &envFilePath,
|
const std::filesystem::path &envFilePath,
|
||||||
std::size_t lineNumber,
|
std::size_t lineNumber,
|
||||||
std::string_view value)
|
std::string_view value)
|
||||||
{
|
{
|
||||||
char quote = value.front();
|
char quote = value.front();
|
||||||
std::size_t closingQuote = findClosingQuote(envFilePath, lineNumber, value);
|
std::size_t closingQuote =
|
||||||
|
findClosingQuote(envFilePath, lineNumber, value);
|
||||||
std::string trailing = trim(value.substr(closingQuote + 1));
|
std::string trailing = trim(value.substr(closingQuote + 1));
|
||||||
if (!trailing.empty() && trailing.front() != '#')
|
if (!trailing.empty() && trailing.front() != '#')
|
||||||
{
|
{
|
||||||
throw makeParseError(
|
throw makeParseError(
|
||||||
envFilePath, lineNumber, "Unexpected text after quoted value.");
|
envFilePath,
|
||||||
|
lineNumber,
|
||||||
|
"Unexpected text after quoted value.");
|
||||||
}
|
}
|
||||||
std::string_view quotedBody = value.substr(1, closingQuote - 1);
|
std::string_view quotedBody = value.substr(1, closingQuote - 1);
|
||||||
if (quote == '"')
|
if (quote == '"') { return decodeDoubleQuotedValue(quotedBody); }
|
||||||
{
|
|
||||||
return decodeDoubleQuotedValue(quotedBody);
|
|
||||||
}
|
|
||||||
return std::string(quotedBody);
|
return std::string(quotedBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string parseValue(
|
static std::string parseValue(
|
||||||
const std::filesystem::path &envFilePath,
|
const std::filesystem::path &envFilePath,
|
||||||
std::size_t lineNumber,
|
std::size_t lineNumber,
|
||||||
std::string_view rawValue)
|
std::string_view rawValue)
|
||||||
{
|
{
|
||||||
std::string value = trim(rawValue);
|
std::string value = trim(rawValue);
|
||||||
if (value.empty())
|
if (value.empty()) { return {}; }
|
||||||
{
|
if (value.front() == '\'' || value.front() == '"') { return parseQuotedValue(envFilePath, lineNumber, value); }
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (value.front() == '\'' || value.front() == '"')
|
|
||||||
{
|
|
||||||
return parseQuotedValue(envFilePath, lineNumber, value);
|
|
||||||
}
|
|
||||||
return trim(value.substr(0, value.find('#')));
|
return trim(value.substr(0, value.find('#')));
|
||||||
}
|
|
||||||
|
|
||||||
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))};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
EnvKvStore::EnvKvStore(
|
EnvKvStore::EnvKvStore(
|
||||||
const std::vector<std::filesystem::path> &envFilePaths,
|
const std::vector<std::filesystem::path> &envFilePaths,
|
||||||
@@ -205,8 +194,7 @@ EnvKvStore::EnvKvStore(
|
|||||||
EnvKvStore::EnvKvStore(
|
EnvKvStore::EnvKvStore(
|
||||||
const std::vector<std::filesystem::path> &envFilePaths)
|
const std::vector<std::filesystem::path> &envFilePaths)
|
||||||
: EnvKvStore(envFilePaths, std::cerr)
|
: EnvKvStore(envFilePaths, std::cerr)
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
|
|
||||||
void EnvKvStore::loadFiles(
|
void EnvKvStore::loadFiles(
|
||||||
const std::vector<std::filesystem::path> &envFilePaths,
|
const std::vector<std::filesystem::path> &envFilePaths,
|
||||||
@@ -218,21 +206,115 @@ void EnvKvStore::loadFiles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> EnvKvStore::get(std::string_view name) const
|
std::optional<std::string> EnvKvStore::find(
|
||||||
|
std::string_view name,
|
||||||
|
bool bypassProcessEnvironment) const
|
||||||
{
|
{
|
||||||
std::string ownedName(name);
|
if (!bypassProcessEnvironment)
|
||||||
if (const char *value = std::getenv(ownedName.c_str()))
|
|
||||||
{
|
{
|
||||||
|
std::string ownedName(name);
|
||||||
|
if (const char *value = std::getenv(ownedName.c_str())) {
|
||||||
return std::string(value);
|
return std::string(value);
|
||||||
}
|
}
|
||||||
auto value = values.find(std::string(name));
|
|
||||||
if (value == values.end())
|
|
||||||
{
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto value = values.find(std::string(name));
|
||||||
|
if (value == values.end()) { return std::nullopt; }
|
||||||
return value->second;
|
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(
|
void EnvKvStore::loadFile(
|
||||||
const std::filesystem::path &envFilePath,
|
const std::filesystem::path &envFilePath,
|
||||||
std::ostream &warningStream)
|
std::ostream &warningStream)
|
||||||
@@ -249,11 +331,9 @@ void EnvKvStore::loadFile(
|
|||||||
while (std::getline(file, line))
|
while (std::getline(file, line))
|
||||||
{
|
{
|
||||||
++lineNumber;
|
++lineNumber;
|
||||||
if (lineIsBlankOrComment(line))
|
if (DotenvParser::lineIsBlankOrComment(line)) { continue; }
|
||||||
{
|
auto [name, value] =
|
||||||
continue;
|
DotenvParser::parseAssignment(envFilePath, lineNumber, line);
|
||||||
}
|
|
||||||
auto [name, value] = parseAssignment(envFilePath, lineNumber, line);
|
|
||||||
storeValue(envFilePath, name, value, warningStream);
|
storeValue(envFilePath, name, value, warningStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
add_library(spinscale_test_support STATIC
|
add_library(spinscale_test_support STATIC
|
||||||
support/threadHarness.cpp
|
support/threadHarness.cpp
|
||||||
support/probeComponentThread.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(spinscale_test_support PUBLIC
|
target_include_directories(spinscale_test_support PUBLIC
|
||||||
@@ -10,6 +9,7 @@ target_include_directories(spinscale_test_support PUBLIC
|
|||||||
|
|
||||||
target_link_libraries(spinscale_test_support PUBLIC
|
target_link_libraries(spinscale_test_support PUBLIC
|
||||||
spinscale
|
spinscale
|
||||||
|
spinscale_probe_support
|
||||||
gtest
|
gtest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+505
-45
@@ -1,8 +1,11 @@
|
|||||||
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -12,6 +15,16 @@
|
|||||||
|
|
||||||
namespace {
|
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
|
class EnvKvStoreTest
|
||||||
: public testing::Test
|
: public testing::Test
|
||||||
{
|
{
|
||||||
@@ -24,12 +37,12 @@ protected:
|
|||||||
.time_since_epoch().count())
|
.time_since_epoch().count())
|
||||||
+ "-" + std::to_string(testCounter++));
|
+ "-" + std::to_string(testCounter++));
|
||||||
std::filesystem::create_directories(root);
|
std::filesystem::create_directories(root);
|
||||||
unsetenv("SSCL_ENV_TEST_VALUE");
|
unsetTestEnvVars();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TearDown() override
|
void TearDown() override
|
||||||
{
|
{
|
||||||
unsetenv("SSCL_ENV_TEST_VALUE");
|
unsetTestEnvVars();
|
||||||
std::filesystem::remove_all(root);
|
std::filesystem::remove_all(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +56,26 @@ protected:
|
|||||||
return path;
|
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;
|
std::filesystem::path root;
|
||||||
static inline int testCounter = 0;
|
static inline int testCounter = 0;
|
||||||
};
|
};
|
||||||
@@ -65,69 +98,196 @@ TEST_F(EnvKvStoreTest, ParsesSupportedDotenvForms)
|
|||||||
std::ostringstream warnings;
|
std::ostringstream warnings;
|
||||||
sscl::EnvKvStore store({envFile}, warnings);
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
EXPECT_EQ(store.get("PLAIN"), "value");
|
EXPECT_EQ(store.find("PLAIN"), "value");
|
||||||
EXPECT_EQ(store.get("TRIMMED"), "value with spaces");
|
EXPECT_EQ(store.find("TRIMMED"), "value with spaces");
|
||||||
EXPECT_EQ(store.get("SINGLE"), " preserved value ");
|
EXPECT_EQ(store.find("SINGLE"), " preserved value ");
|
||||||
EXPECT_EQ(store.get("DOUBLE"), "another preserved value");
|
EXPECT_EQ(store.find("DOUBLE"), "another preserved value");
|
||||||
EXPECT_EQ(store.get("ESCAPED"), "quote: \" slash: \\ tab: \t");
|
EXPECT_EQ(store.find("ESCAPED"), "quote: \" slash: \\ tab: \t");
|
||||||
EXPECT_EQ(store.get("COMMENTED"), "value");
|
EXPECT_EQ(store.find("COMMENTED"), "value");
|
||||||
EXPECT_TRUE(warnings.str().empty());
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(EnvKvStoreTest, LaterFilesOverwriteEarlierFilesAndWarn)
|
TEST_F(EnvKvStoreTest, EmptyPathListYieldsEmptyStore)
|
||||||
{
|
{
|
||||||
std::filesystem::path first = writeFile("first.env", "VALUE=first\n");
|
|
||||||
std::filesystem::path second = writeFile("second.env", "VALUE=second\n");
|
|
||||||
|
|
||||||
std::ostringstream warnings;
|
std::ostringstream warnings;
|
||||||
sscl::EnvKvStore store({first, second}, warnings);
|
sscl::EnvKvStore store({}, warnings);
|
||||||
|
|
||||||
EXPECT_EQ(store.get("VALUE"), "second");
|
EXPECT_EQ(store.find("ANY"), std::nullopt);
|
||||||
EXPECT_NE(warnings.str().find("VALUE"), std::string::npos);
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
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, DuplicateKeysInsideSameFileOverwriteAndWarn)
|
TEST_F(EnvKvStoreTest, EmptyFileYieldsEmptyStore)
|
||||||
{
|
{
|
||||||
std::filesystem::path envFile =
|
std::filesystem::path envFile = writeFile("empty.env", "");
|
||||||
writeFile("one.env", "VALUE=first\nVALUE=second\n");
|
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;
|
std::ostringstream warnings;
|
||||||
sscl::EnvKvStore store({envFile}, warnings);
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
EXPECT_EQ(store.get("VALUE"), "second");
|
EXPECT_EQ(store.find("KEEP"), "yes");
|
||||||
EXPECT_NE(warnings.str().find("VALUE"), std::string::npos);
|
EXPECT_TRUE(warnings.str().empty());
|
||||||
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)
|
TEST_F(EnvKvStoreTest, EmptyUnquotedValueIsAccepted)
|
||||||
{
|
{
|
||||||
std::filesystem::path envFile =
|
std::filesystem::path envFile = writeFile("empty-value.env", "EMPTY=\n");
|
||||||
writeFile("one.env", "SSCL_ENV_TEST_VALUE=file\n");
|
std::ostringstream warnings;
|
||||||
setenv("SSCL_ENV_TEST_VALUE", "process", 1);
|
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;
|
std::ostringstream warnings;
|
||||||
sscl::EnvKvStore store({envFile}, warnings);
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
EXPECT_EQ(store.get("SSCL_ENV_TEST_VALUE"), "process");
|
EXPECT_EQ(store.find("A"), "before");
|
||||||
EXPECT_TRUE(warnings.str().empty());
|
EXPECT_EQ(store.find("B"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(EnvKvStoreTest, EmptyProcessEnvironmentValueOverridesStoreSilently)
|
TEST_F(EnvKvStoreTest, HashInsideQuotesIsLiteral)
|
||||||
{
|
{
|
||||||
std::filesystem::path envFile =
|
std::filesystem::path envFile = writeFile(
|
||||||
writeFile("one.env", "SSCL_ENV_TEST_VALUE=file\n");
|
"hash-quoted.env",
|
||||||
setenv("SSCL_ENV_TEST_VALUE", "", 1);
|
"SINGLE='#not-comment'\n"
|
||||||
|
"DOUBLE=\"#not-comment\"\n"
|
||||||
|
"DOUBLE_TRAIL=\"kept\" # trailing comment ok\n");
|
||||||
|
|
||||||
std::ostringstream warnings;
|
std::ostringstream warnings;
|
||||||
sscl::EnvKvStore store({envFile}, warnings);
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
|
|
||||||
EXPECT_EQ(store.get("SSCL_ENV_TEST_VALUE"), "");
|
EXPECT_EQ(store.find("SINGLE"), "#not-comment");
|
||||||
EXPECT_TRUE(warnings.str().empty());
|
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)
|
TEST_F(EnvKvStoreTest, MissingFileThrows)
|
||||||
@@ -138,20 +298,320 @@ TEST_F(EnvKvStoreTest, MissingFileThrows)
|
|||||||
std::runtime_error);
|
std::runtime_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(EnvKvStoreTest, MalformedLineThrows)
|
TEST_F(EnvKvStoreTest, UnreadableFileThrowsOpenFailure)
|
||||||
{
|
{
|
||||||
std::filesystem::path envFile = writeFile("bad.env", "NOT AN ASSIGNMENT\n");
|
std::filesystem::path envFile = writeFile("noread.env", "X=1\n");
|
||||||
std::ostringstream warnings;
|
std::filesystem::permissions(envFile, std::filesystem::perms::none);
|
||||||
|
|
||||||
|
std::ostringstream warnings;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sscl::EnvKvStore store({envFile}, warnings);
|
sscl::EnvKvStore store({envFile}, warnings);
|
||||||
FAIL() << "Expected malformed env file to throw.";
|
FAIL() << "Expected unreadable file open to throw.";
|
||||||
}
|
}
|
||||||
catch (const std::runtime_error &e)
|
catch (const std::runtime_error &e)
|
||||||
{
|
{
|
||||||
std::string message = e.what();
|
EXPECT_NE(
|
||||||
EXPECT_NE(message.find(envFile.string()), std::string::npos);
|
std::string(e.what()).find("Failed to open env file:"),
|
||||||
EXPECT_NE(message.find(":1:"), std::string::npos);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,65 +1,19 @@
|
|||||||
#ifndef SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
|
#ifndef SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
|
||||||
#define SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
|
#define SPINSCALE_TEST_SUPPORT_PROBE_COMPONENT_THREAD_H
|
||||||
|
|
||||||
#include <chrono>
|
/** EXPLANATION:
|
||||||
#include <exception>
|
* Compatibility shim: probe harness lives in spinscale_probe_support under
|
||||||
#include <functional>
|
* sscl::probe. Test code may keep including this path and using sscl::tests
|
||||||
#include <future>
|
* names; tools should include <probe/probeComponentThread.h> directly.
|
||||||
#include <memory>
|
*/
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
#include <spinscale/componentThread.h>
|
#include <probe/probeComponentThread.h>
|
||||||
#include <spinscale/co/invokers.h>
|
|
||||||
#include <spinscale/co/nonViralTaskNursery.h>
|
|
||||||
|
|
||||||
namespace sscl::tests {
|
namespace sscl::tests {
|
||||||
|
|
||||||
constexpr std::chrono::milliseconds defaultProbeTaskTimeout{10000};
|
using sscl::probe::defaultProbeTaskTimeout;
|
||||||
|
using sscl::probe::runNonViralNurseryOnComponentThread;
|
||||||
void runNonViralNurseryOnComponentThread(
|
using sscl::probe::ProbeComponentThreadHarness;
|
||||||
const std::shared_ptr<sscl::ComponentThread>& componentThread,
|
|
||||||
std::function<sscl::co::NonViralNonPostingInvoker(
|
|
||||||
sscl::co::NonViralTaskNursery::Slot::Lease&)> invokerFactory,
|
|
||||||
std::chrono::milliseconds timeout = defaultProbeTaskTimeout);
|
|
||||||
|
|
||||||
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::tests::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::tests
|
} // namespace sscl::tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#ifndef SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
#ifndef SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||||
#define SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
#define SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||||
|
|
||||||
|
#include <boostAsioLinkageFix.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <coroutine>
|
#include <coroutine>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -9,15 +12,17 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <boost/asio/deadline_timer.hpp>
|
|
||||||
#include <boost/asio/error.hpp>
|
#include <boost/asio/error.hpp>
|
||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
#include <boost/date_time/posix_time/posix_time_types.hpp>
|
#include <boost/asio/steady_timer.hpp>
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
|
|
||||||
namespace sscl::tests {
|
namespace sscl::tests {
|
||||||
|
|
||||||
using SharedDeadlineTimer = std::shared_ptr<boost::asio::deadline_timer>;
|
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
|
class CancelableDeadlineTimerRegistry
|
||||||
{
|
{
|
||||||
@@ -30,7 +35,7 @@ public:
|
|||||||
|
|
||||||
void registerTimer(
|
void registerTimer(
|
||||||
int labelMilliseconds,
|
int labelMilliseconds,
|
||||||
const SharedDeadlineTimer &timer)
|
const SharedSteadyTimer &timer)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> guard(mutex);
|
std::lock_guard<std::mutex> guard(mutex);
|
||||||
timersByLabel[labelMilliseconds] = timer;
|
timersByLabel[labelMilliseconds] = timer;
|
||||||
@@ -43,15 +48,15 @@ public:
|
|||||||
|
|
||||||
if (iterator == timersByLabel.end()) {
|
if (iterator == timersByLabel.end()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"No cancelable deadline_timer registered for label "
|
"No cancelable steady_timer registered for label "
|
||||||
+ std::to_string(labelMilliseconds));
|
+ std::to_string(labelMilliseconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedDeadlineTimer timer = iterator->second.lock();
|
const SharedSteadyTimer timer = iterator->second.lock();
|
||||||
|
|
||||||
if (!timer) {
|
if (!timer) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"Cancelable deadline_timer expired before cancel for label "
|
"Cancelable steady_timer expired before cancel for label "
|
||||||
+ std::to_string(labelMilliseconds));
|
+ std::to_string(labelMilliseconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +65,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
std::unordered_map<int, std::weak_ptr<boost::asio::deadline_timer>>
|
std::unordered_map<int, std::weak_ptr<boost::asio::steady_timer>>
|
||||||
timersByLabel;
|
timersByLabel;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,13 +74,13 @@ struct DeadlineTimerAwaiter
|
|||||||
DeadlineTimerAwaiter(
|
DeadlineTimerAwaiter(
|
||||||
boost::asio::io_context &ioContext,
|
boost::asio::io_context &ioContext,
|
||||||
int delayMilliseconds)
|
int delayMilliseconds)
|
||||||
: timer(std::make_shared<boost::asio::deadline_timer>(ioContext))
|
: timer(std::make_shared<boost::asio::steady_timer>(ioContext))
|
||||||
{
|
{
|
||||||
start(delayMilliseconds);
|
start(delayMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeadlineTimerAwaiter(
|
DeadlineTimerAwaiter(
|
||||||
SharedDeadlineTimer sharedTimer,
|
SharedSteadyTimer sharedTimer,
|
||||||
int delayMilliseconds)
|
int delayMilliseconds)
|
||||||
: timer(std::move(sharedTimer))
|
: timer(std::move(sharedTimer))
|
||||||
{
|
{
|
||||||
@@ -97,8 +102,8 @@ struct DeadlineTimerAwaiter
|
|||||||
private:
|
private:
|
||||||
void start(int delayMilliseconds)
|
void start(int delayMilliseconds)
|
||||||
{
|
{
|
||||||
timer->expires_from_now(
|
timer->expires_after(
|
||||||
boost::posix_time::milliseconds(delayMilliseconds));
|
std::chrono::milliseconds(delayMilliseconds));
|
||||||
timer->async_wait(
|
timer->async_wait(
|
||||||
[this](const boost::system::error_code &errorCode)
|
[this](const boost::system::error_code &errorCode)
|
||||||
{
|
{
|
||||||
@@ -110,7 +115,7 @@ private:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedDeadlineTimer timer;
|
SharedSteadyTimer timer;
|
||||||
boost::system::error_code completionErrorCode;
|
boost::system::error_code completionErrorCode;
|
||||||
bool waitCompleted = false;
|
bool waitCompleted = false;
|
||||||
std::coroutine_handle<> resumeHandle;
|
std::coroutine_handle<> resumeHandle;
|
||||||
@@ -123,7 +128,7 @@ struct RegisteredDeadlineTimerAwaiter
|
|||||||
int delayMilliseconds,
|
int delayMilliseconds,
|
||||||
int registrationLabelMilliseconds,
|
int registrationLabelMilliseconds,
|
||||||
CancelableDeadlineTimerRegistry ®istry)
|
CancelableDeadlineTimerRegistry ®istry)
|
||||||
: timer(std::make_shared<boost::asio::deadline_timer>(ioContext))
|
: timer(std::make_shared<boost::asio::steady_timer>(ioContext))
|
||||||
{
|
{
|
||||||
registry.registerTimer(registrationLabelMilliseconds, timer);
|
registry.registerTimer(registrationLabelMilliseconds, timer);
|
||||||
waiter.emplace(timer, delayMilliseconds);
|
waiter.emplace(timer, delayMilliseconds);
|
||||||
@@ -138,7 +143,7 @@ struct RegisteredDeadlineTimerAwaiter
|
|||||||
boost::system::error_code await_resume() const noexcept
|
boost::system::error_code await_resume() const noexcept
|
||||||
{ return waiter->await_resume(); }
|
{ return waiter->await_resume(); }
|
||||||
|
|
||||||
SharedDeadlineTimer timer;
|
SharedSteadyTimer timer;
|
||||||
std::optional<DeadlineTimerAwaiter> waiter;
|
std::optional<DeadlineTimerAwaiter> waiter;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,7 +152,7 @@ inline void throwIfTimerWaitFailed(
|
|||||||
{
|
{
|
||||||
if (waitError) {
|
if (waitError) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"deadline_timer wait failed: " + waitError.message());
|
"steady_timer wait failed: " + waitError.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user