Evita baseline completa del watchdog

This commit is contained in:
2026-07-02 10:05:18 +02:00
parent 2f0a5d53cf
commit 7b609a8d65
7 changed files with 62 additions and 16 deletions

View File

@@ -317,9 +317,12 @@ unchanged_checks_required: 2
## Riduzione falsi positivi
Quando si monitora una directory molto ampia, ad esempio un'intera unita' `D:\`, la semplice navigazione con Explorer o altri programmi puo' generare eventi `modified` non rilevanti. Per ridurre questi falsi positivi, il servizio deve mantenere uno snapshot locale di dimensione e timestamp di modifica dei file monitorati.
Quando si monitora una directory molto ampia, ad esempio un'intera unita' `D:\`, la semplice navigazione con Explorer o altri programmi puo' generare eventi `modified` non rilevanti. Per ridurre questi falsi positivi senza costruire una baseline completa del disco, il servizio deve applicare una strategia pigra:
All'avvio del servizio viene popolata una baseline degli snapshot per i file gia' presenti, senza inserirli nella coda di backup. Un evento `modified` viene registrato solo se dimensione o timestamp di scrittura risultano diversi dallo snapshot conosciuto.
- non deve scansionare all'avvio tutti i file gia' presenti;
- un evento `modified` viene registrato solo se il timestamp di modifica del file e' recente rispetto all'evento;
- se esiste gia' uno snapshot locale per quel file, l'evento viene registrato solo se dimensione o timestamp risultano diversi dallo snapshot conosciuto;
- i file gia' presenti nel disco entrano quindi in coda solo quando vengono realmente salvati/modificati, non quando vengono solo navigati.
## Configurazione

View File

@@ -6,6 +6,7 @@ autostart = true
[watch]
target_user = ""
modified_event_max_age_seconds = 300
include_dirs = [
"%USERPROFILE%\\Desktop",
"%USERPROFILE%\\Documents",

View File

@@ -40,6 +40,7 @@ class WatchConfig:
include_extensions: frozenset[str]
exclude_dirs: tuple[Path | str, ...]
exclude_patterns: tuple[str, ...]
modified_event_max_age_seconds: int
@dataclass(frozen=True)
@@ -187,6 +188,7 @@ def parse_config(data: dict[str, Any], path: Path) -> AppConfig:
include_extensions=include_extensions,
exclude_dirs=exclude_dirs,
exclude_patterns=tuple(watch.get("exclude_patterns", [])),
modified_event_max_age_seconds=int(watch.get("modified_event_max_age_seconds", 300)),
),
backup=BackupConfig(
engine=str(backup.get("engine", "robocopy")).lower(),

View File

@@ -61,8 +61,24 @@ class BakRestConfigApp(ctk.CTk):
target_frame = ctk.CTkFrame(self.monitor_tab)
target_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=8, pady=(8, 0))
target_frame.grid_columnconfigure(1, weight=1)
target_frame.grid_columnconfigure(3, weight=1)
self.target_user_var = tk.StringVar()
self.modified_event_max_age_var = tk.StringVar()
_label_entry(target_frame, "Utente percorsi", self.target_user_var, 0)
ctk.CTkLabel(target_frame, text="Eta max modifica (sec)", anchor="w").grid(
row=0,
column=2,
sticky="w",
padx=12,
pady=6,
)
ctk.CTkEntry(target_frame, textvariable=self.modified_event_max_age_var).grid(
row=0,
column=3,
sticky="ew",
padx=12,
pady=6,
)
left = ctk.CTkFrame(self.monitor_tab)
left.grid(row=1, column=0, sticky="nsew", padx=(0, 8), pady=8)
@@ -181,6 +197,7 @@ class BakRestConfigApp(ctk.CTk):
server_check = backup.setdefault("server_check", {})
self.target_user_var.set(str(watch.get("target_user", "")))
self.modified_event_max_age_var.set(str(watch.get("modified_event_max_age_seconds", 300)))
self.include_dirs.set_items(watch.get("include_dirs", []))
self.extensions.set_items(watch.get("include_extensions", []))
self.exclude_dirs.set_items(watch.get("exclude_dirs", []))
@@ -200,6 +217,10 @@ class BakRestConfigApp(ctk.CTk):
watch["include_dirs"] = self.include_dirs.items()
watch["target_user"] = self.target_user_var.get().strip()
watch["modified_event_max_age_seconds"] = _safe_int(
self.modified_event_max_age_var.get(),
300,
)
watch["include_extensions"] = self.extensions.items()
watch["exclude_dirs"] = self.exclude_dirs.items()
watch["exclude_patterns"] = self.exclude_patterns.items()

View File

@@ -6,6 +6,7 @@ autostart = true
[watch]
target_user = ""
modified_event_max_age_seconds = 300
include_dirs = [
"%USERPROFILE%\\\\Desktop",
"%USERPROFILE%\\\\Documents",

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import logging
import threading
import time
from pathlib import Path
from .config import AppConfig
@@ -35,7 +36,6 @@ class WatchdogRuntime:
watched = 0
for include_dir in self.config.watch.include_dirs:
if include_dir.exists():
self._prime_snapshots(include_dir)
observer.schedule(Handler(), str(include_dir), recursive=True)
self.logger.info("Monitoring directory: %s", include_dir)
watched += 1
@@ -72,6 +72,13 @@ class WatchdogRuntime:
return
size, mtime_ns = _safe_snapshot(target_path)
if event_type == "modified" and not _mtime_is_recent(
mtime_ns,
self.config.watch.modified_event_max_age_seconds,
):
self.logger.info("Ignored stale modified event: %s", target_path)
return
if event_type == "modified" and not self.registry.has_snapshot_changed(
str(target_path),
size,
@@ -94,19 +101,6 @@ class WatchdogRuntime:
while not self.stop_event.wait(60):
pass
def _prime_snapshots(self, root: Path) -> None:
self.logger.info("Priming file snapshots: %s", root)
count = 0
for path in root.rglob("*"):
if self.stop_event.is_set():
break
if not self.filter.should_track(path, is_directory=path.is_dir()):
continue
size, mtime_ns = _safe_snapshot(path)
self.registry.update_snapshot(str(path), size, mtime_ns)
count += 1
self.logger.info("Primed %s file snapshot(s): %s", count, root)
def _safe_snapshot(path: Path) -> tuple[int | None, int | None]:
try:
@@ -114,3 +108,10 @@ def _safe_snapshot(path: Path) -> tuple[int | None, int | None]:
return stat.st_size, stat.st_mtime_ns
except OSError:
return None, None
def _mtime_is_recent(mtime_ns: int | None, max_age_seconds: int) -> bool:
if mtime_ns is None:
return False
age_seconds = time.time() - (mtime_ns / 1_000_000_000)
return age_seconds <= max_age_seconds

17
tests/test_watcher.py Normal file
View File

@@ -0,0 +1,17 @@
from __future__ import annotations
import time
from bakrest.watcher import _mtime_is_recent
def test_mtime_is_recent_rejects_old_timestamps() -> None:
old_mtime_ns = int((time.time() - 3600) * 1_000_000_000)
assert not _mtime_is_recent(old_mtime_ns, 300)
def test_mtime_is_recent_accepts_recent_timestamps() -> None:
recent_mtime_ns = int(time.time() * 1_000_000_000)
assert _mtime_is_recent(recent_mtime_ns, 300)