feat(forward): rate cap + seed-state mode for safe backfill (v1.5.2)
Two safety nets for first-deploy on Blastware ACH machines that
have accumulated tens or hundreds of thousands of historical events
in the watch folder.
1. SFM_MAX_FORWARDS_PER_PASS (default 500, 0=unlimited)
---------------------------------------------------
Cap on the number of events forwarded per scan tick. At the
60-second default interval that's ~30K events/hour throughput —
the SFM server gets a steady drip instead of one giant burst.
Scan now sorts by mtime ascending so backfill advances
chronologically (oldest first) and successive scans always
make progress instead of re-considering the same N newest files.
Wired into:
- event_forwarder.find_pending_events / forward_pending
- series3_watcher.run_watcher loop
- config-template.ini
- settings_dialog SFM Forward tab (new "Max Events Per Pass"
spinbox, validated in _on_save)
2. event_forwarder.py --seed-state CLI
-----------------------------------
One-shot mode that walks the watch folder, sha256s every in-window
event binary, and marks them all as already-forwarded WITHOUT
POSTing anything. Run before flipping SFM_FORWARD_ENABLED=true
to skip the historical backfill entirely — the watcher then only
forwards events that appear AFTER the seed.
Usage:
python event_forwarder.py --seed-state \
--watch "C:\Blastware 10\Event\autocall home" \
--state "C:\...\sfm_forwarded.json" \
[--max-age-days 365]
7 new unit tests:
- max_per_pass cap enforcement (=N, =0 unlimited, oldest-first
ordering)
- seed-state mode (in-window seeding, max-age skip,
end-to-end skip-after-seed, idempotent re-runs)
README adds a "First-time deployment" section walking through both
options. Bumps to v1.5.2.
This commit is contained in:
+12
-7
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Series 3 Watcher — Settings Dialog v1.5.1
|
||||
Series 3 Watcher — Settings Dialog v1.5.2
|
||||
|
||||
Provides a Tkinter settings dialog that doubles as a first-run wizard.
|
||||
|
||||
@@ -52,6 +52,7 @@ DEFAULTS = {
|
||||
"SFM_MISSING_REPORT_GRACE_SECONDS": "60",
|
||||
"SFM_HTTP_TIMEOUT": "60",
|
||||
"SFM_STATE_FILE": "",
|
||||
"SFM_MAX_FORWARDS_PER_PASS": "500",
|
||||
}
|
||||
|
||||
|
||||
@@ -257,6 +258,7 @@ class SettingsDialog:
|
||||
self.var_sfm_missing_report_grace = tk.StringVar(value=v["SFM_MISSING_REPORT_GRACE_SECONDS"])
|
||||
self.var_sfm_http_timeout = tk.StringVar(value=v["SFM_HTTP_TIMEOUT"])
|
||||
self.var_sfm_state_file = tk.StringVar(value=v["SFM_STATE_FILE"])
|
||||
self.var_sfm_max_per_pass = tk.StringVar(value=v["SFM_MAX_FORWARDS_PER_PASS"])
|
||||
|
||||
# --- UI construction ---
|
||||
|
||||
@@ -522,15 +524,16 @@ class SettingsDialog:
|
||||
self._sfm_test_status.grid(row=0, column=2, padx=(6, 0))
|
||||
|
||||
_add_label_spinbox(f, 2, "Forward Interval (sec)", self.var_sfm_forward_interval, 5, 3600)
|
||||
_add_label_spinbox(f, 3, "Quiescence (sec)", self.var_sfm_quiescence, 1, 60)
|
||||
_add_label_spinbox(f, 4, "Missing-Report Grace (sec)", self.var_sfm_missing_report_grace, 0, 600)
|
||||
_add_label_spinbox(f, 5, "HTTP Timeout (sec)", self.var_sfm_http_timeout, 5, 600)
|
||||
_add_label_spinbox(f, 3, "Max Events Per Pass", self.var_sfm_max_per_pass, 0, 100000)
|
||||
_add_label_spinbox(f, 4, "Quiescence (sec)", self.var_sfm_quiescence, 1, 60)
|
||||
_add_label_spinbox(f, 5, "Missing-Report Grace (sec)", self.var_sfm_missing_report_grace, 0, 600)
|
||||
_add_label_spinbox(f, 6, "HTTP Timeout (sec)", self.var_sfm_http_timeout, 5, 600)
|
||||
|
||||
tk.Label(f, text="State File", anchor="w").grid(
|
||||
row=6, column=0, sticky="w", padx=(8, 4), pady=4
|
||||
row=7, column=0, sticky="w", padx=(8, 4), pady=4
|
||||
)
|
||||
state_frame = tk.Frame(f)
|
||||
state_frame.grid(row=6, column=1, sticky="ew", padx=(0, 8), pady=4)
|
||||
state_frame.grid(row=7, column=1, sticky="ew", padx=(0, 8), pady=4)
|
||||
state_frame.columnconfigure(0, weight=1)
|
||||
|
||||
state_entry = ttk.Entry(state_frame, textvariable=self.var_sfm_state_file, width=32)
|
||||
@@ -560,7 +563,7 @@ class SettingsDialog:
|
||||
"to default to <log dir>/sfm_forwarded.json."
|
||||
)
|
||||
tk.Label(f, text=hint_text, justify="left", fg="#555555", wraplength=380).grid(
|
||||
row=7, column=0, columnspan=2, sticky="w", padx=(8, 8), pady=(8, 4)
|
||||
row=8, column=0, columnspan=2, sticky="w", padx=(8, 8), pady=(8, 4)
|
||||
)
|
||||
|
||||
def _test_sfm_connection(self):
|
||||
@@ -637,6 +640,7 @@ class SettingsDialog:
|
||||
(self.var_sfm_quiescence, "SFM Quiescence", 1, 60, 5),
|
||||
(self.var_sfm_missing_report_grace, "SFM Missing-Report Grace", 0, 600, 60),
|
||||
(self.var_sfm_http_timeout, "SFM HTTP Timeout", 5, 600, 60),
|
||||
(self.var_sfm_max_per_pass, "SFM Max Events Per Pass", 0, 100000, 500),
|
||||
]
|
||||
int_values = {}
|
||||
for var, name, mn, mx, dflt in checks:
|
||||
@@ -693,6 +697,7 @@ class SettingsDialog:
|
||||
"SFM_MISSING_REPORT_GRACE_SECONDS": str(int_values["SFM Missing-Report Grace"]),
|
||||
"SFM_HTTP_TIMEOUT": str(int_values["SFM HTTP Timeout"]),
|
||||
"SFM_STATE_FILE": self.var_sfm_state_file.get().strip(),
|
||||
"SFM_MAX_FORWARDS_PER_PASS": str(int_values["SFM Max Events Per Pass"]),
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user