Files
zoneminder/db/views.sql
Isaac Connor 1f84c57ccc fix: use READ COMMITTED in Refresh_Summaries_SWR to avoid Events deadlock
INSERT INTO Event_Summaries_New SELECT * FROM VIEW_Event_Summaries took
shared next-key locks on the source Events table (required for STATEMENT
binlog correctness under the default REPEATABLE READ).  Concurrent DELETE
FROM Events deadlocked against those S locks.

Switch the procedure to READ COMMITTED so the source-side SELECT runs as
a consistent snapshot read with no row locks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:29:35 -04:00

195 lines
6.5 KiB
SQL

DELIMITER //
-- This index covers MonitorId grouping, StartDateTime ranges,
-- Archived status filtering, and includes DiskSpace for summation.
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = DATABASE()
AND table_name = 'Events'
AND index_name = 'Events_Summaries_Performance_idx'
) > 0,
"SELECT 'Events_Summaries_Performance_idx already exists'",
"CREATE INDEX Events_Summaries_Performance_idx ON Events (MonitorId, StartDateTime, Archived, DiskSpace)"
))//
PREPARE stmt FROM @s//
EXECUTE stmt//
DEALLOCATE PREPARE stmt//
-- Clean up a previous typoed version of the index
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = DATABASE()
AND table_name = 'Events'
AND index_name = 'Events_Summaries_Perfomance_idx'
) > 0,
"DROP INDEX Events_Summaries_Perfomance_idx ON Events",
"SELECT 'Events_Summaries_Perfomance_idx does not exist'"
))//
PREPARE stmt FROM @s//
EXECUTE stmt//
DEALLOCATE PREPARE stmt//
-- Hourly Events View
CREATE OR REPLACE VIEW Events_Hour AS
SELECT
Id AS EventId,
MonitorId,
DiskSpace,
StartDateTime
FROM Events
WHERE StartDateTime >= (NOW() - INTERVAL 1 HOUR)//
-- Daily Events View
CREATE OR REPLACE VIEW Events_Day AS
SELECT
Id AS EventId,
MonitorId,
DiskSpace,
StartDateTime
FROM Events
WHERE StartDateTime >= (NOW() - INTERVAL 1 DAY)//
-- Weekly Events View
CREATE OR REPLACE VIEW Events_Week AS
SELECT
Id AS EventId,
MonitorId,
DiskSpace,
StartDateTime
FROM Events
WHERE StartDateTime >= (NOW() - INTERVAL 7 DAY)//
-- Monthly Events View
CREATE OR REPLACE VIEW Events_Month AS
SELECT
Id AS EventId,
MonitorId,
DiskSpace,
StartDateTime
FROM Events
WHERE StartDateTime >= (NOW() - INTERVAL 1 MONTH)//
-- Archived Events View
CREATE OR REPLACE VIEW Events_Archived AS
SELECT
Id AS EventId,
MonitorId,
DiskSpace
FROM Events
WHERE Archived = 1//
-- Event Summaries Source View (used by the refresh procedure)
CREATE OR REPLACE VIEW VIEW_Event_Summaries AS
SELECT
MonitorId,
-- Hour Stats
COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 HOUR) THEN 1 END) AS HourEvents,
COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 HOUR) THEN DiskSpace ELSE 0 END), 0) AS HourEventDiskSpace,
-- Day Stats
COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 DAY) THEN 1 END) AS DayEvents,
COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 DAY) THEN DiskSpace ELSE 0 END), 0) AS DayEventDiskSpace,
-- Week Stats
COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 7 DAY) THEN 1 END) AS WeekEvents,
COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 7 DAY) THEN DiskSpace ELSE 0 END), 0) AS WeekEventDiskSpace,
-- Month Stats
COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 MONTH) THEN 1 END) AS MonthEvents,
COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 MONTH) THEN DiskSpace ELSE 0 END), 0) AS MonthEventDiskSpace,
-- Archive Stats
COUNT(CASE WHEN Archived = 1 THEN 1 END) AS ArchivedEvents,
COALESCE(SUM(CASE WHEN Archived = 1 THEN DiskSpace ELSE 0 END), 0) AS ArchivedEventDiskSpace,
-- Totals
COUNT(Id) AS TotalEvents,
COALESCE(SUM(DiskSpace), 0) AS TotalEventDiskSpace
FROM Events
GROUP BY MonitorId//
-- Event Summaries snapshot table (SWR pattern)
CREATE TABLE IF NOT EXISTS `Event_Summaries` (
`MonitorId` int(10) unsigned NOT NULL,
`HourEvents` int(10) DEFAULT 0,
`HourEventDiskSpace` bigint DEFAULT 0,
`DayEvents` int(10) DEFAULT 0,
`DayEventDiskSpace` bigint DEFAULT 0,
`WeekEvents` int(10) DEFAULT 0,
`WeekEventDiskSpace` bigint DEFAULT 0,
`MonthEvents` int(10) DEFAULT 0,
`MonthEventDiskSpace` bigint DEFAULT 0,
`ArchivedEvents` int(10) DEFAULT 0,
`ArchivedEventDiskSpace` bigint DEFAULT 0,
`TotalEvents` int(10) DEFAULT 0,
`TotalEventDiskSpace` bigint DEFAULT 0,
PRIMARY KEY (`MonitorId`)
) ENGINE=InnoDB//
-- Metadata table for SWR staleness tracking
CREATE TABLE IF NOT EXISTS `Event_Summaries_Metadata` (
`table_name` VARCHAR(64) NOT NULL,
`last_updated` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00',
PRIMARY KEY (`table_name`)
) ENGINE=InnoDB//
INSERT IGNORE INTO `Event_Summaries_Metadata`
(`table_name`, `last_updated`) VALUES ('Event_Summaries', '1970-01-01 00:00:00')//
-- Stored procedure: atomic refresh of Event_Summaries with GET_LOCK to prevent thundering herd
DROP PROCEDURE IF EXISTS `Refresh_Summaries_SWR`//
CREATE PROCEDURE `Refresh_Summaries_SWR`()
proc: BEGIN
DECLARE v_lock_result INT DEFAULT 0;
DECLARE v_last DATETIME;
-- Use READ COMMITTED so the INSERT...SELECT below performs a consistent
-- snapshot read on Events without taking shared next-key locks. Under the
-- default REPEATABLE READ (and STATEMENT binlog format) the source-side
-- locks deadlock with concurrent DELETE FROM Events.
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Non-blocking lock: skip if another process is already refreshing
SET v_lock_result = GET_LOCK('refresh_summaries_lock', 0);
IF v_lock_result != 1 THEN
-- Another process holds the lock; return immediately (stale read is fine)
LEAVE proc;
END IF;
-- Double-check staleness inside lock
SELECT `last_updated` INTO v_last
FROM `Event_Summaries_Metadata`
WHERE `table_name` = 'Event_Summaries';
IF v_last IS NOT NULL AND TIMESTAMPDIFF(SECOND, v_last, NOW()) < 60 THEN
DO RELEASE_LOCK('refresh_summaries_lock');
LEAVE proc;
END IF;
-- Atomic rename pattern: build new table, swap, drop old
DROP TABLE IF EXISTS `Event_Summaries_New`;
CREATE TABLE `Event_Summaries_New` LIKE `Event_Summaries`;
INSERT INTO `Event_Summaries_New` SELECT * FROM `VIEW_Event_Summaries`;
DROP TABLE IF EXISTS `Event_Summaries_Old`;
RENAME TABLE `Event_Summaries` TO `Event_Summaries_Old`,
`Event_Summaries_New` TO `Event_Summaries`;
DROP TABLE IF EXISTS `Event_Summaries_Old`;
-- Update metadata timestamp
UPDATE `Event_Summaries_Metadata`
SET `last_updated` = NOW()
WHERE `table_name` = 'Event_Summaries';
DO RELEASE_LOCK('refresh_summaries_lock');
END proc//
-- MySQL EVENT for background refresh every 600 seconds (10 minutes)
-- Note: Requires event_scheduler=ON in my.cnf or SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS `Event_Summaries_Refresh_Event`//
CREATE EVENT IF NOT EXISTS `Event_Summaries_Refresh_Event`
ON SCHEDULE EVERY 600 SECOND
DO CALL `Refresh_Summaries_SWR`()//
DELIMITER ;