0.4.1.0.a5
- Yet another patch for the exectuion counter.. - *When stuff won't play nice, you brute-force it*
This commit is contained in:
parent
73175bbecd
commit
fe09e1dd1f
2
bot.py
2
bot.py
@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
|
||||
|
||||
# Version consists of:
|
||||
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update)
|
||||
VERSION = "0.4.1.0.a4"
|
||||
VERSION = "0.4.1.0.a5"
|
||||
|
||||
# ---------- Env loading ----------
|
||||
|
||||
|
@ -33,7 +33,8 @@ class DataManager:
|
||||
'nick_reviews': [],
|
||||
'rr_msg_channels': [],
|
||||
'_counters': {}, # key -> int (metrics)
|
||||
'_events_seen': {}, # stamp-key -> unix_ts (dedup window)
|
||||
'_events_seen': {}, # optional (kept for other uses)
|
||||
'_counter_last_ts': {}, # key -> last increment unix_ts (timelock)
|
||||
}
|
||||
|
||||
def _load(self):
|
||||
@ -44,6 +45,7 @@ class DataManager:
|
||||
raise ValueError("root is not an object")
|
||||
data.setdefault('_counters', {})
|
||||
data.setdefault('_events_seen', {})
|
||||
data.setdefault('_counter_last_ts', {})
|
||||
return data
|
||||
except FileNotFoundError:
|
||||
default = self._default_payload()
|
||||
@ -104,7 +106,7 @@ class DataManager:
|
||||
return True
|
||||
return False
|
||||
|
||||
# ------------- counters -------------
|
||||
# ------------- counters (plain) -------------
|
||||
def incr_counter(self, key: str, by: int = 1) -> int:
|
||||
with self.lock:
|
||||
c = self._data.setdefault('_counters', {})
|
||||
@ -121,36 +123,34 @@ class DataManager:
|
||||
c = dict(self._data.get('_counters', {}))
|
||||
return {k: v for k, v in c.items() if (not prefix or k.startswith(prefix))}
|
||||
|
||||
# ------------- NEW: once-per-event increment with TTL dedup -------------
|
||||
def incr_counter_once(self, counter_key: str, event_key: str, window_sec: float = 3.0, namespace: str = "cmd") -> int | None:
|
||||
# ------------- counters (timelocked) -------------
|
||||
def incr_counter_timelocked(self, counter_key: str, window_sec: float = 1.0) -> int | None:
|
||||
"""
|
||||
Atomically: if `event_key` wasn't seen within `window_sec`, mark it seen and increment `counter_key`.
|
||||
Returns new counter value, or None if this event is a duplicate.
|
||||
Increment `counter_key` at most once per `window_sec`.
|
||||
Returns the new value if incremented, or None if suppressed by the timelock.
|
||||
"""
|
||||
now = time.time()
|
||||
stamp = f"{namespace}:{event_key}"
|
||||
with self.lock:
|
||||
seen = self._data.setdefault('_events_seen', {})
|
||||
last = float(seen.get(stamp, 0.0))
|
||||
if now - last <= float(window_sec):
|
||||
# duplicate; ignore
|
||||
last_map = self._data.setdefault('_counter_last_ts', {})
|
||||
last = float(last_map.get(counter_key, 0.0))
|
||||
if now - last < float(window_sec):
|
||||
# within lock window -> ignore
|
||||
return None
|
||||
|
||||
# mark seen
|
||||
seen[stamp] = now
|
||||
|
||||
# opportunistic pruning to keep file small
|
||||
if len(seen) > 5000:
|
||||
cutoff = now - (window_sec * 2)
|
||||
for k in list(seen.keys()):
|
||||
try:
|
||||
if float(seen.get(k, 0.0)) < cutoff:
|
||||
seen.pop(k, None)
|
||||
except Exception:
|
||||
seen.pop(k, None)
|
||||
|
||||
# increment
|
||||
# increment and stamp
|
||||
counters = self._data.setdefault('_counters', {})
|
||||
counters[counter_key] = int(counters.get(counter_key, 0)) + 1
|
||||
last_map[counter_key] = now
|
||||
|
||||
# opportunistic pruning for very old stamps (keeps file smaller)
|
||||
if len(last_map) > 5000:
|
||||
cutoff = now - (window_sec * 60)
|
||||
for k in list(last_map.keys()):
|
||||
try:
|
||||
if float(last_map.get(k, 0.0)) < cutoff:
|
||||
last_map.pop(k, None)
|
||||
except Exception:
|
||||
last_map.pop(k, None)
|
||||
|
||||
self._save(self._data)
|
||||
return counters[counter_key]
|
||||
|
@ -4,6 +4,7 @@ from discord.ext import commands
|
||||
import discord
|
||||
|
||||
COUNTER_KEY_PREFIX = "cmd::"
|
||||
LOCK_WINDOW_SEC = 1.0 # timelock window; change if you want stricter/looser locking
|
||||
|
||||
def _key_from_app(cmd: discord.app_commands.Command) -> str:
|
||||
name = getattr(cmd, "qualified_name", None) or getattr(cmd, "name", "unknown")
|
||||
@ -15,72 +16,46 @@ def _key_from_ctx(ctx: commands.Context) -> str:
|
||||
return f"{COUNTER_KEY_PREFIX}{name}"
|
||||
|
||||
class UsageStatsCog(commands.Cog):
|
||||
"""Lightweight command run counters with storage-level dedup."""
|
||||
"""Command run counters with a per-command timelock."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
print("[usage] UsageStatsCog init")
|
||||
|
||||
# ---- successful completions ----
|
||||
print("[usage] UsageStatsCog init (timelock)")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_app_command_completion(self, interaction: discord.Interaction, command: discord.app_commands.Command):
|
||||
dm = getattr(self.bot, "data_manager", None)
|
||||
if not dm:
|
||||
print("[usage] app ~~ skip (no data_manager)")
|
||||
return
|
||||
try:
|
||||
# Use the interaction id as the dedup event key
|
||||
event_key = str(getattr(interaction, "id", None) or "no-iid")
|
||||
counter_key = _key_from_app(command)
|
||||
newv = dm.incr_counter_once(counter_key=counter_key, event_key=event_key, window_sec=3.0, namespace="app")
|
||||
newv = dm.incr_counter_timelocked(counter_key, window_sec=LOCK_WINDOW_SEC)
|
||||
if newv is not None:
|
||||
print(f"[usage] app ++ {counter_key} -> {newv}")
|
||||
else:
|
||||
print(f"[usage] app ~~ dup ignored ({event_key})")
|
||||
print(f"[usage] app ~~ timelocked {counter_key}")
|
||||
except Exception as e:
|
||||
print("[usage] app !! incr failed:", repr(e))
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_command_completion(self, ctx: commands.Context):
|
||||
# If a HybridCommand was invoked via slash, skip here (the app listener handled it).
|
||||
# If a HybridCommand was invoked as a slash interaction, let the app listener count it.
|
||||
if isinstance(getattr(ctx, "command", None), commands.HybridCommand) and getattr(ctx, "interaction", None):
|
||||
print("[usage] px ~~ hybrid-as-slash; ignore here")
|
||||
return
|
||||
|
||||
dm = getattr(self.bot, "data_manager", None)
|
||||
if not dm:
|
||||
print("[usage] px ~~ skip (no data_manager)")
|
||||
return
|
||||
try:
|
||||
# Prefer interaction id when present (hybrid-as-slash defense), else message id
|
||||
event_key = None
|
||||
if getattr(ctx, "interaction", None):
|
||||
event_key = str(getattr(ctx.interaction, "id", None) or "")
|
||||
if not event_key:
|
||||
msg = getattr(ctx, "message", None)
|
||||
event_key = str(getattr(msg, "id", None) or "no-mid")
|
||||
|
||||
counter_key = _key_from_ctx(ctx)
|
||||
newv = dm.incr_counter_once(counter_key=counter_key, event_key=event_key, window_sec=3.0, namespace="px")
|
||||
newv = dm.incr_counter_timelocked(counter_key, window_sec=LOCK_WINDOW_SEC)
|
||||
if newv is not None:
|
||||
print(f"[usage] px ++ {counter_key} -> {newv}")
|
||||
else:
|
||||
print(f"[usage] px ~~ dup ignored ({event_key})")
|
||||
print(f"[usage] px ~~ timelocked {counter_key}")
|
||||
except Exception as e:
|
||||
print("[usage] px !! incr failed:", repr(e))
|
||||
|
||||
# ---- error visibility (optional) ----
|
||||
@commands.Cog.listener()
|
||||
async def on_app_command_error(self, interaction: discord.Interaction, error: Exception):
|
||||
iid = getattr(interaction, "id", None)
|
||||
print(f"[usage] app ** error ({type(error).__name__}) iid={iid}")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_command_error(self, ctx: commands.Context, error: Exception):
|
||||
mid = getattr(getattr(ctx, "message", None), "id", None)
|
||||
print(f"[usage] px ** error ({type(error).__name__}) mid={mid}")
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
# Prevent duplicate registration if extensions are reloaded / auto-discovered twice
|
||||
if getattr(bot, "_usage_stats_loaded", False):
|
||||
@ -88,4 +63,4 @@ async def setup(bot: commands.Bot):
|
||||
return
|
||||
await bot.add_cog(UsageStatsCog(bot))
|
||||
bot._usage_stats_loaded = True
|
||||
print("[usage] UsageStatsCog loaded")
|
||||
print("[usage] UsageStatsCog loaded (timelock)")
|
||||
|
Loading…
Reference in New Issue
Block a user