AGENTS.md

June 24, 2026 · View on GitHub

Working notes for agents modifying this repository. For the design read ARCHITECTURE.md; for usage read README.md. This file covers the repo layout, how to build and test, the invariants you must not break, and the traps that are easy to fall into.

Repo map

threadpool.hh                 The pool: ThreadPool, ThreadPoolThread, PackagedTask, TaskWrapper, TaskQueue. Header.
thread.hh                     Thread<Impl,policy> CRTP base, ThreadPolicyType enum, thread-name free function decls. Header.
thread.cc                     The only .cc: run_thread/setup_thread/set_thread_name/get_thread_name/sched_getcpu.
blocking_concurrent_queue.h   Mutex + condvar queue the pool waits on. Header.
concurrent_queue.h            Mutex + std::deque queue (NOT lock-free). Header.
likely.h                      likely/unlikely macros; self-contained (no config.h).
threadpool_trace.h            No-op L_EXC + THREADPOOL_THREAD_REGISTER hook (stands in for Xapiand's log.h/traceback.h).
config.h.in                   CMake template for pthread feature macros; configured into build/config.h.
test/test.cc                  Runnable smoke test: pool of N tasks, async futures, TaskQueue.
CMakeLists.txt                STATIC library target `threadpool` (+ alias threadpool::threadpool) + CTest test `threadpool`.
LICENSE                       MIT, Copyright (c) 2015-2019 Dubalu LLC.
README.md                     What it is, install, usage, API reference, tracing.
ARCHITECTURE.md               Internal design, concurrency model, trade-offs.

Only thread.cc is compiled into the library; everything else is header-only. config.h is generated by CMake into the build directory, not checked in.

Build and run the test

cmake -B build && cmake --build build && ctest --test-dir build

Or directly (you must compile thread.cc too, it is not header-only):

c++ -std=c++20 -I. test/test.cc thread.cc -o test/test && ./test/test

Expected output ends with all threadpool tests passed, exit 0. The CMake threadpool target is a STATIC library that requests cxx_std_20, links Threads::Threads, and exposes both the source dir and the generated-config dir as PUBLIC includes. The test target is threadpool_test; the registered CTest name is threadpool.

Conventions

  • C++20. Required by std::format (worker naming in threadpool.hh). Don't drop below it.
  • No external dependencies. The only includes are the C++ standard library, pthreads, and the bundled headers. Do not add Xapiand headers (log.h, strings.hh, traceback.h) back; that is exactly what the trace hooks and std::format replaced.
  • Tracing and thread registration flow through two injectable hooks, both no-ops by default: L_EXC(...) and THREADPOOL_THREAD_REGISTER(pthread, name). Defaults live in threadpool_trace.h, each #ifndef-guarded. Both threadpool.hh and thread.cc reach them through #ifdef THREADPOOL_TRACE_HEADER (include the consumer's header) #else (include threadpool_trace.h). A consumer injects its own via -DTHREADPOOL_TRACE_HEADER='"my_trace.h"', or defines the macros first. Keep any new trace/registration call behind these macros; never assume they do anything.
  • config.h is for pthread feature macros only (HAVE_PTHREADS, HAVE_PTHREAD_SETNAME_NP, HAVE_PTHREAD_SET_NAME_NP, HAVE_PTHREAD_NP_H), generated by CMake from config.h.in. likely.h is deliberately self-contained and does NOT include config.h — it detects __builtin_expect with __has_builtin. Don't reintroduce a config.h dependency in likely.h.
  • Tabs for indentation, double quotes in code, no em dashes in prose.

Load-bearing invariants

  • ThreadPolicyType and the policy template parameter must stay. The enum and the ThreadPolicyType thread_policy parameter on Thread, ThreadPool, and ThreadPoolThread are kept for source-compatibility: Xapiand has 25+ call sites of the form ThreadPool<T, ThreadPolicyType::x>. The policy is currently runtime-ignored (run_thread/setup_thread take it unnamed), but deleting it breaks every one of those call sites. If you wire it up to do something (affinity, priority), that is a real behavior change to call out — do it inside run_thread/setup_thread so call sites stay unchanged.
  • PackagedTask's copy constructor must never actually run. It exists only to satisfy std::function's copyability requirement and assert(false)s (threadpool.hh). The contract is that the task is moved, never copied.
  • Worker shutdown is two-phase. end() enqueues one nullptr per thread and lets workers drain first; finish() sets _finished so workers stop as soon as possible. The worker loop checks _finished (acquire) each iteration and _ending only when it dequeues nothing. Keep end/finish/the worker loop in sync, or shutdown either hangs or drops queued tasks.
  • Counters are the only state queries. _enqueued/_running/_workers are atomics maintained around enqueue/dequeue/run. size() etc. read them with relaxed loads. Keep every fetch_add paired with its fetch_sub on the matching path (enqueue failure rolls back, dequeue decrements _enqueued, run brackets _running), or the reported numbers drift.
  • The blocking queue is mutex-backed, not lock-free. ConcurrentQueue is a std::deque under a std::mutex; BlockingConcurrentQueue adds a condvar. The class names match moodycamel's lock-free API but the implementation does not. Do not assume lock-free semantics anywhere.

How to extend

  • Add a pool state query / control. Follow the existing atomic-counter pattern; read with relaxed loads, never take a lock for a query.
  • Wire up a thread policy. Read thread_policy inside run_thread / setup_thread (thread.cc) — e.g. set affinity or priority per policy. Call sites and the template signatures stay exactly as they are.
  • Plug in tracing / crash registration. Inject a trace header with -DTHREADPOOL_TRACE_HEADER='"my_trace.h"' that defines L_EXC and THREADPOOL_THREAD_REGISTER, or define them before including the headers. To recover Xapiand's behavior, map THREADPOOL_THREAD_REGISTER to init_thread_info and L_EXC to log.h's macro.
  • Always extend the smoke test. test/test.cc is the only executable check. Any behavioral change should grow a corresponding assertion there.

Traps

  • Don't delete ThreadPolicyType thinking it is dead. It is unused at runtime but load-bearing at compile time for the consumer's call sites.
  • Don't reintroduce config.h into likely.h. It is self-contained on purpose so the headers work without the CMake-generated config.
  • Don't add Xapiand headers back. log.h, strings.hh, traceback.h were the whole point of the extraction. Route through the hooks and std::format.
  • TaskQueue::clear() uses ConcurrentQueueDefaultTraits::BLOCK_SIZE. The Xapiand original referenced Queue::BLOCK_SIZE, which this ConcurrentQueue does not define; it compiled only because clear() was never instantiated. The fix here points at the trait that actually defines it. If you re-sync from Xapiand, keep this fix.
  • Compile thread.cc. It is the one translation unit; forgetting it gives link errors for set_thread_name/run_thread/sched_getcpu.
  • sched_getcpu is macOS-only here. On Linux glibc already provides it; the bundled definition is guarded by #ifdef __APPLE__. On Apple Silicon it returns -1 (no user-space CPU id); callers must treat a negative result as "unknown".

Standalone vs. Xapiand

This is a standalone extraction from Xapiand. The delta from the original is pure decoupling:

  • threadpool.hh dropped #include "log.h" and #include "strings.hh". The two L_EXC(...) sites now resolve through threadpool_trace.h (no-op by default), and strings::format(pool->_format, idx) became std::vformat(pool->_format, std::make_format_args(idx)) (the format strings like "CH{:02}" are already std::format syntax).
  • thread.cc dropped #include "traceback.h"; the init_thread_info(...) call became THREADPOOL_THREAD_REGISTER(...) (no-op by default). The hardcoded "Xapiand:" name prefix became the THREADPOOL_THREAD_NAME_PREFIX macro (empty by default).
  • likely.h dropped #include "config.h" and detects __builtin_expect directly; config.h is now CMake-generated and carries only the pthread thread-naming feature macros that thread.cc needs.

The pool logic is otherwise unchanged. Keep extraction hygiene separate from behavior changes so they can be reconciled with upstream. To restore Xapiand's tracing/registration without editing these files, see "Tracing and thread registration" in README.md.