Skip to content

Commit 2cd4cdb

Browse files
fix: scope partition maintenance to target table and lock create path (Cacti#271)
Refs Cacti#271 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
1 parent 95eba58 commit 2cd4cdb

File tree

2 files changed

+326
-40
lines changed

2 files changed

+326
-40
lines changed

functions.php

Lines changed: 137 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -187,86 +187,184 @@ function syslog_partition_manage() {
187187
}
188188

189189
/**
190-
* This function will create a new partition for the specified table.
190+
* Validate tables that support partition maintenance.
191+
*
192+
* Any value added to the allowlist MUST match ^[a-z_]+$ so it is safe
193+
* for identifier interpolation in DDL statements (MySQL does not support
194+
* parameter binding for identifiers).
195+
*/
196+
function syslog_partition_table_allowed($table) {
197+
if (!in_array($table, array('syslog', 'syslog_removed'), true)) {
198+
return false;
199+
}
200+
201+
/* Defense-in-depth: reject values unsafe for identifier interpolation. */
202+
if (!preg_match('/^[a-z_]+$/', $table)) {
203+
return false;
204+
}
205+
206+
return true;
207+
}
208+
209+
/**
210+
* Create a new partition for the specified table.
211+
*
212+
* @return bool true on success, false on lock failure or disallowed table.
191213
*/
192214
function syslog_partition_create($table) {
193215
global $syslogdb_default;
194216

195-
/* determine the format of the table name */
196-
$time = time();
197-
$cformat = 'd' . date('Ymd', $time);
198-
$lnow = date('Y-m-d', $time+86400);
217+
if (!syslog_partition_table_allowed($table)) {
218+
return false;
219+
}
199220

200-
$exists = syslog_db_fetch_row("SELECT *
201-
FROM `information_schema`.`partitions`
202-
WHERE table_schema='" . $syslogdb_default . "'
203-
AND partition_name='" . $cformat . "'
204-
AND table_name='syslog'
205-
ORDER BY partition_ordinal_position");
221+
/* Hash to guarantee the lock name stays within MySQL's 64-byte limit. */
222+
$lock_name = substr(hash('sha256', $syslogdb_default . '.syslog_partition_create.' . $table), 0, 60);
206223

207-
if (!cacti_sizeof($exists)) {
208-
cacti_log("SYSLOG: Creating new partition '$cformat'", false, 'SYSTEM');
224+
/*
225+
* 10-second timeout is sufficient: partition maintenance runs once per
226+
* poller cycle (typically 5 minutes), so sustained contention is not
227+
* expected. A failure is logged so monitoring can detect repeated misses.
228+
*/
229+
$locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name));
209230

210-
syslog_debug("Creating new partition '$cformat'");
231+
if ($locked === null) {
232+
/* NULL means the GET_LOCK call itself failed, not just contention. */
233+
cacti_log("SYSLOG: GET_LOCK call failed for partition create on '$table'", false, 'SYSTEM');
234+
return false;
235+
}
211236

212-
syslog_db_execute("ALTER TABLE `" . $syslogdb_default . "`.`$table` REORGANIZE PARTITION dMaxValue INTO (
213-
PARTITION $cformat VALUES LESS THAN (TO_DAYS('$lnow')),
214-
PARTITION dMaxValue VALUES LESS THAN MAXVALUE)");
237+
if ((int)$locked !== 1) {
238+
cacti_log("SYSLOG: Unable to acquire partition create lock for '$table'", false, 'SYSTEM');
239+
return false;
215240
}
241+
242+
try {
243+
/* determine the format of the table name */
244+
$time = time();
245+
$cformat = 'd' . date('Ymd', $time);
246+
$lnow = date('Y-m-d', $time+86400);
247+
248+
$exists = syslog_db_fetch_row_prepared("SELECT *
249+
FROM `information_schema`.`partitions`
250+
WHERE table_schema = ?
251+
AND partition_name = ?
252+
AND table_name = ?
253+
ORDER BY partition_ordinal_position",
254+
array($syslogdb_default, $cformat, $table));
255+
256+
if (!cacti_sizeof($exists)) {
257+
cacti_log("SYSLOG: Creating new partition '$cformat'", false, 'SYSTEM');
258+
259+
syslog_debug("Creating new partition '$cformat'");
260+
261+
/*
262+
* MySQL does not support parameter binding for DDL identifiers
263+
* or partition definitions. $table is safe because it passed
264+
* syslog_partition_table_allowed() (two-value allowlist plus
265+
* regex guard). $cformat and $lnow derive from date() and
266+
* contain only digits, hyphens, and the letter 'd'.
267+
*/
268+
syslog_db_execute("ALTER TABLE `" . $syslogdb_default . "`.`$table` REORGANIZE PARTITION dMaxValue INTO (
269+
PARTITION $cformat VALUES LESS THAN (TO_DAYS('$lnow')),
270+
PARTITION dMaxValue VALUES LESS THAN MAXVALUE)");
271+
}
272+
} finally {
273+
syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name));
274+
}
275+
276+
return true;
216277
}
217278

218279
/**
219-
* This function will remove all old partitions for the specified table.
280+
* Remove old partitions for the specified table.
220281
*/
221282
function syslog_partition_remove($table) {
222283
global $syslogdb_default;
223284

285+
if (!syslog_partition_table_allowed($table)) {
286+
cacti_log("SYSLOG: partition_remove called with disallowed table '$table'", false, 'SYSTEM');
287+
return 0;
288+
}
289+
290+
$lock_name = substr(hash('sha256', $syslogdb_default . '.syslog_partition_remove.' . $table), 0, 60);
291+
292+
$locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name));
293+
294+
if ($locked === null) {
295+
cacti_log("SYSLOG: GET_LOCK call failed for partition remove on '$table'", false, 'SYSTEM');
296+
return 0;
297+
}
298+
299+
if ((int)$locked !== 1) {
300+
cacti_log("SYSLOG: Unable to acquire partition remove lock for '$table'", false, 'SYSTEM');
301+
return 0;
302+
}
303+
224304
$syslog_deleted = 0;
225-
$number_of_partitions = syslog_db_fetch_assoc("SELECT *
226-
FROM `information_schema`.`partitions`
227-
WHERE table_schema='" . $syslogdb_default . "' AND table_name='syslog'
228-
ORDER BY partition_ordinal_position");
229305

230-
$days = read_config_option('syslog_retention');
306+
try {
307+
$number_of_partitions = syslog_db_fetch_assoc_prepared("SELECT *
308+
FROM `information_schema`.`partitions`
309+
WHERE table_schema = ? AND table_name = ?
310+
ORDER BY partition_ordinal_position",
311+
array($syslogdb_default, $table));
312+
313+
$days = read_config_option('syslog_retention');
231314

232-
syslog_debug("There are currently '" . sizeof($number_of_partitions) . "' Syslog Partitions, We will keep '$days' of them.");
315+
syslog_debug("There are currently '" . sizeof($number_of_partitions) . "' Syslog Partitions, We will keep '$days' of them.");
233316

234-
if ($days > 0) {
235-
$user_partitions = sizeof($number_of_partitions) - 1;
236-
if ($user_partitions >= $days) {
237-
$i = 0;
238-
while ($user_partitions > $days) {
239-
$oldest = $number_of_partitions[$i];
317+
if ($days > 0) {
318+
$user_partitions = sizeof($number_of_partitions) - 1;
319+
if ($user_partitions >= $days) {
320+
$i = 0;
321+
while ($user_partitions > $days) {
322+
$oldest = $number_of_partitions[$i];
240323

241-
cacti_log("SYSLOG: Removing old partition '" . $oldest['PARTITION_NAME'] . "'", false, 'SYSTEM');
324+
cacti_log("SYSLOG: Removing old partition '" . $oldest['PARTITION_NAME'] . "'", false, 'SYSTEM');
242325

243-
syslog_debug("Removing partition '" . $oldest['PARTITION_NAME'] . "'");
326+
syslog_debug("Removing partition '" . $oldest['PARTITION_NAME'] . "'");
244327

245-
syslog_db_execute("ALTER TABLE `" . $syslogdb_default . "`.`$table` DROP PARTITION " . $oldest['PARTITION_NAME']);
328+
syslog_db_execute("ALTER TABLE `" . $syslogdb_default . "`.`$table` DROP PARTITION " . $oldest['PARTITION_NAME']);
246329

247-
$i++;
248-
$user_partitions--;
249-
$syslog_deleted++;
330+
$i++;
331+
$user_partitions--;
332+
$syslog_deleted++;
333+
}
250334
}
251335
}
336+
} finally {
337+
syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name));
252338
}
253339

254340
return $syslog_deleted;
255341
}
256342

343+
/*
344+
* syslog_partition_check is a read-only SELECT against information_schema.
345+
* It does not execute DDL, so it does not need the named lock that
346+
* syslog_partition_create and syslog_partition_remove acquire. External
347+
* serialization is provided by the poller cycle calling
348+
* syslog_partition_manage().
349+
*/
257350
function syslog_partition_check($table) {
258351
global $syslogdb_default;
259352

353+
if (!syslog_partition_table_allowed($table)) {
354+
return false;
355+
}
356+
260357
if (defined('SYSLOG_CONFIG')) {
261358
include(SYSLOG_CONFIG);
262359
}
263360

264361
/* find date of last partition */
265-
$last_part = syslog_db_fetch_cell("SELECT PARTITION_NAME
362+
$last_part = syslog_db_fetch_cell_prepared("SELECT PARTITION_NAME
266363
FROM `information_schema`.`partitions`
267-
WHERE table_schema='" . $syslogdb_default . "' AND table_name='syslog'
364+
WHERE table_schema = ? AND table_name = ?
268365
ORDER BY partition_ordinal_position DESC
269-
LIMIT 1,1;");
366+
LIMIT 1,1",
367+
array($syslogdb_default, $table));
270368

271369
$lformat = str_replace('d', '', $last_part);
272370
$cformat = date('Ymd');
@@ -2421,4 +2519,3 @@ function alert_replace_variables($alert, $results, $hostname = '') {
24212519

24222520
return $command;
24232521
}
2424-

0 commit comments

Comments
 (0)