Evita baseline completa del watchdog
This commit is contained in:
@@ -317,9 +317,12 @@ unchanged_checks_required: 2
|
|||||||
|
|
||||||
## Riduzione falsi positivi
|
## 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
|
## Configurazione
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ autostart = true
|
|||||||
|
|
||||||
[watch]
|
[watch]
|
||||||
target_user = ""
|
target_user = ""
|
||||||
|
modified_event_max_age_seconds = 300
|
||||||
include_dirs = [
|
include_dirs = [
|
||||||
"%USERPROFILE%\\Desktop",
|
"%USERPROFILE%\\Desktop",
|
||||||
"%USERPROFILE%\\Documents",
|
"%USERPROFILE%\\Documents",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class WatchConfig:
|
|||||||
include_extensions: frozenset[str]
|
include_extensions: frozenset[str]
|
||||||
exclude_dirs: tuple[Path | str, ...]
|
exclude_dirs: tuple[Path | str, ...]
|
||||||
exclude_patterns: tuple[str, ...]
|
exclude_patterns: tuple[str, ...]
|
||||||
|
modified_event_max_age_seconds: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -187,6 +188,7 @@ def parse_config(data: dict[str, Any], path: Path) -> AppConfig:
|
|||||||
include_extensions=include_extensions,
|
include_extensions=include_extensions,
|
||||||
exclude_dirs=exclude_dirs,
|
exclude_dirs=exclude_dirs,
|
||||||
exclude_patterns=tuple(watch.get("exclude_patterns", [])),
|
exclude_patterns=tuple(watch.get("exclude_patterns", [])),
|
||||||
|
modified_event_max_age_seconds=int(watch.get("modified_event_max_age_seconds", 300)),
|
||||||
),
|
),
|
||||||
backup=BackupConfig(
|
backup=BackupConfig(
|
||||||
engine=str(backup.get("engine", "robocopy")).lower(),
|
engine=str(backup.get("engine", "robocopy")).lower(),
|
||||||
|
|||||||
@@ -61,8 +61,24 @@ class BakRestConfigApp(ctk.CTk):
|
|||||||
target_frame = ctk.CTkFrame(self.monitor_tab)
|
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(row=0, column=0, columnspan=2, sticky="ew", padx=8, pady=(8, 0))
|
||||||
target_frame.grid_columnconfigure(1, weight=1)
|
target_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
target_frame.grid_columnconfigure(3, weight=1)
|
||||||
self.target_user_var = tk.StringVar()
|
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)
|
_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 = ctk.CTkFrame(self.monitor_tab)
|
||||||
left.grid(row=1, column=0, sticky="nsew", padx=(0, 8), pady=8)
|
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", {})
|
server_check = backup.setdefault("server_check", {})
|
||||||
|
|
||||||
self.target_user_var.set(str(watch.get("target_user", "")))
|
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.include_dirs.set_items(watch.get("include_dirs", []))
|
||||||
self.extensions.set_items(watch.get("include_extensions", []))
|
self.extensions.set_items(watch.get("include_extensions", []))
|
||||||
self.exclude_dirs.set_items(watch.get("exclude_dirs", []))
|
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["include_dirs"] = self.include_dirs.items()
|
||||||
watch["target_user"] = self.target_user_var.get().strip()
|
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["include_extensions"] = self.extensions.items()
|
||||||
watch["exclude_dirs"] = self.exclude_dirs.items()
|
watch["exclude_dirs"] = self.exclude_dirs.items()
|
||||||
watch["exclude_patterns"] = self.exclude_patterns.items()
|
watch["exclude_patterns"] = self.exclude_patterns.items()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ autostart = true
|
|||||||
|
|
||||||
[watch]
|
[watch]
|
||||||
target_user = ""
|
target_user = ""
|
||||||
|
modified_event_max_age_seconds = 300
|
||||||
include_dirs = [
|
include_dirs = [
|
||||||
"%USERPROFILE%\\\\Desktop",
|
"%USERPROFILE%\\\\Desktop",
|
||||||
"%USERPROFILE%\\\\Documents",
|
"%USERPROFILE%\\\\Documents",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .config import AppConfig
|
from .config import AppConfig
|
||||||
@@ -35,7 +36,6 @@ class WatchdogRuntime:
|
|||||||
watched = 0
|
watched = 0
|
||||||
for include_dir in self.config.watch.include_dirs:
|
for include_dir in self.config.watch.include_dirs:
|
||||||
if include_dir.exists():
|
if include_dir.exists():
|
||||||
self._prime_snapshots(include_dir)
|
|
||||||
observer.schedule(Handler(), str(include_dir), recursive=True)
|
observer.schedule(Handler(), str(include_dir), recursive=True)
|
||||||
self.logger.info("Monitoring directory: %s", include_dir)
|
self.logger.info("Monitoring directory: %s", include_dir)
|
||||||
watched += 1
|
watched += 1
|
||||||
@@ -72,6 +72,13 @@ class WatchdogRuntime:
|
|||||||
return
|
return
|
||||||
|
|
||||||
size, mtime_ns = _safe_snapshot(target_path)
|
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(
|
if event_type == "modified" and not self.registry.has_snapshot_changed(
|
||||||
str(target_path),
|
str(target_path),
|
||||||
size,
|
size,
|
||||||
@@ -94,19 +101,6 @@ class WatchdogRuntime:
|
|||||||
while not self.stop_event.wait(60):
|
while not self.stop_event.wait(60):
|
||||||
pass
|
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]:
|
def _safe_snapshot(path: Path) -> tuple[int | None, int | None]:
|
||||||
try:
|
try:
|
||||||
@@ -114,3 +108,10 @@ def _safe_snapshot(path: Path) -> tuple[int | None, int | None]:
|
|||||||
return stat.st_size, stat.st_mtime_ns
|
return stat.st_size, stat.st_mtime_ns
|
||||||
except OSError:
|
except OSError:
|
||||||
return None, None
|
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
17
tests/test_watcher.py
Normal 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)
|
||||||
Reference in New Issue
Block a user