mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 19:47:06 +09:00
* 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>
95 lines
4.8 KiB
PHP
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']);
|