Skip to content

Commit 8abecb7

Browse files
committed
PLG: Implement selective recording for Plugins_History to prevent unbounded growth
1 parent b0c687a commit 8abecb7

File tree

3 files changed

+424
-2
lines changed

3 files changed

+424
-2
lines changed

server/plugin.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,10 @@ def process_plugin_events(db, plugin, plugEventsArr):
786786
pluginEvents[index].status = "watched-not-changed"
787787
index += 1
788788

789+
# Track objects whose state actually changed this cycle
790+
# (only these will be recorded in Plugins_History)
791+
changed_this_cycle = set()
792+
789793
# Loop thru events and check if previously available objects are missing
790794
for tmpObj in pluginObjects:
791795
isMissing = True
@@ -799,6 +803,7 @@ def process_plugin_events(db, plugin, plugEventsArr):
799803
if tmpObj.status != "missing-in-last-scan":
800804
tmpObj.changed = timeNowUTC()
801805
tmpObj.status = "missing-in-last-scan"
806+
changed_this_cycle.add(tmpObj.idsHash)
802807
# mylog('debug', [f'[Plugins] Missing from last scan (PrimaryID | SecondaryID): {tmpObj.primaryId} | {tmpObj.secondaryId}'])
803808

804809
# Merge existing plugin objects with newly discovered ones and update existing ones with new values
@@ -807,10 +812,14 @@ def process_plugin_events(db, plugin, plugEventsArr):
807812
if tmpObjFromEvent.status == "not-processed":
808813
# This is a new object as it was not discovered as "exists" previously
809814
tmpObjFromEvent.status = "new"
815+
changed_this_cycle.add(tmpObjFromEvent.idsHash)
810816

811817
pluginObjects.append(tmpObjFromEvent)
812818
# update data of existing objects
813819
else:
820+
if tmpObjFromEvent.status == "watched-changed":
821+
changed_this_cycle.add(tmpObjFromEvent.idsHash)
822+
814823
index = 0
815824
for plugObj in pluginObjects:
816825
# find corresponding object for the event and merge
@@ -871,8 +880,9 @@ def process_plugin_events(db, plugin, plugEventsArr):
871880
if plugObj.status in statuses_to_report_on:
872881
events_to_insert.append(values)
873882

874-
# combine all DB insert and update events into one for history
875-
history_to_insert.append(values)
883+
# Only record history for objects that actually changed this cycle
884+
if plugObj.idsHash in changed_this_cycle:
885+
history_to_insert.append(values)
876886

877887
mylog("debug", f"[Plugins] pluginEvents count: {len(pluginEvents)}")
878888
mylog("debug", f"[Plugins] pluginObjects count: {len(pluginObjects)}")

test/db_test_helpers.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys, os
77
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
88
from db_test_helpers import make_db, insert_device, minutes_ago, DummyDB, down_event_macs, make_device_dict, sync_insert_devices
9+
from db_test_helpers import make_plugin_db, make_plugin_dict, make_plugin_event_row, seed_plugin_object, plugin_history_rows, plugin_objects_rows, PluginFakeDB
910
"""
1011

1112
import sqlite3
@@ -351,3 +352,201 @@ def __init__(self, conn: sqlite3.Connection):
351352

352353
def commitDB(self) -> None:
353354
self._conn.commit()
355+
356+
357+
# ---------------------------------------------------------------------------
358+
# Plugin tables DDL & helpers (used by test/server/test_plugin_history_filtering.py)
359+
# ---------------------------------------------------------------------------
360+
361+
CREATE_PLUGINS_OBJECTS = """
362+
CREATE TABLE IF NOT EXISTS Plugins_Objects(
363+
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
364+
plugin TEXT NOT NULL,
365+
objectPrimaryId TEXT NOT NULL,
366+
objectSecondaryId TEXT NOT NULL,
367+
dateTimeCreated TEXT NOT NULL,
368+
dateTimeChanged TEXT NOT NULL,
369+
watchedValue1 TEXT NOT NULL,
370+
watchedValue2 TEXT NOT NULL,
371+
watchedValue3 TEXT NOT NULL,
372+
watchedValue4 TEXT NOT NULL,
373+
"status" TEXT NOT NULL,
374+
extra TEXT NOT NULL,
375+
userData TEXT NOT NULL,
376+
foreignKey TEXT NOT NULL,
377+
syncHubNodeName TEXT,
378+
helpVal1 TEXT,
379+
helpVal2 TEXT,
380+
helpVal3 TEXT,
381+
helpVal4 TEXT,
382+
objectGuid TEXT
383+
);
384+
"""
385+
386+
CREATE_PLUGINS_EVENTS = """
387+
CREATE TABLE IF NOT EXISTS Plugins_Events(
388+
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
389+
plugin TEXT NOT NULL,
390+
objectPrimaryId TEXT NOT NULL,
391+
objectSecondaryId TEXT NOT NULL,
392+
dateTimeCreated TEXT NOT NULL,
393+
dateTimeChanged TEXT NOT NULL,
394+
watchedValue1 TEXT NOT NULL,
395+
watchedValue2 TEXT NOT NULL,
396+
watchedValue3 TEXT NOT NULL,
397+
watchedValue4 TEXT NOT NULL,
398+
"status" TEXT NOT NULL,
399+
extra TEXT NOT NULL,
400+
userData TEXT NOT NULL,
401+
foreignKey TEXT NOT NULL,
402+
syncHubNodeName TEXT,
403+
helpVal1 TEXT,
404+
helpVal2 TEXT,
405+
helpVal3 TEXT,
406+
helpVal4 TEXT,
407+
objectGuid TEXT
408+
);
409+
"""
410+
411+
CREATE_PLUGINS_HISTORY = """
412+
CREATE TABLE IF NOT EXISTS Plugins_History(
413+
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
414+
plugin TEXT NOT NULL,
415+
objectPrimaryId TEXT NOT NULL,
416+
objectSecondaryId TEXT NOT NULL,
417+
dateTimeCreated TEXT NOT NULL,
418+
dateTimeChanged TEXT NOT NULL,
419+
watchedValue1 TEXT NOT NULL,
420+
watchedValue2 TEXT NOT NULL,
421+
watchedValue3 TEXT NOT NULL,
422+
watchedValue4 TEXT NOT NULL,
423+
"status" TEXT NOT NULL,
424+
extra TEXT NOT NULL,
425+
userData TEXT NOT NULL,
426+
foreignKey TEXT NOT NULL,
427+
syncHubNodeName TEXT,
428+
helpVal1 TEXT,
429+
helpVal2 TEXT,
430+
helpVal3 TEXT,
431+
helpVal4 TEXT,
432+
objectGuid TEXT
433+
);
434+
"""
435+
436+
437+
class PluginFakeSQL:
438+
"""Wraps a sqlite3.Cursor to provide the interface plugin.py expects."""
439+
def __init__(self, cursor):
440+
self._cursor = cursor
441+
442+
def execute(self, sql, params=None):
443+
if params:
444+
return self._cursor.execute(sql, params)
445+
return self._cursor.execute(sql)
446+
447+
def executemany(self, sql, params_list):
448+
return self._cursor.executemany(sql, params_list)
449+
450+
451+
class PluginFakeDB:
452+
"""Minimal DB facade expected by process_plugin_events."""
453+
def __init__(self, conn):
454+
self.sql_connection = conn
455+
self.sql = PluginFakeSQL(conn.cursor())
456+
457+
def get_sql_array(self, query):
458+
cur = self.sql_connection.cursor()
459+
cur.execute(query)
460+
return cur.fetchall()
461+
462+
def commitDB(self):
463+
self.sql_connection.commit()
464+
465+
466+
def make_plugin_db() -> tuple:
467+
"""
468+
Return a (PluginFakeDB, connection) backed by an in-memory SQLite
469+
database with all three plugin tables created.
470+
"""
471+
conn = sqlite3.connect(":memory:")
472+
conn.executescript(
473+
CREATE_PLUGINS_OBJECTS + CREATE_PLUGINS_EVENTS + CREATE_PLUGINS_HISTORY
474+
)
475+
conn.commit()
476+
db = PluginFakeDB(conn)
477+
return db, conn
478+
479+
480+
def make_plugin_dict(prefix: str, watched_columns=None) -> dict:
481+
"""Return a minimal plugin dict compatible with process_plugin_events."""
482+
return {
483+
"unique_prefix": prefix,
484+
"settings": [
485+
{
486+
"function": "WATCH",
487+
"value": watched_columns or ["watchedValue1"],
488+
},
489+
],
490+
}
491+
492+
493+
def make_plugin_event_row(prefix: str, primary_id: str, secondary_id="sec",
494+
watched1="val1", watched2="", watched3="",
495+
watched4="", changed="2026-01-01 00:00:00",
496+
extra="", user_data="", foreign_key="",
497+
status="not-processed"):
498+
"""Build a tuple mimicking a raw plugin output row (19 columns + index)."""
499+
return (
500+
0, # index (placeholder, not used for events)
501+
prefix, # plugin
502+
primary_id,
503+
secondary_id,
504+
changed, # dateTimeCreated
505+
changed, # dateTimeChanged
506+
watched1,
507+
watched2,
508+
watched3,
509+
watched4,
510+
status,
511+
extra,
512+
user_data,
513+
foreign_key,
514+
None, # syncHubNodeName
515+
None, # helpVal1
516+
None, # helpVal2
517+
None, # helpVal3
518+
None, # helpVal4
519+
)
520+
521+
522+
def seed_plugin_object(cur, prefix: str, primary_id: str,
523+
secondary_id="sec", watched1="val1",
524+
status="watched-not-changed",
525+
changed="2026-01-01 00:00:00"):
526+
"""Insert a row into Plugins_Objects to simulate a pre-existing object."""
527+
cur.execute(
528+
"""INSERT INTO Plugins_Objects
529+
(plugin, objectPrimaryId, objectSecondaryId, dateTimeCreated,
530+
dateTimeChanged, watchedValue1, watchedValue2, watchedValue3,
531+
watchedValue4, status, extra, userData, foreignKey)
532+
VALUES (?, ?, ?, ?, ?, ?, '', '', '', ?, '', '', '')""",
533+
(prefix, primary_id, secondary_id, changed, changed, watched1, status),
534+
)
535+
536+
537+
def plugin_history_rows(conn, prefix: str):
538+
"""Return all Plugins_History rows for a given plugin prefix."""
539+
cur = conn.cursor()
540+
cur.execute(
541+
"SELECT * FROM Plugins_History WHERE plugin = ?", (prefix,)
542+
)
543+
return cur.fetchall()
544+
545+
546+
def plugin_objects_rows(conn, prefix: str):
547+
"""Return all Plugins_Objects rows for a given plugin prefix."""
548+
cur = conn.cursor()
549+
cur.execute(
550+
"SELECT * FROM Plugins_Objects WHERE plugin = ?", (prefix,)
551+
)
552+
return cur.fetchall()

0 commit comments

Comments
 (0)