From 5e6430daaf794e6abdff17e721e254de2a19cc82 Mon Sep 17 00:00:00 2001 From: FURRO404 Date: Thu, 2 Jul 2026 11:52:26 -0700 Subject: [PATCH] feat: add entitlement_dirty_watch_task for fast entitlement cache refresh Polls a marker file bumped by the Whop webhook (srebot-web) so new purchases show up in /diagnose and /unlock within seconds instead of waiting on the hourly entitlement_cache_task. --- BOT/tasks.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/BOT/tasks.py b/BOT/tasks.py index adaae13..c268d0a 100644 --- a/BOT/tasks.py +++ b/BOT/tasks.py @@ -343,6 +343,49 @@ async def before_entitlement_cache(): await get_bot().wait_until_ready() +# ── Entitlement cache refresh (webhook-triggered, fast path) ──────────────── +# The Whop webhook lands in the separate srebot-webhook/srebot-web (Node) +# process, which only writes to entitlements.db — it has no direct channel +# into this process. It signals us by bumping the mtime of a marker file; +# we poll that file cheaply (a single stat call) every few seconds and do a +# full cache reload only when it actually changes, so new purchases show up +# in /diagnose and /unlock within seconds instead of waiting on the hourly +# entitlement_cache_task or an incidental refresh from another loop. +ENTITLEMENTS_DIRTY_PATH: Path = STORAGE_DIR / "entitlements.dirty" +_entitlements_dirty_mtime: float = 0.0 + + +@tasks.loop(seconds=5) +async def entitlement_dirty_watch_task(): + global _entitlements_dirty_mtime + try: + mtime = ENTITLEMENTS_DIRTY_PATH.stat().st_mtime + except FileNotFoundError: + return + if mtime <= _entitlements_dirty_mtime: + return + _entitlements_dirty_mtime = mtime + try: + await refresh_entitled_guilds(force=True) + await _record("entitlement_dirty_watch", True) + except Exception as e: + await _record("entitlement_dirty_watch", False, str(e)) + raise + + +@entitlement_dirty_watch_task.before_loop +async def before_entitlement_dirty_watch(): + await get_bot().wait_until_ready() + # Seed with the current mtime (if any) so a stale flag left over from + # before startup doesn't trigger a redundant refresh on the first tick — + # on_ready already does a forced refresh. + global _entitlements_dirty_mtime + try: + _entitlements_dirty_mtime = ENTITLEMENTS_DIRTY_PATH.stat().st_mtime + except FileNotFoundError: + pass + + # ============================================================================ # POINTS ALARM TASK # ============================================================================ @@ -693,6 +736,7 @@ async def start_all_tasks(): """ # Phase 1: Lightweight time-check tasks entitlement_cache_task.start() + entitlement_dirty_watch_task.start() ldb_alarm_task.start() squadron_stats_boundary_task.start() points_alarm_task.start()