Files
cacti/tests/Unit/DataCollection/Poller/PollerOutputAcknowledgementTest.php
TheWitnessandCopilot 76a916cf03 fix(boost): never re-stage poller output when boost_redirect is on (#8374)
* fix(boost): never re-stage poller output when boost_redirect is on

#8332 gated the boost_redirect bypass in boost_poller_on_demand() on the
caller not passing $staged. process_poller_output() always passes it, so
the bypass never fired and the main poller re-inserted every row spine or
cmd.php had already written to poller_output_boost, flooding the log with
"Boost staging ignored N of N duplicate sample keys" warnings.

When boost_redirect is enabled the poller now never inserts into
poller_output_boost and reports the rows as staged so they are cleared
from poller_output. poller_recovery.php writes recovered remote rows via
boost_flush_output_batch() directly and is unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

* chore(changelog): use changelog.d fragment for #8374

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

* fix(boost): log failed poller_output_boost staging as an ERROR

boost_flush_output_batch() returned false silently when the database did
not acknowledge a chunk. cmd.php's boost_redirect staging ignores that
return, so a failed collector-side write left no trace. Log a BOOST ERROR
with the sample count from the helper so every caller's failure is visible.

Adds in-process coverage for the failure log and for boost_poller_on_demand()
never re-staging rows when boost_redirect is on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: TheWitness <thewitness@cacti.net>

---------

Signed-off-by: TheWitness <thewitness@cacti.net>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-14 21:13:43 -04:00

95 lines
4.8 KiB
PHP

<?php
// Copyright (C) 2026 The Cacti Group. Licensed under GPL-2.0-or-later.
require_once dirname(__DIR__, 3) . '/Helpers/IsolatedProbe.php';
function poller_output_probe(string $scenario) : array {
return cacti_test_isolated_probe(dirname(__DIR__, 3) . '/fixtures/PollerOutputProbe.php', [$scenario]);
}
test('completed timestamps do not delete incomplete timestamps for the same source', function () {
$r = poller_output_probe('partial');
expect($r['first'])->toBe(1)->and($r['remaining'])->toBe(1);
expect($r['rows'][0]['unix_time'])->toBe(1700000060);
});
test('samples arriving after the fetch are not acknowledged by the previous update', function () {
$r = poller_output_probe('arrival');
expect($r['first'])->toBe(1)->and($r['remaining'])->toBe(1);
});
test('failed RRDtool writes retain their queue rows', function () {
$r = poller_output_probe('failed');
expect($r['first'])->toBe(0)->and($r['remaining'])->toBe(1)->and($r['delete_calls'])->toBe(0);
});
test('one failing file stops before later timestamps while other files can succeed', function () {
$r = poller_output_probe('mixed');
expect($r['first'])->toBe(1)->and($r['remaining'])->toBe(2)->and($r['rrd_calls'])->toBe(2);
});
test('template mappings expand into individual RRD fields', function () {
$r = poller_output_probe('fanout');
expect($r['first'])->toBe(1)->and($r['remaining'])->toBe(0);
expect($r['commands'][0])->toContain('--template in:out')->toContain('1700000000:42:42');
});
test('mapping expansion applies orphan exclusions to each field', function () {
$r = poller_output_probe('fanout-orphan');
expect($r['first'])->toBe(1)->and($r['commands'][0])->toContain('--template in ')->not->toContain('in:out');
});
test('batch boundaries defer a whole timestamp group', function () {
$r = poller_output_probe('boundary');
expect($r['first'])->toBe(39999)->and($r['second'])->toBe(1)->and($r['remaining'])->toBe(0)->and($r['queries'])->toBe(2);
});
test('large incomplete backlogs return without recursion and advance past the first batch', function () {
$r = poller_output_probe('backlog');
expect($r['first'])->toBe(0)->and($r['second'])->toBe(0)->and($r['remaining'])->toBe(40001)->and($r['queries'])->toBe(2);
});
test('successful Boost staging acknowledges queue rows without inline RRD writes', function () {
$r = poller_output_probe('boost-success');
expect($r['remaining'])->toBe(0)->and($r['boost_calls'])->toBe(1)->and($r['rrd_calls'])->toBe(0);
});
test('redirected polling never re-inserts collector-staged samples into poller_output_boost', function () {
$r = poller_output_probe('boost-redirect');
expect($r['remaining'])->toBe(0)->and($r['boost_calls'])->toBe(0)->and($r['rrd_calls'])->toBe(0);
});
test('failed Boost staging and failed RRD fallback retain queue rows', function () {
$r = poller_output_probe('boost-failure');
expect($r['remaining'])->toBe(1)->and($r['boost_calls'])->toBe(1)->and($r['rrd_calls'])->toBe(1);
});
test('failed acknowledgement leaves stored samples available for a safe retry', function () {
$r = poller_output_probe('delete-failure');
expect($r['first'])->toBe(1)->and($r['remaining'])->toBe(1);
});
test('failed template metadata reads retain samples and retry on the next pass', function () {
$r = poller_output_probe('metadata');
expect($r['first'])->toBe(0)->and($r['second'])->toBe(1)->and($r['mapping_queries'])->toBe(2)->and($r['remaining'])->toBe(0);
});
test('later timestamps cannot advance an RRD past an incomplete earlier timestamp', function () {
$r = poller_output_probe('early-partial');
expect($r['first'])->toBe(0)->and($r['remaining'])->toBe(3)->and($r['rrd_calls'])->toBe(0);
});
test('incomplete timestamps block later timestamps across batch boundaries', function () {
$r = poller_output_probe('boundary-partial');
expect($r['first'])->toBe(39999)->and($r['second'])->toBe(0)->and($r['remaining'])->toBe(3);
});
test('failed timestamps block later timestamps across batch boundaries', function () {
$r = poller_output_probe('boundary-failure');
expect($r['first'])->toBe(39999)->and($r['second'])->toBe(0)->and($r['remaining'])->toBe(2);
});
test('a failed delete never leaves only part of a stored timestamp group queued', function () {
$r = poller_output_probe('delete-group');
expect($r['first'])->toBe(1001)->and($r['remaining'])->toBe(1);
expect($r['rows'][0]['local_data_id'])->toBe(1001);
});
test('a failed sample succeeds on a later pass without being lost or duplicated', function () {
$r = poller_output_probe('retry-failed');
expect($r['first'])->toBe(0)->and($r['second'])->toBe(1)->and($r['remaining'])->toBe(0);
});
test('failed source-specific metadata reads retry without losing samples', function (string $scenario) {
$r = poller_output_probe($scenario);
expect($r['first'])->toBe(0)->and($r['second'])->toBe(1)->and($r['remaining'])->toBe(0)
->and($r['source_queries'])->toBe(2);
})->with(['manual-query-failure', 'orphan-query-failure']);