mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-28 15:47:39 +09:00
172 lines
7.4 KiB
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],
|
|
]);
|
|
});
|