Files
cacti/tests/integration/UpdateDataQueryCacheBatchingIntegrationTest.php

172 lines
7.4 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* Runtime integration coverage for issue #7531 (update_data_query_cache() commits
* poller-cache writes per-row instead of batching).
*
* poller_update_poller_cache_from_buffer() is the function whose call count
* the fix reduces: each invocation does its own "present = 0" UPDATE,
* INSERT ... ON DUPLICATE KEY UPDATE, and stale-row DELETE. This test calls
* the real function (not a spy) against a real (sqlite-backed) connection
* and counts actual SQL statements via lib/database.php's
* $database_total_queries counter -- the same mechanism
* tests/Unit/Database/DbColumnTypeCacheTest.php-style tests rely on implicitly
* through db_execute_prepared() -- proving the O(n) -> O(1) round-trip
* reduction the issue describes, not just asserting it algebraically.
*
* See UpdateDataQueryCacheBatchingStructureTest.php for the companion test
* that proves update_data_query_cache()'s actual loop body calls things in
* this per-row-vs-batched shape in the first place.
*/
/* poller_update_poller_cache_from_buffer() records a last-changed timestamp on
every flush. That used to be stubbed out behind function_exists(), but the
stub only applied when nothing had already loaded lib/functions.php, and the
unit suite always has by the time this file runs. The real function then ran
instead, dereferenced CACTI_PATH_LIBRARY and fataled, and its extra query
changed the counts below. Load the real one deliberately and give it what it
needs, so this file behaves the same whatever ran before it. */
/* Guarded defines rather than require_once of include/global_constants.php:
that file defines unconditionally, and sibling tests (Helpers/UnitStubs.php,
CmdPhpPollerItemColumnCollisionIntegrationTest) already define a subset of
the same names, so requiring it here emits "already defined" warnings on any
ordering where one of those ran first. Guarded defines emit none either way. */
foreach ([
'CACTI_WEB' => false,
'POLLER_ID' => 1,
'CACTI_PATH_BASE' => dirname(__DIR__, 2),
'CACTI_PATH_LIBRARY' => dirname(__DIR__, 2) . '/lib',
'OPTIONS_CLI' => 'config_options_array',
'OPTIONS_WEB' => 'sess_config_array',
] as $name => $value) {
if (!defined($name)) {
define($name, $value);
} elseif (constant($name) !== $value) {
throw new RuntimeException("$name has an incompatible value for the batching integration test");
}
}
// An existing suite bootstrap may point logging at another valid directory.
if (!defined('CACTI_PATH_LOG')) {
define('CACTI_PATH_LOG', sys_get_temp_dir());
}
require_once dirname(__DIR__, 2) . '/lib/functions.php';
/* after lib/functions.php: UnitStubs guards with function_exists(), so loading
it first would make the real definitions a redeclaration fatal */
require_once dirname(__DIR__) . '/Helpers/UnitStubs.php';
require_once dirname(__DIR__) . '/Helpers/FakeMySQLPDO.php';
require_once dirname(__DIR__, 2) . '/include/vendor/autoload.php';
require_once dirname(__DIR__, 2) . '/lib/database.php';
require_once dirname(__DIR__, 2) . '/lib/utility.php';
function batching_seed_poller_item_table() : FakeMySQLPDO {
$conn = new FakeMySQLPDO();
// set_config_option() writes the last-changed timestamp here
$conn->exec('CREATE TABLE settings (name TEXT PRIMARY KEY, value TEXT)');
$conn->exec('CREATE TABLE poller_item (
local_data_id INTEGER PRIMARY KEY, poller_id INTEGER, host_id INTEGER, action INTEGER,
hostname TEXT, snmp_community TEXT, snmp_version INTEGER, snmp_timeout INTEGER,
snmp_retries INTEGER, snmp_username TEXT, snmp_password TEXT, snmp_auth_protocol TEXT,
snmp_priv_passphrase TEXT, snmp_priv_protocol TEXT, snmp_context TEXT, snmp_engine_id TEXT,
snmp_port INTEGER, rrd_name TEXT, rrd_path TEXT, rrd_num INTEGER, rrd_step INTEGER,
rrd_next_step INTEGER, arg1 TEXT, arg2 TEXT, arg3 TEXT, present INTEGER
)');
return $conn;
}
beforeEach(function () {
global $database_sessions, $database_hostname, $database_port, $database_default;
$this->db_globals = [$database_sessions, $database_hostname, $database_port, $database_default];
});
// Put the default db_* connection back. Left in place, the fake handle answers
// every later read_config_option() in the run and throws on Cacti's MySQL SQL,
// aborting the suite.
afterEach(function () {
global $database_sessions, $database_hostname, $database_port, $database_default;
[$database_sessions, $database_hostname, $database_port, $database_default] = $this->db_globals;
});
function batching_wire_default_connection(PDO $conn) : void {
$GLOBALS['database_hostname'] = 'unittest';
$GLOBALS['database_port'] = 0;
$GLOBALS['database_default'] = 'unittest';
$GLOBALS['config'] = $GLOBALS['config'] ?? [];
$GLOBALS['database_total_queries'] = 0;
$GLOBALS['database_sessions'] = ['unittest:0:unittest' => $conn];
}
function batching_tuple(int $id) : string {
// matches poller_update_poller_cache_from_buffer()'s fixed 26-column
// INSERT list: local_data_id, poller_id, host_id, action, hostname,
// snmp_community, snmp_version, snmp_timeout, snmp_retries,
// snmp_username, snmp_password, snmp_auth_protocol, snmp_priv_passphrase,
// snmp_priv_protocol, snmp_context, snmp_engine_id, snmp_port, rrd_name,
// rrd_path, rrd_num, rrd_step, rrd_next_step, arg1, arg2, arg3, present
return "($id, 1, 10, 0, 'host', 'community', 2, 500, 3, '', '', '', '', '', '', '', 161, 'ds$id', '', 1, 300, 0, '', '', '', 1)";
}
test('batching 3 changed rows into a single poller_update_poller_cache_from_buffer() call issues fewer SQL statements than 3 separate per-row calls', function () {
// old behavior: update_poller_cache($data_source, true) flushed
// immediately, once per changed row
$conn = batching_seed_poller_item_table();
batching_wire_default_connection($conn);
foreach ([1, 2, 3] as $id) {
$idsArg = [$id];
$itemsArg = [batching_tuple($id)];
poller_update_poller_cache_from_buffer($idsArg, $itemsArg, 1);
}
$perRowQueries = $GLOBALS['database_total_queries'];
// new behavior: accumulate across the loop, flush once
$conn = batching_seed_poller_item_table();
batching_wire_default_connection($conn);
$ids = [1, 2, 3];
$tuples = array_map('batching_tuple', $ids);
poller_update_poller_cache_from_buffer($ids, $tuples, 1);
$batchedQueries = $GLOBALS['database_total_queries'];
expect($batchedQueries)->toBeLessThan($perRowQueries);
/* each flush does a present=0 UPDATE, an INSERT, a stale-row DELETE and the
set_config_option() last-changed write, so 3 separate flushes cost 12
statements and 1 batched flush costs 4 */
expect($perRowQueries)->toBe(12)
->and($batchedQueries)->toBe(4);
});
test('the batched flush leaves the same end-state as 3 per-row flushes would', function () {
$conn = batching_seed_poller_item_table();
batching_wire_default_connection($conn);
$ids = [1, 2, 3];
$tuples = array_map('batching_tuple', $ids);
poller_update_poller_cache_from_buffer($ids, $tuples, 1);
$rows = $conn->query('SELECT local_data_id, present FROM poller_item ORDER BY local_data_id')->fetchAll(PDO::FETCH_ASSOC);
expect($rows)->toBe([
['local_data_id' => 1, 'present' => 1],
['local_data_id' => 2, 'present' => 1],
['local_data_id' => 3, 'present' => 1],
]);
});