fix(poller): evaluate the right thresholds and clear only what was checked - #803
Open
somethingwithproof wants to merge 4 commits into
Open
fix(poller): evaluate the right thresholds and clear only what was checked#803somethingwithproof wants to merge 4 commits into
somethingwithproof wants to merge 4 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes three scheduling/selection defects in the Thold poller pipeline (includes/polling.php) so the poller evaluates the intended threshold set (correct SQL precedence), batches daemon inserts correctly, and only clears the dirty flag (tcheck) for thresholds that were actually checked. It also adds a PHPUnit-based unit test harness plus CI workflow to prevent regressions.
Changes:
- Fix SQL
AND/ORprecedence for poller 1 remote-storage threshold selection. - Fix daemon batching by using the correct
array_chunk()chunk size (50). - Clear
tcheckonly for the evaluated threshold IDs, and add unit tests + CI (including changed-lines coverage enforcement) to guard these behaviors.
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| includes/polling.php | Fixes SQL precedence, daemon chunking, and makes tcheck clearing ID-scoped. |
| tests/Unit/PollerSchedulingTest.php | Adds unit tests covering poller scheduling behavior and structural guards. |
| tests/TestCase.php | Introduces a base PHPUnit TestCase with common reset/setup helpers. |
| tests/Helpers/CactiStubs.php | Adds programmable stubs/recording for Cacti global helper functions. |
| tests/fixtures/optional-core-functions.php | Provides optional Cacti core functions for tests that need them. |
| tests/fixtures/cacti-lib/variables.php | Fixture file to satisfy runtime include_once() expectations in plugin code. |
| tests/docker/Dockerfile | Adds a reproducible local/CI unit-test runner image (PHP 8.1 + pcov). |
| tests/docker/docker-compose.yml | Provides a local docker compose run mirror of the CI unit job. |
| tests/bootstrap-unit.php | Adds PHPUnit bootstrap that stubs Cacti framework functions and sets up globals. |
| tests/bin/patch-coverage.php | Adds changed-lines coverage gate helper for CI PR enforcement. |
| phpunit.xml | Adds PHPUnit configuration and scopes coverage to relevant plugin files. |
| composer.json | Adds dev dependencies and scripts for linting/testing (including docker runner). |
| .gitignore | Ignores vendor, coverage outputs, and PHPUnit caches for the new test tooling. |
| .github/workflows/php-unit-tests.yml | Adds GitHub Actions workflow to run lint + unit tests + coverage gates in Docker. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+32
to
+45
| /* | ||
| * base_path has to point at the Cacti root two levels above this plugin: | ||
| * thold_functions.php builds include paths from it at runtime. | ||
| */ | ||
| $GLOBALS['config'] = [ | ||
| 'base_path' => dirname(dirname(dirname(__DIR__))), | ||
| 'url_path' => '/cacti/', | ||
| 'cacti_version' => '1.2.31', | ||
| 'cacti_server_os' => 'unix', | ||
| ]; | ||
|
|
||
| // thold_expand_string() include_once()s library_path/variables.php at call time. | ||
| $GLOBALS['config']['library_path'] = __DIR__ . '/fixtures/cacti-lib'; | ||
|
|
bmfmancini
previously approved these changes
Aug 17, 2026
…ecked Three defects in includes/polling.php. The poller_id alternation had no parentheses, and AND binds tighter than OR, so the clause read as (... AND h.poller_id = 1) OR (h.poller_id IS NULL AND tcheck = 1 AND status = 3) The first branch carries neither the dirty-flag nor the device-status filter, so on poller 1 every enabled threshold was re-evaluated on every run against whatever lastread happened to be there, including for devices that were down. The second branch cannot match, since the LEFT JOIN gives a NULL host a NULL status. Confirmed against MariaDB on seeded rows: the clause returned a threshold with no new data and one on a down device; parenthesised, it returns neither. The dirty flag was then cleared by poller, or across the whole table, rather than for the identifiers just evaluated. Any reading arriving between the select and that update was discarded unchecked. It now clears exactly what was visited, which also collapses three statements into one; the select already scopes to this poller, so the identifiers carry that scoping with them. array_chunk() takes the size of each chunk, not how many to make, so 500 readings became 50 chunks of 10 rather than 10 of 50, and each chunk costs a round trip. Refs Cacti#783 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The batch size only takes effect when the daemon is enabled, which none of the other cases exercise, so the changed line went uncovered. The stub can now answer a matched query with a callable, which lets the threshold lookup return just the batch it was asked about. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
somethingwithproof
force-pushed
the
fix/poller-scheduling
branch
from
August 17, 2026 21:19
cd056e1 to
d8f5702
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #783. Three defects in
includes/polling.php.The poller_id alternation was unparenthesised
ANDbinds tighter thanOR, so this reads asThe first branch carries neither the dirty-flag nor the device-status filter. The second cannot match at all, because the
LEFT JOINgives a NULL host a NULL status.So on poller 1, every enabled threshold is re-evaluated on every run against whatever
lastreadhappens to be sitting there — including for devices that are down. Fail counters advance on stale readings, and a threshold that breached once keeps counting until it crosses its trigger and alerts on data that may be hours old.Confirmed against MariaDB 10.6 rather than by reading it. Three thresholds: one with new data on an up device, one with no new data, one on a down device.
The
poller_id != 1and non-remote variants of the same query are already parenthesised correctly, which is what makes this look like a typo rather than intent.The dirty flag was cleared for more than was evaluated
After the loop,
tcheckwas cleared by poller, or across the whole table:Any
thold_poller_output()write landing between the select and that update was discarded without ever being evaluated. With multiple pollers or a remote collector, thresholds whose data arrives late in the cycle are simply skipped.It now clears exactly the identifiers that were visited. That also collapses the three branches into one statement: the select already scopes to this poller, so the identifiers carry that scoping with them.
array_chunk was given a count where a size belongs
The second argument is the size of each chunk. With 500 readings this produced 50 chunks of 10 where 10 chunks of 50 was intended; with 25 readings, 25 chunks of one. Each chunk costs a select plus a bulk insert.
Tests
Six tests. The dirty-flag behaviour is checked by driving
thold_check_all_thresholds()and asserting on the statement it issues — that it clears the identifiers it visited, that it issues nothing when it visited nothing, and that no statement clears table-wide.The precedence and chunking fixes are guarded structurally, and the test says so in as many words. Proving a SQL precedence bug needs a database to run the query against, which the unit workflow has none of; the evidence above is the proof, and the guard is there so the parentheses cannot quietly go missing again.
Coverage is now scoped to
includes/polling.phpas well asthold_functions.php, since this is the first change to it.