push_blocking parked a scheduler worker inside the push. Nodes own a private single-thread pool, so the parked thread was the only one that could drain that node's own input — hold-and-wait, and under sustained backpressure four nodes of a five-node chain slept in nanosleep at once. channel.hpp already warned about this for sentinels; it applies just as much to data pushes. The scheduler was purely input-driven: on_input_ready() wakes a node when input arrives, with no counterpart for "my output has room". Lacking that signal, blocking the thread was the only way to handle a full output. This adds the missing half. - Channel::try_push + has_space + set_space_callback; the callback fires from both pop() and try_pop_now(). - PoolNode/PoolObjectNode keep a one-slot pending_ buffer with per-element done flags, so a retry cannot duplicate an already-accepted element. One slot suffices because queued_ admits at most one fire_once per node. - The re-check after clearing queued_ closes the lost-wakeup race where a space callback fires while the flag is still up and is swallowed. Two bugs surfaced once nodes actually parked, both fixed here: - pop_one reports an *empty* channel as ChannelClosedError, which is also the node's "upstream finished, self-stop" signal. A node woken by output space with empty inputs therefore killed itself. fire_once now releases the worker when its inputs are not ready rather than falling through. - The drained-park path resubmitted unconditionally instead of via on_input_ready(), firing nodes with nothing to read. compute_priority is now output-aware: mean output fill is deducted from mean input fill, mapped as 0.5·(1 + in - out). Input fill alone asks only "how much work is waiting for me"; a node whose outputs are already full cannot deliver, so running it just parks it again and wastes the slot while the node that would drain that channel waits behind it. The scheduler now favours whoever is furthest downstream of a bottleneck. Also adds a network-level error listener. A node's exception was discarded at the node boundary and survived only as a Closed event, which reports that a node stopped but not why — that missing detail is what made the above slow to diagnose. INode::set_network_error_callback plus StaticNetwork::set_error_handler forward it to the application. Tests: 121/121. test_backpressure_deadlock drives a five-node chain with capacity-2 channels against a slow sink and fails on the old code. The four test_pool_node overflow tests now assert parking rather than the removed drop-and-report behaviour. Known-incomplete: a rare hang remains, roughly 1 run in 20 against a 300s timeout, down from every run failing. Committed because the fix is a large strict improvement and the residual case needs its own reproduction.
79 lines
3.4 KiB
CMake
79 lines
3.4 KiB
CMake
cmake_minimum_required(VERSION 3.21)
|
|
|
|
# ── Catch2 ────────────────────────────────────────────────────────────────────
|
|
find_package(Catch2 3 QUIET)
|
|
if(NOT Catch2_FOUND)
|
|
include(FetchContent)
|
|
FetchContent_Declare(
|
|
Catch2
|
|
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
|
GIT_TAG v3.5.3
|
|
)
|
|
FetchContent_MakeAvailable(Catch2)
|
|
endif()
|
|
|
|
# ── Google Test ───────────────────────────────────────────────────────────────
|
|
find_package(GTest QUIET)
|
|
if(NOT GTest_FOUND)
|
|
include(FetchContent)
|
|
FetchContent_Declare(
|
|
googletest
|
|
GIT_REPOSITORY https://github.com/google/googletest.git
|
|
GIT_TAG v1.14.0
|
|
)
|
|
FetchContent_MakeAvailable(googletest)
|
|
endif()
|
|
|
|
# ── Test executable ───────────────────────────────────────────────────────────
|
|
add_executable(kpn_tests
|
|
test_fixed_string.cpp
|
|
test_traits.cpp
|
|
test_channel.cpp
|
|
test_node.cpp
|
|
test_network.cpp
|
|
test_static_network.cpp
|
|
test_shared_resource.cpp
|
|
test_pool_node.cpp
|
|
test_backpressure_deadlock.cpp
|
|
test_scheduler.cpp
|
|
)
|
|
|
|
target_link_libraries(kpn_tests PRIVATE
|
|
kpn
|
|
Catch2::Catch2WithMain
|
|
GTest::gtest
|
|
)
|
|
|
|
# ── Channel stress suite (separate executable) ────────────────────────────────
|
|
# Contended SPSC tests for the lock-free Channel<T>. Kept out of kpn_tests
|
|
# because each case runs many reps / tens of thousands of items and is slow.
|
|
# Most valuable under -DKPN_SANITIZER=thread, but correct (and run) without it.
|
|
add_executable(kpn_tests_stress test_channel_stress.cpp)
|
|
target_link_libraries(kpn_tests_stress PRIVATE kpn Catch2::Catch2WithMain)
|
|
|
|
# ── Sanitizer flags ───────────────────────────────────────────────────────────
|
|
# kpn_sanitizer_flags() is defined in the top-level CMakeLists and is a no-op
|
|
# unless -DKPN_SANITIZER=... is set. Sanitizer must be on both compile and link.
|
|
kpn_sanitizer_flags(_kpn_san)
|
|
if(_kpn_san)
|
|
foreach(_t kpn_tests kpn_tests_stress)
|
|
target_compile_options(${_t} PRIVATE ${_kpn_san})
|
|
target_link_options(${_t} PRIVATE ${_kpn_san})
|
|
endforeach()
|
|
endif()
|
|
|
|
include(CTest)
|
|
include(Catch)
|
|
|
|
# DISCOVERY_MODE PRE_TEST defers test enumeration to `ctest` run time. The
|
|
# default (POST_BUILD) runs each test binary during the build to list its
|
|
# cases — which fails a sanitizer build: a TSan/ASan binary needs a fixed
|
|
# address-space layout and aborts on startup ("unexpected memory mapping")
|
|
# under the container's ASLR, breaking the build before any test runs. The
|
|
# tsan.yaml job invokes the binaries directly (not via ctest), so deferring
|
|
# discovery costs nothing there and keeps `ctest` working for normal builds.
|
|
catch_discover_tests(kpn_tests DISCOVERY_MODE PRE_TEST)
|
|
# Register the stress suite under its own label so CI can run / time it
|
|
# separately from the fast unit tests.
|
|
catch_discover_tests(kpn_tests_stress DISCOVERY_MODE PRE_TEST PROPERTIES LABELS "stress")
|