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:
 | 
					# Version consists of:
 | 
				
			||||||
# Major.Enhancement.Minor.Patch.Test  (Test is alphanumeric; doesn’t trigger auto update)
 | 
					# 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 ----------
 | 
					# ---------- Env loading ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,8 @@ class DataManager:
 | 
				
			|||||||
            'nick_reviews': [],
 | 
					            'nick_reviews': [],
 | 
				
			||||||
            'rr_msg_channels': [],
 | 
					            'rr_msg_channels': [],
 | 
				
			||||||
            '_counters': {},         # key -> int (metrics)
 | 
					            '_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):
 | 
					    def _load(self):
 | 
				
			||||||
@ -44,6 +45,7 @@ class DataManager:
 | 
				
			|||||||
                    raise ValueError("root is not an object")
 | 
					                    raise ValueError("root is not an object")
 | 
				
			||||||
                data.setdefault('_counters', {})
 | 
					                data.setdefault('_counters', {})
 | 
				
			||||||
                data.setdefault('_events_seen', {})
 | 
					                data.setdefault('_events_seen', {})
 | 
				
			||||||
 | 
					                data.setdefault('_counter_last_ts', {})
 | 
				
			||||||
                return data
 | 
					                return data
 | 
				
			||||||
        except FileNotFoundError:
 | 
					        except FileNotFoundError:
 | 
				
			||||||
            default = self._default_payload()
 | 
					            default = self._default_payload()
 | 
				
			||||||
@ -104,7 +106,7 @@ class DataManager:
 | 
				
			|||||||
                    return True
 | 
					                    return True
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ------------- counters -------------
 | 
					    # ------------- counters (plain) -------------
 | 
				
			||||||
    def incr_counter(self, key: str, by: int = 1) -> int:
 | 
					    def incr_counter(self, key: str, by: int = 1) -> int:
 | 
				
			||||||
        with self.lock:
 | 
					        with self.lock:
 | 
				
			||||||
            c = self._data.setdefault('_counters', {})
 | 
					            c = self._data.setdefault('_counters', {})
 | 
				
			||||||
@ -121,36 +123,34 @@ class DataManager:
 | 
				
			|||||||
            c = dict(self._data.get('_counters', {}))
 | 
					            c = dict(self._data.get('_counters', {}))
 | 
				
			||||||
        return {k: v for k, v in c.items() if (not prefix or k.startswith(prefix))}
 | 
					        return {k: v for k, v in c.items() if (not prefix or k.startswith(prefix))}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ------------- NEW: once-per-event increment with TTL dedup -------------
 | 
					    # ------------- counters (timelocked) -------------
 | 
				
			||||||
    def incr_counter_once(self, counter_key: str, event_key: str, window_sec: float = 3.0, namespace: str = "cmd") -> int | None:
 | 
					    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`.
 | 
					        Increment `counter_key` at most once per `window_sec`.
 | 
				
			||||||
        Returns new counter value, or None if this event is a duplicate.
 | 
					        Returns the new value if incremented, or None if suppressed by the timelock.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        now = time.time()
 | 
					        now = time.time()
 | 
				
			||||||
        stamp = f"{namespace}:{event_key}"
 | 
					 | 
				
			||||||
        with self.lock:
 | 
					        with self.lock:
 | 
				
			||||||
            seen = self._data.setdefault('_events_seen', {})
 | 
					            last_map = self._data.setdefault('_counter_last_ts', {})
 | 
				
			||||||
            last = float(seen.get(stamp, 0.0))
 | 
					            last = float(last_map.get(counter_key, 0.0))
 | 
				
			||||||
            if now - last <= float(window_sec):
 | 
					            if now - last < float(window_sec):
 | 
				
			||||||
                # duplicate; ignore
 | 
					                # within lock window -> ignore
 | 
				
			||||||
                return None
 | 
					                return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # mark seen
 | 
					            # increment and stamp
 | 
				
			||||||
            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
 | 
					 | 
				
			||||||
            counters = self._data.setdefault('_counters', {})
 | 
					            counters = self._data.setdefault('_counters', {})
 | 
				
			||||||
            counters[counter_key] = int(counters.get(counter_key, 0)) + 1
 | 
					            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)
 | 
					            self._save(self._data)
 | 
				
			||||||
            return counters[counter_key]
 | 
					            return counters[counter_key]
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ from discord.ext import commands
 | 
				
			|||||||
import discord
 | 
					import discord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COUNTER_KEY_PREFIX = "cmd::"
 | 
					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:
 | 
					def _key_from_app(cmd: discord.app_commands.Command) -> str:
 | 
				
			||||||
    name = getattr(cmd, "qualified_name", None) or getattr(cmd, "name", "unknown")
 | 
					    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}"
 | 
					    return f"{COUNTER_KEY_PREFIX}{name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UsageStatsCog(commands.Cog):
 | 
					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):
 | 
					    def __init__(self, bot: commands.Bot):
 | 
				
			||||||
        self.bot = bot
 | 
					        self.bot = bot
 | 
				
			||||||
        print("[usage] UsageStatsCog init")
 | 
					        print("[usage] UsageStatsCog init (timelock)")
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # ---- successful completions ----
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @commands.Cog.listener()
 | 
					    @commands.Cog.listener()
 | 
				
			||||||
    async def on_app_command_completion(self, interaction: discord.Interaction, command: discord.app_commands.Command):
 | 
					    async def on_app_command_completion(self, interaction: discord.Interaction, command: discord.app_commands.Command):
 | 
				
			||||||
        dm = getattr(self.bot, "data_manager", None)
 | 
					        dm = getattr(self.bot, "data_manager", None)
 | 
				
			||||||
        if not dm:
 | 
					        if not dm:
 | 
				
			||||||
            print("[usage] app ~~ skip (no data_manager)")
 | 
					 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        try:
 | 
					        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)
 | 
					            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:
 | 
					            if newv is not None:
 | 
				
			||||||
                print(f"[usage] app ++ {counter_key} -> {newv}")
 | 
					                print(f"[usage] app ++ {counter_key} -> {newv}")
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                print(f"[usage] app ~~ dup ignored ({event_key})")
 | 
					                print(f"[usage] app ~~ timelocked {counter_key}")
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            print("[usage] app !! incr failed:", repr(e))
 | 
					            print("[usage] app !! incr failed:", repr(e))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @commands.Cog.listener()
 | 
					    @commands.Cog.listener()
 | 
				
			||||||
    async def on_command_completion(self, ctx: commands.Context):
 | 
					    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):
 | 
					        if isinstance(getattr(ctx, "command", None), commands.HybridCommand) and getattr(ctx, "interaction", None):
 | 
				
			||||||
            print("[usage] px  ~~ hybrid-as-slash; ignore here")
 | 
					 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dm = getattr(self.bot, "data_manager", None)
 | 
					        dm = getattr(self.bot, "data_manager", None)
 | 
				
			||||||
        if not dm:
 | 
					        if not dm:
 | 
				
			||||||
            print("[usage] px  ~~ skip (no data_manager)")
 | 
					 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        try:
 | 
					        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)
 | 
					            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:
 | 
					            if newv is not None:
 | 
				
			||||||
                print(f"[usage] px  ++ {counter_key} -> {newv}")
 | 
					                print(f"[usage] px  ++ {counter_key} -> {newv}")
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                print(f"[usage] px  ~~ dup ignored ({event_key})")
 | 
					                print(f"[usage] px  ~~ timelocked {counter_key}")
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            print("[usage] px  !! incr failed:", repr(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):
 | 
					async def setup(bot: commands.Bot):
 | 
				
			||||||
    # Prevent duplicate registration if extensions are reloaded / auto-discovered twice
 | 
					    # Prevent duplicate registration if extensions are reloaded / auto-discovered twice
 | 
				
			||||||
    if getattr(bot, "_usage_stats_loaded", False):
 | 
					    if getattr(bot, "_usage_stats_loaded", False):
 | 
				
			||||||
@ -88,4 +63,4 @@ async def setup(bot: commands.Bot):
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
    await bot.add_cog(UsageStatsCog(bot))
 | 
					    await bot.add_cog(UsageStatsCog(bot))
 | 
				
			||||||
    bot._usage_stats_loaded = True
 | 
					    bot._usage_stats_loaded = True
 | 
				
			||||||
    print("[usage] UsageStatsCog loaded")
 | 
					    print("[usage] UsageStatsCog loaded (timelock)")
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user