From 3be30c7ec7af7db976bf6983a273011fef65a966 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Wed, 10 Jun 2026 23:59:31 +1000 Subject: [PATCH 1/7] chore(release): v0.6.0 - add CI, contribution scaffolding, fix unit-file casing Bump version to 0.6.0. No public API changes. Added: - .github/workflows/ci.yml - builds package, runs test suite, and builds all examples on Linux and Windows for every push/PR - CONTRIBUTING.md - build/test instructions, code style, PR workflow - .github/ISSUE_TEMPLATE/ (bug_report, feature_request, config) and .github/PULL_REQUEST_TEMPLATE.md - docs/release-notes-v0.6.0.md Changed: - Rename source files to match their unit identifier casing so the library compiles on case-sensitive filesystems (Linux): src/threadpool.simple.pas -> src/ThreadPool.Simple.pas src/threadpool.producerconsumer.pas -> src/ThreadPool.ProducerConsumer.pas tests/threadpool.producerconsumer.tests.pas -> tests/ThreadPool.ProducerConsumer.Tests.pas - tests/TestRunner.lpr: normalize unit reference (Threadpool. -> ThreadPool.) - README.md: replace hardcoded test-count badge with live CI badge, add Contributing section, bump version badge to 0.6.0 - package/lazarus/threadpool_fp.lpk: bump package version to 0.6 - CHANGELOG.md: add [0.6.0] entry --- .claude/settings.local.json | 10 ++- .github/ISSUE_TEMPLATE/bug_report.yml | 64 ++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 22 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 24 ++++++ .github/workflows/ci.yml | 67 +++++++++++++++++ CHANGELOG.md | 16 ++++ CONTRIBUTING.md | 73 +++++++++++++++++++ README.md | 9 ++- docs/release-notes-v0.6.0.md | 56 ++++++++++++++ package/lazarus/threadpool_fp.lpk | 2 +- ...er.pas => ThreadPool.ProducerConsumer.pas} | 0 ...dpool.simple.pas => ThreadPool.Simple.pas} | 0 tests/TestRunner.lpr | 2 +- ... => ThreadPool.ProducerConsumer.Tests.pas} | 0 15 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 CONTRIBUTING.md create mode 100644 docs/release-notes-v0.6.0.md rename src/{threadpool.producerconsumer.pas => ThreadPool.ProducerConsumer.pas} (100%) rename src/{threadpool.simple.pas => ThreadPool.Simple.pas} (100%) rename tests/{threadpool.producerconsumer.tests.pas => ThreadPool.ProducerConsumer.Tests.pas} (100%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 707722f..12853d2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,15 @@ { "permissions": { "allow": [ - "Bash(wc -l /c/Users/iwank/Documents/github/threadpool-fp/src/*.pas)" + "Bash(wc -l /c/Users/iwank/Documents/github/threadpool-fp/src/*.pas)", + "Bash(grep -iE '\\\\.\\(o|ppu|a|compiled|bak|lps|res\\)$|/lib/|/backup/')", + "Bash(grep -iE 'threadpool.*\\\\.pas$|\\\\.lpk$|\\\\.lpi$|\\\\.lpr$')", + "Bash(grep -iE '\\\\.\\(o|ppu|bak\\)$|/lib/|/backup/')", + "Bash(fpc -iV)", + "Bash(grep '\\\\.lpi$')", + "Bash(lazbuild tests/TestRunner.lpi)", + "Bash(git remote *)", + "Bash(git status *)" ] } } diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..756f2e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,64 @@ +name: πŸ› Bug Report +description: Report something that isn't working as expected +labels: ["bug"] +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug, including what you expected instead. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: A minimal code snippet or the steps that trigger the problem. + placeholder: | + 1. Create a TSimpleThreadPool with ... + 2. Queue ... + 3. Call WaitForAll ... + render: pascal + validations: + required: true + - type: dropdown + id: pool + attributes: + label: Which implementation? + options: + - ThreadPool.Simple + - ThreadPool.ProducerConsumer + - Both / not sure + validations: + required: true + - type: input + id: fpc-version + attributes: + label: Free Pascal version + placeholder: "3.2.2" + validations: + required: true + - type: input + id: lazarus-version + attributes: + label: Lazarus version (if used) + placeholder: "3.6.0" + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Windows + - Linux + - macOS + - Other + validations: + required: true + - type: textarea + id: extra + attributes: + label: Anything else? + description: Logs, error messages, or context that might help. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..5abb3cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: πŸ’¬ Questions & Discussion + url: https://github.com/ikelaiah/threadpool-fp/discussions + about: Ask usage questions or share what you built. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..20e6aae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,22 @@ +name: πŸ’‘ Feature Request +description: Suggest an idea or improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem would this solve? + description: Describe the use case or pain point motivating the request. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What would you like to see? An API sketch is welcome. + render: pascal + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches or workarounds you've thought about. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c6358aa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ + + +## What does this PR do? + + + +## Related issue + + + +## Type of change + +- [ ] πŸ› Bug fix +- [ ] ✨ New feature +- [ ] πŸ“ Documentation / examples +- [ ] πŸ”§ Refactor / internal change + +## Checklist + +- [ ] Tests pass locally (`lazbuild tests/TestRunner.lpi` then run `TestRunner`) +- [ ] Added/updated tests where it makes sense +- [ ] Updated `CHANGELOG.md` +- [ ] Updated docs in `docs/` if behavior or API changed +- [ ] New units use `{$mode objfpc}{$H+}{$J-}` and filenames match unit casing diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4165494 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + build-and-test: + name: Build & Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Lazarus + FPC + uses: gcarreno/setup-lazarus@v3 + with: + lazarus-version: stable + + - name: Build package + run: lazbuild package/lazarus/threadpool_fp.lpk + + - name: Build test runner + run: lazbuild tests/TestRunner.lpi + + - name: Run tests + shell: bash + run: | + if [ -f tests/TestRunner.exe ]; then + ./tests/TestRunner.exe -a -p --format=plain + else + ./tests/TestRunner -a -p --format=plain + fi + + build-examples: + name: Build examples (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Lazarus + FPC + uses: gcarreno/setup-lazarus@v3 + with: + lazarus-version: stable + + - name: Build all examples + shell: bash + run: | + set -e + for lpi in examples/*/*.lpi; do + echo "==> Building $lpi" + lazbuild "$lpi" + done diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b94ca..dff918d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.6.0] - 2026-06-10 + +### Added + +- GitHub Actions CI (`.github/workflows/ci.yml`) β€” builds the package, runs the FPCUnit test suite, and builds all examples on both Linux and Windows for every push and pull request +- `CONTRIBUTING.md` β€” build/test instructions, code style guide (including the unit-filename casing rule), and pull-request workflow +- Issue templates (`.github/ISSUE_TEMPLATE/`) for bug reports and feature requests, plus a discussions contact link +- Pull request template (`.github/PULL_REQUEST_TEMPLATE.md`) +- README: Contributing section linking to `CONTRIBUTING.md` + +### Changed + +- Renamed source files to match their `unit` identifier casing so the library compiles on case-sensitive filesystems (Linux): `threadpool.simple.pas` β†’ `ThreadPool.Simple.pas`, `threadpool.producerconsumer.pas` β†’ `ThreadPool.ProducerConsumer.pas`, and `tests/threadpool.producerconsumer.tests.pas` β†’ `tests/ThreadPool.ProducerConsumer.Tests.pas` +- Normalized the unit reference in `tests/TestRunner.lpr` (`Threadpool.` β†’ `ThreadPool.`) +- README: replaced the hardcoded test-count badge with a live CI status badge + ## [0.5.0] - 2026-04-13 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d08bcbe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# Contributing to ThreadPool for Free Pascal + +Thanks for your interest in improving this library! Contributions of all +sizes are welcome β€” bug reports, documentation fixes, new examples, and code. + +## Ways to contribute + +- πŸ› **Report a bug** β€” open an [issue](https://github.com/ikelaiah/threadpool-fp/issues) using the Bug Report template. +- πŸ’‘ **Request a feature** β€” open an issue using the Feature Request template. +- πŸ“ **Improve docs or examples** β€” these are especially appreciated and easy to start with. +- πŸ”§ **Fix a bug or add a feature** β€” see the workflow below. + +## Prerequisites + +- Free Pascal 3.2.2 or later +- Lazarus 3.6.0 or later (provides `lazbuild`) +- No external dependencies + +## Building + +Build the package: + +```bash +lazbuild package/lazarus/threadpool_fp.lpk +``` + +Or compile a single example directly with FPC: + +```bash +fpc -Fu./src examples/SimpleDemo/SimpleDemo.lpr +``` + +## Running the tests + +```bash +lazbuild tests/TestRunner.lpi +# Windows +./tests/TestRunner.exe -a -p --format=plain +# Linux / macOS +./tests/TestRunner -a -p --format=plain +``` + +All tests should pass before you submit a change. The full suite can take a +few minutes to run because some tests exercise high task volumes. + +CI runs the package build, the test suite, and all examples on Linux and +Windows for every pull request β€” please make sure your branch is green. + +## Code style + +- Use `{$mode objfpc}{$H+}{$J-}` at the top of new units and programs. +- Match the existing formatting: two-space indentation, `PascalCase` for + types/methods, and a descriptive comment for non-obvious logic. +- **Filenames must match the `unit` identifier's casing exactly** + (e.g. unit `ThreadPool.Simple` lives in `ThreadPool.Simple.pas`). This keeps + the library compiling on case-sensitive filesystems like Linux. +- Keep the library dependency-free (FCL only). + +## Pull request workflow + +1. Fork the repo and create a branch off `main` + (e.g. `feat/adaptive-threads` or `fix/queue-overflow`). +2. Make your change, including tests and/or an example where it makes sense. +3. Run the test suite locally and make sure it passes. +4. Update `CHANGELOG.md` and any relevant docs in `docs/`. +5. Open a pull request describing **what** changed and **why**. + +## Reporting security issues + +For anything sensitive, please email the maintainer rather than opening a +public issue. + +Thank you for helping make this library better! πŸ™ diff --git a/README.md b/README.md index c8726e3..fe1e8bd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # πŸš€ ThreadPool for Free Pascal -[![Version](https://img.shields.io/badge/version-0.5.0-8B5CF6.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-0.6.0-8B5CF6.svg)](CHANGELOG.md) [![License: MIT](https://img.shields.io/badge/License-MIT-1E3A8A.svg)](https://opensource.org/licenses/MIT) [![Free Pascal](https://img.shields.io/badge/Free%20Pascal-3.2.2+-3B82F6.svg)](https://www.freepascal.org/) [![Lazarus](https://img.shields.io/badge/Lazarus-4.0+-60A5FA.svg)](https://www.lazarus-ide.org/) ![Supports Windows](https://img.shields.io/badge/support-Windows-F59E0B?logo=Windows) ![Supports Linux](https://img.shields.io/badge/support-Linux-F59E0B?logo=Linux) ![No Dependencies](https://img.shields.io/badge/dependencies-none-10B981.svg) -![Tests](https://img.shields.io/badge/tests-35%20passed-10B981.svg) +[![CI](https://github.com/ikelaiah/threadpool-fp/actions/workflows/ci.yml/badge.svg)](https://github.com/ikelaiah/threadpool-fp/actions/workflows/ci.yml) [![Documentation](https://img.shields.io/badge/Docs-Available-brightgreen.svg)](docs/) [![Status](https://img.shields.io/badge/Status-Stable-10B981.svg)](README.md) @@ -522,6 +522,11 @@ GlobalThreadPool.WaitForAll; - More comprehensive tests - More examples +## 🀝 Contributing + +Contributions are welcome β€” bug reports, docs, examples, and code. See +[CONTRIBUTING.md](CONTRIBUTING.md) for how to build, test, and submit changes. + ## πŸ‘ Acknowledgments Special thanks to the Free Pascal and Lazarus communities and the creators of the threading libraries mentioned above for inspiration! diff --git a/docs/release-notes-v0.6.0.md b/docs/release-notes-v0.6.0.md new file mode 100644 index 0000000..db0a2f6 --- /dev/null +++ b/docs/release-notes-v0.6.0.md @@ -0,0 +1,56 @@ +# ThreadPool for Free Pascal β€” v0.6.0 + +This release focuses on **developer experience and cross-platform reliability**. +There are no public API changes β€” existing code continues to compile and run +unchanged. + +## What's new + +### Continuous Integration + +- **GitHub Actions CI** (`.github/workflows/ci.yml`) β€” every push and pull + request now builds the package, runs the full FPCUnit test suite, and builds + all examples on both **Linux and Windows**. +- **Live CI badge** β€” the README's hardcoded test-count badge has been replaced + with a real build-status badge, so the README always reflects the current + state of the `main` branch. + +### Easier contributing + +- **`CONTRIBUTING.md`** β€” build and test instructions, code style guide + (including the unit-filename casing rule), and the pull-request workflow. +- **Issue templates** (`.github/ISSUE_TEMPLATE/`) β€” structured forms for bug + reports and feature requests, plus a link to Discussions for questions. +- **Pull request template** (`.github/PULL_REQUEST_TEMPLATE.md`) β€” a checklist + covering tests, changelog, docs, and the unit-casing convention. +- **README: Contributing section** linking to `CONTRIBUTING.md`. + +## Cross-platform fix + +Unit filenames now match their `unit` identifier casing exactly, so the library +compiles on case-sensitive filesystems such as Linux. On Windows this was +masked by the case-insensitive filesystem. + +| Before | After | +| --- | --- | +| `src/threadpool.simple.pas` | `src/ThreadPool.Simple.pas` | +| `src/threadpool.producerconsumer.pas` | `src/ThreadPool.ProducerConsumer.pas` | +| `tests/threadpool.producerconsumer.tests.pas` | `tests/ThreadPool.ProducerConsumer.Tests.pas` | + +The unit reference in `tests/TestRunner.lpr` was also normalized +(`Threadpool.` β†’ `ThreadPool.`). + +## Upgrade notes + +- **No code changes required.** `uses ThreadPool.Simple;`, + `uses ThreadPool.ProducerConsumer;`, etc. are unchanged. +- If you reference the source files by literal path (rare), update to the new + capitalized filenames listed above. + +## Verification + +- `lazbuild tests/TestRunner.lpi` builds clean (0 errors) after the renames. + +## Full changelog + +See [CHANGELOG.md](../CHANGELOG.md) for the complete version history. diff --git a/package/lazarus/threadpool_fp.lpk b/package/lazarus/threadpool_fp.lpk index 47bfc2f..247293c 100644 --- a/package/lazarus/threadpool_fp.lpk +++ b/package/lazarus/threadpool_fp.lpk @@ -81,7 +81,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "/> - + diff --git a/src/threadpool.producerconsumer.pas b/src/ThreadPool.ProducerConsumer.pas similarity index 100% rename from src/threadpool.producerconsumer.pas rename to src/ThreadPool.ProducerConsumer.pas diff --git a/src/threadpool.simple.pas b/src/ThreadPool.Simple.pas similarity index 100% rename from src/threadpool.simple.pas rename to src/ThreadPool.Simple.pas diff --git a/tests/TestRunner.lpr b/tests/TestRunner.lpr index 6eb89cb..93e9466 100644 --- a/tests/TestRunner.lpr +++ b/tests/TestRunner.lpr @@ -5,7 +5,7 @@ uses Classes, consoletestrunner, ThreadPool.Simple.Tests, - Threadpool.ProducerConsumer.Tests; + ThreadPool.ProducerConsumer.Tests; type diff --git a/tests/threadpool.producerconsumer.tests.pas b/tests/ThreadPool.ProducerConsumer.Tests.pas similarity index 100% rename from tests/threadpool.producerconsumer.tests.pas rename to tests/ThreadPool.ProducerConsumer.Tests.pas From f02706f21d845a27bb870f912aa8578ed4c22426 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Thu, 11 Jun 2026 00:28:07 +1000 Subject: [PATCH 2/7] fix(ci): build on Linux and resolve src/ in example default build mode Two pre-existing bugs surfaced by the new CI; neither is a regression from the v0.6.0 file renames. - src/ThreadPool.Simple.pas: TSimpleWorkerThread hardcoded `stdcall` on QueryInterface/_AddRef/_Release. FPC's IInterface uses stdcall only on Windows and cdecl elsewhere, so the signatures had no matching implementation on Linux (error 5042). Guard all three declarations and implementations with {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}. - examples/*/*.lpi (all except Starter): OtherUnitFiles="..\..\src" was set only in the Release build mode, but lazbuild builds the Default mode, so ThreadPool.* could not be found (error 10022) on both Linux and Windows. Add OtherUnitFiles to each project's global CompilerOptions search paths, matching the working Starter example. Verified locally: package, test runner, and all 8 examples build clean. --- .../ProdConMessageProcessor.lpi | 1 + examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi | 1 + .../ProdConSquareNumbers/ProdConSquareNumbers.lpi | 1 + examples/SimpleDemo/SimpleDemo.lpi | 1 + examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi | 1 + .../SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi | 1 + examples/SimpleWordCounter/SimpleWordCounter.lpi | 1 + src/ThreadPool.Simple.pas | 12 ++++++------ 8 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi index d7639f3..c9ae83f 100644 --- a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi +++ b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi @@ -101,6 +101,7 @@ + diff --git a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi index ccbbecd..a0146dd 100644 --- a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi +++ b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi @@ -101,6 +101,7 @@ + diff --git a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi index 93b1b91..708e892 100644 --- a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi +++ b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi @@ -101,6 +101,7 @@ + diff --git a/examples/SimpleDemo/SimpleDemo.lpi b/examples/SimpleDemo/SimpleDemo.lpi index 7694efa..e208d83 100644 --- a/examples/SimpleDemo/SimpleDemo.lpi +++ b/examples/SimpleDemo/SimpleDemo.lpi @@ -101,6 +101,7 @@ + diff --git a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi index 07ab534..25746f4 100644 --- a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi +++ b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi @@ -101,6 +101,7 @@ + diff --git a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi index 80a10c9..9372804 100644 --- a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi +++ b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi @@ -101,6 +101,7 @@ + diff --git a/examples/SimpleWordCounter/SimpleWordCounter.lpi b/examples/SimpleWordCounter/SimpleWordCounter.lpi index 9319f99..1c59692 100644 --- a/examples/SimpleWordCounter/SimpleWordCounter.lpi +++ b/examples/SimpleWordCounter/SimpleWordCounter.lpi @@ -101,6 +101,7 @@ + diff --git a/src/ThreadPool.Simple.pas b/src/ThreadPool.Simple.pas index 271b931..e82f45f 100644 --- a/src/ThreadPool.Simple.pas +++ b/src/ThreadPool.Simple.pas @@ -45,9 +45,9 @@ TSimpleWorkerThread = class(TThread, IWorkerThread) procedure Terminate; procedure WaitFor; function GetThreadID: TThreadID; - function QueryInterface(constref IID: TGUID; out Obj): HResult; stdcall; - function _AddRef: Integer; stdcall; - function _Release: Integer; stdcall; + function QueryInterface(constref IID: TGUID; out Obj): HResult; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; + function _AddRef: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; + function _Release: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; end; {$ENDREGION} @@ -130,7 +130,7 @@ function TSimpleWorkerThread.GetThreadID: TThreadID; Result := ThreadID; end; -function TSimpleWorkerThread.QueryInterface(constref IID: TGUID; out Obj): HResult; stdcall; +function TSimpleWorkerThread.QueryInterface(constref IID: TGUID; out Obj): HResult; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; begin if GetInterface(IID, Obj) then Result := S_OK @@ -138,12 +138,12 @@ function TSimpleWorkerThread.QueryInterface(constref IID: TGUID; out Obj): HResu Result := E_NOINTERFACE; end; -function TSimpleWorkerThread._AddRef: Integer; stdcall; +function TSimpleWorkerThread._AddRef: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; begin Result := InterlockedIncrement(FRefCount); end; -function TSimpleWorkerThread._Release: Integer; stdcall; +function TSimpleWorkerThread._Release: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; begin Result := InterlockedDecrement(FRefCount); if Result = 0 then From 378fe0e7d5d161e84e2f35ebeadc15a9fb9da454 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Thu, 11 Jun 2026 08:24:14 +1000 Subject: [PATCH 3/7] fix: make IWorkerThread non-ref-counted to prevent worker double-free The cdecl calling-convention fix correctly engaged IWorkerThread's interface methods on Linux, which exposed a latent double-free: _Release called Destroy at refcount 0 while the pool also frees workers explicitly in ClearThreads. On Windows the prior convention mismatch masked this; on Linux it caused an EAccessViolation during unit finalization (exit code 217). - src/ThreadPool.Simple.pas: _AddRef/_Release now return -1 (non-ref-counted, the TComponent contract). The pool owns worker lifetime via FThreads / ClearThreads, so interface assignment must never free a live worker. Remove the now-unused FRefCount field. - tests/ThreadPool.Simple.Tests.pas: Test03_WorkerThreadInterface relied on interface refcounting to free a worker it created directly; free it explicitly with nested try/finally to match the pool's ownership model. --- src/ThreadPool.Simple.pas | 13 +++++++------ tests/threadpool.simple.tests.pas | 12 +++++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ThreadPool.Simple.pas b/src/ThreadPool.Simple.pas index e82f45f..53d53c3 100644 --- a/src/ThreadPool.Simple.pas +++ b/src/ThreadPool.Simple.pas @@ -33,7 +33,6 @@ TSimpleWorkItem = class(TInterfacedObject, IWorkItem) { Simple worker thread implementation } TSimpleWorkerThread = class(TThread, IWorkerThread) private - FRefCount: Integer; FThreadPool: TObject; protected procedure Execute; override; @@ -100,7 +99,6 @@ implementation constructor TSimpleWorkerThread.Create(AThreadPool: TObject); begin inherited Create(True); // Create suspended - FRefCount := 0; FThreadPool := AThreadPool; FreeOnTerminate := False; end; @@ -140,14 +138,17 @@ function TSimpleWorkerThread.QueryInterface(constref IID: TGUID; out Obj): HResu function TSimpleWorkerThread._AddRef: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; begin - Result := InterlockedIncrement(FRefCount); + // The pool owns this thread's lifetime via FThreads/ClearThreads, so the + // IWorkerThread interface must NOT reference-count. Returning -1 marks this + // as a non-ref-counted interface (the same contract TComponent uses), which + // prevents an interface assignment from freeing the still-live worker. + Result := -1; end; function TSimpleWorkerThread._Release: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; begin - Result := InterlockedDecrement(FRefCount); - if Result = 0 then - Destroy; + // See _AddRef: lifetime is owned by the pool, never by interface refcount. + Result := -1; end; procedure TSimpleWorkerThread.Execute; diff --git a/tests/threadpool.simple.tests.pas b/tests/threadpool.simple.tests.pas index 7353875..b2b4150 100644 --- a/tests/threadpool.simple.tests.pas +++ b/tests/threadpool.simple.tests.pas @@ -186,10 +186,16 @@ procedure TSimpleThreadPoolTests.Test03_WorkerThreadInterface; Pool := TSimpleThreadPool.Create(4); try WorkerThread := TSimpleWorkerThread.Create(Pool); - Thread := WorkerThread; // Explicit interface conversion - AssertTrue('TSimpleWorkerThread should implement IWorkerThread', Thread <> nil); + try + Thread := WorkerThread; // Explicit interface conversion + AssertTrue('TSimpleWorkerThread should implement IWorkerThread', Thread <> nil); + finally + // IWorkerThread is non-ref-counted (the pool owns worker lifetime), so + // the worker must be freed explicitly rather than via the interface. + Thread := nil; + WorkerThread.Free; + end; finally - Thread := nil; Pool.Free; end; end; From 8858bd878c1683579143b636e20a69ecbccc4e04 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Thu, 11 Jun 2026 08:28:07 +1000 Subject: [PATCH 4/7] feat(doc): update docs --- CHANGELOG.md | 6 ++++++ docs/release-notes-v0.6.0.md | 26 +++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff918d..201c118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Normalized the unit reference in `tests/TestRunner.lpr` (`Threadpool.` β†’ `ThreadPool.`) - README: replaced the hardcoded test-count badge with a live CI status badge +### Fixed + +- `ThreadPool.Simple` now compiles on non-Windows targets: the `IWorkerThread` methods (`QueryInterface`/`_AddRef`/`_Release`) hardcoded the `stdcall` calling convention, which only matches `IInterface` on Windows β€” FPC uses `cdecl` elsewhere. Guarded with `{$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}` +- Worker-thread double-free that caused an `EAccessViolation` on Linux during unit finalization: `IWorkerThread` is now non-ref-counted (the pool owns worker lifetime explicitly via `ClearThreads`), so an interface assignment can no longer free a live worker. Removed the now-unused `FRefCount` field +- Example projects (all except `Starter`) only had the `src` search path in their `Release` build mode, so `lazbuild` (which builds the `Default` mode) could not find the units. Added the search path to each project's global compiler options + ## [0.5.0] - 2026-04-13 ### Added diff --git a/docs/release-notes-v0.6.0.md b/docs/release-notes-v0.6.0.md index db0a2f6..d780a6a 100644 --- a/docs/release-notes-v0.6.0.md +++ b/docs/release-notes-v0.6.0.md @@ -40,6 +40,26 @@ masked by the case-insensitive filesystem. The unit reference in `tests/TestRunner.lpr` was also normalized (`Threadpool.` β†’ `ThreadPool.`). +## Bug fixes + +Setting up CI surfaced and fixed three issues that previously only affected +Linux (and example builds) β€” they were masked on Windows: + +- **Compilation on non-Windows targets.** `TSimpleWorkerThread` hardcoded the + `stdcall` calling convention on its `IWorkerThread` methods + (`QueryInterface`/`_AddRef`/`_Release`). FPC's `IInterface` only uses + `stdcall` on Windows and `cdecl` elsewhere, so the implementations didn't + match on Linux. Now guarded with `{$IFDEF WINDOWS}`. +- **Worker-thread double-free on Linux.** Once the interface methods worked + correctly, `_Release` calling `Destroy` clashed with the pool freeing its + workers explicitly, causing an `EAccessViolation` during finalization. + `IWorkerThread` is now non-ref-counted β€” the pool owns worker lifetime via + `ClearThreads`. +- **Example projects couldn't find the library.** Every example except + `Starter` had the `src` search path only in its `Release` build mode, so + `lazbuild` (which builds the `Default` mode) failed with "Can't find unit". + The search path is now in each project's global compiler options. + ## Upgrade notes - **No code changes required.** `uses ThreadPool.Simple;`, @@ -49,7 +69,11 @@ The unit reference in `tests/TestRunner.lpr` was also normalized ## Verification -- `lazbuild tests/TestRunner.lpi` builds clean (0 errors) after the renames. +CI builds the package, runs the test suite, and builds all 8 examples on both +**Linux and Windows**: + +- Test suite: **35 tests, 0 errors, 0 failures, 0 unfreed memory blocks**. +- All 8 example projects build cleanly on both platforms. ## Full changelog From 6f3b7676cc06dd89efa348dbdcd73c664df2e400 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Thu, 11 Jun 2026 08:29:04 +1000 Subject: [PATCH 5/7] feat(doc): update date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 201c118..39e06f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## [0.6.0] - 2026-06-10 +## [0.6.0] - 2026-06-11 ### Added From 44fc43548f43e31ec87663382c15564807388d66 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Thu, 11 Jun 2026 08:41:10 +1000 Subject: [PATCH 6/7] fix: add cthreads on Unix to stop Linux runtime access violation The Linux CI job crashed at runtime with an EAccessViolation (exit code 217) while the thread pool was constructing its workers. Root cause: FPC programs that create threads must include `cthreads` as the first unit on Unix/Linux; the test runner and examples didn't, so thread and synchronization-object setup faulted. Windows does not need cthreads and was unaffected, which is why the crash was Linux-only. - tests/TestRunner.lpr and all 8 examples: add {$IFDEF UNIX}cthreads{$ENDIF} as the first unit. - src/ThreadPool.Simple.pas: harden worker/pool lifetime to remove a latent cross-platform footgun uncovered while diagnosing this: - IWorkerThread is now explicitly non-ref-counted (_AddRef/_Release return -1, the TComponent contract), so an interface assignment can never free a worker the pool owns via ClearThreads. Removed the unused FRefCount field. - TSimpleThreadPool.Destroy is now safe on a partially constructed pool: it skips WaitForAll and cleanup when the relevant fields are nil. - CHANGELOG.md, docs/release-notes-v0.6.0.md: document the cthreads root cause and the lifetime hardening under 0.6.0 "Fixed". --- CHANGELOG.md | 4 +++- docs/release-notes-v0.6.0.md | 22 ++++++++++++------- .../ProdConMessageProcessor.lpr | 3 +++ .../ProdConSimpleDemo/ProdConSimpleDemo.lpr | 3 +++ .../ProdConSquareNumbers.lpr | 3 +++ examples/SimpleDemo/SimpleDemo.lpr | 3 +++ .../SimpleSquareNumbers.lpr | 3 +++ .../SimpleThreadpoolDemo.lpr | 3 +++ .../SimpleWordCounter/SimpleWordCounter.lpr | 3 +++ examples/Starter/Starter.lpr | 3 +++ src/ThreadPool.Simple.pas | 21 +++++++++++++----- tests/TestRunner.lpr | 3 +++ 12 files changed, 59 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e06f2..5dc7b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **Access violation on Linux at runtime (exit code 217).** The test runner and example programs were missing the `cthreads` unit, which Unix/Linux requires (as the first unit) for any program that creates threads. Without it, constructing the thread pool crashed in the worker/sync-object setup. Added `{$IFDEF UNIX}cthreads{$ENDIF}` to `tests/TestRunner.lpr` and all 8 example programs. Windows was unaffected and did not need it - `ThreadPool.Simple` now compiles on non-Windows targets: the `IWorkerThread` methods (`QueryInterface`/`_AddRef`/`_Release`) hardcoded the `stdcall` calling convention, which only matches `IInterface` on Windows β€” FPC uses `cdecl` elsewhere. Guarded with `{$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}` -- Worker-thread double-free that caused an `EAccessViolation` on Linux during unit finalization: `IWorkerThread` is now non-ref-counted (the pool owns worker lifetime explicitly via `ClearThreads`), so an interface assignment can no longer free a live worker. Removed the now-unused `FRefCount` field +- `TSimpleWorkerThread` is now explicitly non-ref-counted (`_AddRef`/`_Release` return `-1`, the `TComponent` contract), so an `IWorkerThread` interface assignment can never free a worker the pool still owns via `ClearThreads`. Removed the now-unused `FRefCount` field +- `TSimpleThreadPool.Destroy` is now safe to call on a partially constructed pool: it skips `WaitForAll` and the queue/thread cleanup when the relevant fields are `nil`, instead of dereferencing them - Example projects (all except `Starter`) only had the `src` search path in their `Release` build mode, so `lazbuild` (which builds the `Default` mode) could not find the units. Added the search path to each project's global compiler options ## [0.5.0] - 2026-04-13 diff --git a/docs/release-notes-v0.6.0.md b/docs/release-notes-v0.6.0.md index d780a6a..253e084 100644 --- a/docs/release-notes-v0.6.0.md +++ b/docs/release-notes-v0.6.0.md @@ -42,19 +42,25 @@ The unit reference in `tests/TestRunner.lpr` was also normalized ## Bug fixes -Setting up CI surfaced and fixed three issues that previously only affected -Linux (and example builds) β€” they were masked on Windows: - +Setting up CI surfaced several issues that previously only affected Linux (and +example builds) β€” all were masked on Windows: + +- **Access violation on Linux at runtime (exit code 217).** The test runner and + example programs were missing the `cthreads` unit. On Unix/Linux, any program + that creates threads must include `cthreads` as its first unit; without it, + constructing the thread pool crashed while setting up its worker threads and + synchronization objects. `{$IFDEF UNIX}cthreads{$ENDIF}` was added to the test + runner and all 8 examples. Windows does not need it and is unaffected. - **Compilation on non-Windows targets.** `TSimpleWorkerThread` hardcoded the `stdcall` calling convention on its `IWorkerThread` methods (`QueryInterface`/`_AddRef`/`_Release`). FPC's `IInterface` only uses `stdcall` on Windows and `cdecl` elsewhere, so the implementations didn't match on Linux. Now guarded with `{$IFDEF WINDOWS}`. -- **Worker-thread double-free on Linux.** Once the interface methods worked - correctly, `_Release` calling `Destroy` clashed with the pool freeing its - workers explicitly, causing an `EAccessViolation` during finalization. - `IWorkerThread` is now non-ref-counted β€” the pool owns worker lifetime via - `ClearThreads`. +- **Hardened worker lifetime.** `IWorkerThread` is now explicitly + non-ref-counted (`_AddRef`/`_Release` return `-1`), so an interface + assignment can never free a worker the pool still owns via `ClearThreads`. + `TSimpleThreadPool.Destroy` is also now safe to run on a partially + constructed pool. - **Example projects couldn't find the library.** Every example except `Starter` had the `src` search path only in its `Release` build mode, so `lazbuild` (which builds the `Default` mode) failed with "Can't find unit". diff --git a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr index fb3b235..57e1747 100644 --- a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr +++ b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+}{$J-} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, SysUtils, ThreadPool.ProducerConsumer; type diff --git a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr index 966dc67..3042a4e 100644 --- a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr +++ b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+}{$J-} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, SysUtils, ThreadPool.ProducerConsumer; type diff --git a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr index 5919d17..fb3b7b6 100644 --- a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr +++ b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+}{$J-} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, SysUtils, ThreadPool.ProducerConsumer, SyncObjs; const diff --git a/examples/SimpleDemo/SimpleDemo.lpr b/examples/SimpleDemo/SimpleDemo.lpr index 16cb206..3d126e9 100644 --- a/examples/SimpleDemo/SimpleDemo.lpr +++ b/examples/SimpleDemo/SimpleDemo.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+}{$J-} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, SysUtils, ThreadPool.Simple; type diff --git a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr index a82f971..fc5e743 100644 --- a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr +++ b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+}{$J-} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, SysUtils, ThreadPool.Simple; const diff --git a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr index fa2ee57..95e540b 100644 --- a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr +++ b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+}{$J-} // Standard FPC mode directives uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, // For TThread and other basic classes SysUtils, // For string handling and formatting ThreadPool.Simple, // Our thread pool implementation diff --git a/examples/SimpleWordCounter/SimpleWordCounter.lpr b/examples/SimpleWordCounter/SimpleWordCounter.lpr index 733e841..397d083 100644 --- a/examples/SimpleWordCounter/SimpleWordCounter.lpr +++ b/examples/SimpleWordCounter/SimpleWordCounter.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+}{$J-} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, SysUtils, ThreadPool.Simple, syncobjs; const diff --git a/examples/Starter/Starter.lpr b/examples/Starter/Starter.lpr index cc6cad3..2aa0830 100644 --- a/examples/Starter/Starter.lpr +++ b/examples/Starter/Starter.lpr @@ -32,6 +32,9 @@ {$mode objfpc}{$H+}{$J-} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, SysUtils, ThreadPool.Simple; // --------------------------------------------------------------------------- diff --git a/src/ThreadPool.Simple.pas b/src/ThreadPool.Simple.pas index 53d53c3..5781d51 100644 --- a/src/ThreadPool.Simple.pas +++ b/src/ThreadPool.Simple.pas @@ -283,19 +283,28 @@ constructor TSimpleThreadPool.Create(AThreadCount: Integer = 0); destructor TSimpleThreadPool.Destroy; begin - WaitForAll; // Ensure all tasks complete before destroying + // Only drain outstanding work if the pool was fully constructed. If the + // constructor failed partway, FPC still calls this destructor on the + // half-built object, so guard against the sync objects being nil. + if Assigned(FWorkItemEvent) and Assigned(FErrorEvent) then + WaitForAll; // Ensure all tasks complete before destroying + FShutdown := True; - ClearWorkItems; - ClearThreads; - - // Clean up synchronization objects + + if Assigned(FWorkItems) then + ClearWorkItems; + if Assigned(FThreads) then + ClearThreads; + + // Clean up synchronization objects (each may be nil after a partial + // construction, so Free β€” which is nil-safe β€” is used throughout). FWorkItemLock.Free; FThreads.Free; FWorkItems.Free; FWorkItemEvent.Free; FErrorEvent.Free; FErrorLock.Free; - + inherited Destroy; end; diff --git a/tests/TestRunner.lpr b/tests/TestRunner.lpr index 93e9466..377faa4 100644 --- a/tests/TestRunner.lpr +++ b/tests/TestRunner.lpr @@ -3,6 +3,9 @@ {$mode objfpc}{$H+} uses + {$IFDEF UNIX} + cthreads, // MUST be first: enables threading support on Unix/Linux + {$ENDIF} Classes, consoletestrunner, ThreadPool.Simple.Tests, ThreadPool.ProducerConsumer.Tests; From 624aa30a29b5fd4ce7ae8acc157e3613eb6f9b05 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Thu, 11 Jun 2026 13:02:41 +1000 Subject: [PATCH 7/7] fix(test): make thread-count and queue-full tests core-count independent The Linux CI suite ran cleanly after the cthreads fix but three tests failed with assertion errors, because they hardcoded expectations that only hold on a multi-core machine. The pool enforces a minimum of 4 worker threads and a 2x- core cap, which a low-core CI runner exposed. - Test04_CreateDestroy / Test11_ThreadCount (ThreadPool.Simple.Tests): assert against the actual Max(Min(requested, 2*ProcessorCount), 4) formula instead of raw multiples, so they pass on any core count. Add Math to uses. - Test08_QueueFullBehavior (ThreadPool.ProducerConsumer.Tests): the test assumed a single worker would keep a 2-slot queue full, but the 4-thread minimum meant the queue drained before overflowing on Linux. It now bursts ThreadCount + QueueSize + 1 long-running tasks, which overflows a fixed queue deterministically regardless of scheduling or core count. - CHANGELOG.md, docs/release-notes-v0.6.0.md: document the test fixes. --- CHANGELOG.md | 2 + docs/release-notes-v0.6.0.md | 6 +++ tests/ThreadPool.ProducerConsumer.Tests.pas | 44 ++++++++++----------- tests/threadpool.simple.tests.pas | 32 +++++++++------ 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc7b39..503ae42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `TSimpleWorkerThread` is now explicitly non-ref-counted (`_AddRef`/`_Release` return `-1`, the `TComponent` contract), so an `IWorkerThread` interface assignment can never free a worker the pool still owns via `ClearThreads`. Removed the now-unused `FRefCount` field - `TSimpleThreadPool.Destroy` is now safe to call on a partially constructed pool: it skips `WaitForAll` and the queue/thread cleanup when the relevant fields are `nil`, instead of dereferencing them - Example projects (all except `Starter`) only had the `src` search path in their `Release` build mode, so `lazbuild` (which builds the `Default` mode) could not find the units. Added the search path to each project's global compiler options +- Tests `Test04_CreateDestroy` and `Test11_ThreadCount` hardcoded thread-count expectations that only held on multi-core machines; on low-core CI runners the enforced minimum of 4 threads made them fail. They now assert against the actual `Max(Min(requested, 2Γ—ProcessorCount), 4)` formula +- `Test08_QueueFullBehavior` assumed a single worker drains the queue, but the pool enforces a 4-thread minimum, so on Linux the queue drained before it could overflow. It now bursts `ThreadCount + QueueSize + 1` long-running tasks, which overflows a fixed queue deterministically regardless of core count or scheduling ## [0.5.0] - 2026-04-13 diff --git a/docs/release-notes-v0.6.0.md b/docs/release-notes-v0.6.0.md index 253e084..d3294f8 100644 --- a/docs/release-notes-v0.6.0.md +++ b/docs/release-notes-v0.6.0.md @@ -65,6 +65,12 @@ example builds) β€” all were masked on Windows: `Starter` had the `src` search path only in its `Release` build mode, so `lazbuild` (which builds the `Default` mode) failed with "Can't find unit". The search path is now in each project's global compiler options. +- **Core-count-dependent tests.** A few tests hardcoded thread-count + expectations and queue-draining timing that only held on the developer's + multi-core machine; on low-core CI runners the enforced 4-thread minimum + made them fail. They now assert against the actual thread-count formula and + overflow the queue with a burst sized off the real thread count, so they're + deterministic on any number of cores. ## Upgrade notes diff --git a/tests/ThreadPool.ProducerConsumer.Tests.pas b/tests/ThreadPool.ProducerConsumer.Tests.pas index 1a2c8b0..4f32044 100644 --- a/tests/ThreadPool.ProducerConsumer.Tests.pas +++ b/tests/ThreadPool.ProducerConsumer.Tests.pas @@ -280,41 +280,42 @@ procedure TTestProducerConsumerThreadPool.Test07_ParallelExecution; procedure TTestProducerConsumerThreadPool.Test08_QueueFullBehavior; const QUEUE_SIZE = 2; - THREAD_COUNT = 1; var TestPool: TProducerConsumerThreadPool; - I: Integer; + I, BurstSize: Integer; ExceptionRaised: Boolean; ExceptionMessage: string; Config: TBackpressureConfig; begin LogTest('Test08_QueueFullBehavior starting...'); - - // Create a separate pool for this test - TestPool := TProducerConsumerThreadPool.Create(THREAD_COUNT, QUEUE_SIZE); + + // The base pool enforces a minimum of 4 worker threads regardless of the + // requested count, so the test cannot rely on a "single slow worker". With + // N workers and a queue of QUEUE_SIZE, the system can hold at most + // N + QUEUE_SIZE long-running tasks before the queue is full. Queue one more + // than that, fast, with MaxAttempts=1 and no backpressure delay, and the + // overflow MUST raise EQueueFullException β€” independent of scheduling or the + // host's core count. + TestPool := TProducerConsumerThreadPool.Create(1, QUEUE_SIZE); try - // Configure aggressive backpressure Config := TestPool.WorkQueue.BackpressureConfig; Config.MaxAttempts := 1; Config.LowLoadDelay := 0; Config.MediumLoadDelay := 0; Config.HighLoadDelay := 0; TestPool.WorkQueue.BackpressureConfig := Config; - - ExceptionRaised := False; - // Fill the queue with long-running tasks so the worker cannot drain the - // queue between these enqueues and the third attempt below. - // SlowTask (250ms) is too short β€” the single worker may dequeue item 1 - // before item 3 is attempted, defeating the "full queue" condition. - LogTest('test08: Queueing first task'); - TestPool.Queue(@LongTask); - LogTest('test08: Queueing second task'); - TestPool.Queue(@LongTask); + ExceptionRaised := False; + // ThreadCount reflects the enforced minimum; +QUEUE_SIZE in-flight slots, + // +1 to guarantee overflow. + BurstSize := TestPool.ThreadCount + QUEUE_SIZE + 1; + LogTest(Format('test08: bursting %d LongTasks (threads=%d, queue=%d)', + [BurstSize, TestPool.ThreadCount, QUEUE_SIZE])); try - TestPool.Queue(@LongTask); - LogTest('ERROR: Queue succeeded when it should have been full'); + for I := 1 to BurstSize do + TestPool.Queue(@LongTask); + LogTest('ERROR: Queue never reported full'); except on E: EQueueFullException do begin @@ -326,17 +327,14 @@ procedure TTestProducerConsumerThreadPool.Test08_QueueFullBehavior; LogTest('Caught unexpected exception type: ' + E.ClassName + ': ' + E.Message); end; - // Wait for queued tasks to complete - TestPool.WaitForAll; - AssertTrue('Should have raised an exception', ExceptionRaised); - AssertTrue('Exception should be EQueueFullException', + AssertTrue('Exception should be EQueueFullException', ExceptionMessage.Contains('Queue is full')); finally TestPool.WaitForAll; TestPool.Free; end; - + LogTest('Test08_QueueFullBehavior finished'); end; diff --git a/tests/threadpool.simple.tests.pas b/tests/threadpool.simple.tests.pas index b2b4150..4b5be15 100644 --- a/tests/threadpool.simple.tests.pas +++ b/tests/threadpool.simple.tests.pas @@ -5,8 +5,8 @@ interface uses - Classes, SysUtils, fpcunit, testregistry, - ThreadPool.Types, ThreadPool.Simple, syncobjs, DateUtils; + Classes, SysUtils, fpcunit, testregistry, + ThreadPool.Types, ThreadPool.Simple, syncobjs, DateUtils, Math; type TTestException = class(Exception); @@ -205,19 +205,22 @@ procedure TSimpleThreadPoolTests.Test04_CreateDestroy; Pool: TSimpleThreadPool; const MinThreads = 4; - TestThreads = 6; + // Request well above both the minimum and any plausible 2x-core cap so the + // result is governed by the cap, independent of the host's core count. + TestThreads = 64; begin Pool := TSimpleThreadPool.Create(TestThreads); try - AssertEquals('Thread count should match when above minimum', - TestThreads, Pool.ThreadCount); + AssertEquals('Thread count should be capped at 2x processor count', + Max(Min(TestThreads, TThread.ProcessorCount * 2), MinThreads), + Pool.ThreadCount); finally Pool.Free; end; Pool := TSimpleThreadPool.Create(2); try - AssertEquals('Thread count should be adjusted to minimum', + AssertEquals('Thread count should be adjusted to minimum', MinThreads, Pool.ThreadCount); finally Pool.Free; @@ -285,23 +288,26 @@ procedure TSimpleThreadPoolTests.Test10_StressTest; end; procedure TSimpleThreadPoolTests.Test11_ThreadCount; +const + MinThreads = 4; var Pool: TSimpleThreadPool; begin - // Test maximum thread count + // Test maximum thread count. The result is the 2x-core cap, but never below + // the enforced minimum of 4 (which matters on low-core CI runners). Pool := TSimpleThreadPool.Create(TThread.ProcessorCount * 3); try - AssertEquals('Thread count should be limited to 2x processor count', - TThread.ProcessorCount * 2, Pool.ThreadCount); + AssertEquals('Thread count should be limited to 2x processor count (min 4)', + Max(TThread.ProcessorCount * 2, MinThreads), Pool.ThreadCount); finally Pool.Free; end; - - // Test zero thread count + + // Test zero thread count: defaults to processor count, again floored at 4. Pool := TSimpleThreadPool.Create(0); try - AssertEquals('Thread count should default to processor count', - TThread.ProcessorCount, Pool.ThreadCount); + AssertEquals('Thread count should default to processor count (min 4)', + Max(TThread.ProcessorCount, MinThreads), Pool.ThreadCount); finally Pool.Free; end;