Current fix status — 2 October 2026
The capture-cleanup defect is fixed in #108 at b6c442a04746d420742ca0640111a65286fd22e9. Completed/canceled callback captures are released outside worker locks. The normal/throwing capture-cleanup regression covers one/four workers, alongside lifecycle teardown cases. Retained Linux Release and macOS sanitizer validation pass; the exact-head build check passed. All current review threads are resolved and independent Instrumentation approval is recorded.
Keep this issue open until foundation #127 and #108 are merged and the regressions pass on merged source. IOC #100 is refreshed to this explicit development pin while remaining draft.
Historical reproduction — 30 September 2026
The callback-owned lifetime path in #108 can deadlock when the completed callback's captures release the last adapter/pool owner. This blocks merging #108 and its dependent stack until corrected. Related release tracker: fermi-ad/redis-pvxs-ioc#99.
Affected revisions: #108 at 0bb9c35e7480e5d01afd58a5ed5ce27f3191f226 and descendants, including #111 at 0c3b0e6b7fa11910afba961d065f17ac749fd345.
In ThreadPool.hpp:93, work() reacquires the worker mutex before the local job function object is destroyed. Destruction of its captured final owner enters ThreadPool::stop(), which attempts to acquire that same non-recursive mutex at line 104. The self-thread detach logic is never reached.
Reproduction:
- Let a subscription callback capture a
shared_ptr<RedisAdapter> and block on a test barrier.
- While it is running, reset its
ReaderHandle, then drop the external adapter owner.
- Release the callback and let it return normally.
- Its capture cleanup enters adapter destruction and hangs.
The full adapter case reproduced against private standalone Redis 7.0.12 on macOS. A pool-only case at #108 reproduces without Redis:
last owner released; destructor entered
FAIL: destructor deadlocked after callback returned
Pool-only reproducer
Save as /tmp/pool_lifetime.cpp and compile from the checked-out PR with c++ -std=c++17 -pthread -I . /tmp/pool_lifetime.cpp -o /tmp/pool_lifetime. The two-second failure exit deliberately terminates the deadlocked test process.
#include "ThreadPool.hpp"
#include <future>
#include <iostream>
#include <cstdlib>
int main() {
std::promise<void> entered, release, destroyed;
auto permit = release.get_future().share();
auto done = destroyed.get_future();
auto pool = std::shared_ptr<ThreadPool>(new ThreadPool(1), [&](ThreadPool* p) {
std::cerr << "last owner released; destructor entered\n";
delete p;
destroyed.set_value();
});
pool->job("test", [keep = pool, &entered, permit] { entered.set_value(); permit.wait(); });
entered.get_future().wait();
pool.reset();
release.set_value();
if (done.wait_for(std::chrono::seconds(2)) != std::future_status::ready) {
std::cerr << "FAIL: destructor deadlocked after callback returned\n";
std::_Exit(2);
}
std::cout << "destructor completed\n";
}
Expected: the last owner can be released by normal callback capture cleanup without deadlock, self-join, or destruction of live worker storage.
A narrow experiment clearing the completed job while the worker mutex is still unlocked made the pool-only reproducer complete. No fix has been committed to the PR yet.
Fix PR: #108, pending implementation and validation. Add coverage for both explicit last-owner reset inside a callback and last-owner release when the completed callback object is destroyed. The existing lifecycle test covers only the former and currently passes despite this defect. Close only after the fix merges and the regressions pass.
Current fix status — 2 October 2026
The capture-cleanup defect is fixed in #108 at
b6c442a04746d420742ca0640111a65286fd22e9. Completed/canceled callback captures are released outside worker locks. The normal/throwing capture-cleanup regression covers one/four workers, alongside lifecycle teardown cases. Retained Linux Release and macOS sanitizer validation pass; the exact-head build check passed. All current review threads are resolved and independent Instrumentation approval is recorded.Keep this issue open until foundation #127 and #108 are merged and the regressions pass on merged source. IOC #100 is refreshed to this explicit development pin while remaining draft.
Historical reproduction — 30 September 2026
The callback-owned lifetime path in #108 can deadlock when the completed callback's captures release the last adapter/pool owner. This blocks merging #108 and its dependent stack until corrected. Related release tracker: fermi-ad/redis-pvxs-ioc#99.
Affected revisions: #108 at
0bb9c35e7480e5d01afd58a5ed5ce27f3191f226and descendants, including #111 at0c3b0e6b7fa11910afba961d065f17ac749fd345.In
ThreadPool.hpp:93,work()reacquires the worker mutex before the localjobfunction object is destroyed. Destruction of its captured final owner entersThreadPool::stop(), which attempts to acquire that same non-recursive mutex at line 104. The self-thread detach logic is never reached.Reproduction:
shared_ptr<RedisAdapter>and block on a test barrier.ReaderHandle, then drop the external adapter owner.The full adapter case reproduced against private standalone Redis 7.0.12 on macOS. A pool-only case at #108 reproduces without Redis:
Pool-only reproducer
Save as
/tmp/pool_lifetime.cppand compile from the checked-out PR withc++ -std=c++17 -pthread -I . /tmp/pool_lifetime.cpp -o /tmp/pool_lifetime. The two-second failure exit deliberately terminates the deadlocked test process.Expected: the last owner can be released by normal callback capture cleanup without deadlock, self-join, or destruction of live worker storage.
A narrow experiment clearing the completed
jobwhile the worker mutex is still unlocked made the pool-only reproducer complete. No fix has been committed to the PR yet.Fix PR: #108, pending implementation and validation. Add coverage for both explicit last-owner reset inside a callback and last-owner release when the completed callback object is destroyed. The existing lifecycle test covers only the former and currently passes despite this defect. Close only after the fix merges and the regressions pass.