Use dirty bit instead of drop for async ring reforge debounce

When a reforge is already running, set a dirty flag so the thread
loops once more after finishing — picks up latest product state
from rapid saves without queueing multiple reforges.
This commit is contained in:
russell@unturf.com 2026-02-09 13:55:00 -05:00
parent 3ba8802d9a
commit bda4d39858

View file

@ -692,45 +692,57 @@ def reforge_discovery_ring(shop):
return ring
# Tracks which shops have a background reforge in progress.
_reforge_locks = {}
_reforge_locks_guard = threading.Lock()
# Tracks which shops have a background reforge in progress and whether
# another reforge was requested while one was already running (dirty bit).
_reforge_running = {} # shop_id_str -> True while a thread is active
_reforge_dirty = {} # shop_id_str -> True if another trigger arrived mid-run
_reforge_guard = threading.Lock()
def reforge_discovery_ring_async(shop_id, session_factory):
"""Reforge the discovery ring in a background thread.
Uses the old ring until the new one is ready. If a reforge is already
running for this shop, the call is a no-op (the in-flight reforge will
pick up the latest products).
running for this shop, sets a dirty bit so the thread runs once more
after it finishes (picking up the latest product state). Never queues
more than one pending reforge.
"""
shop_id_str = str(shop_id)
with _reforge_locks_guard:
if shop_id_str in _reforge_locks:
return # already running, skip
_reforge_locks[shop_id_str] = True
with _reforge_guard:
if shop_id_str in _reforge_running:
_reforge_dirty[shop_id_str] = True
return
_reforge_running[shop_id_str] = True
def _reforge():
try:
from sqlalchemy.orm import Session
session = Session(bind=session_factory().get_bind())
try:
shop = session.get(Shop, shop_id)
if shop is None:
return
ring = compute_discovery_ring(shop)
shop.discovery_ring = ring
session.commit()
log.info("Ring reforged for shop %s (%d products)", shop.name, len(ring))
while True:
shop = session.get(Shop, shop_id)
if shop is None:
return
ring = compute_discovery_ring(shop)
shop.discovery_ring = ring
session.commit()
log.info("Ring reforged for shop %s (%d products)", shop.name, len(ring))
with _reforge_guard:
if _reforge_dirty.pop(shop_id_str, False):
session.expire_all()
continue # dirty bit set, run again
break
except Exception:
session.rollback()
log.exception("Background ring reforge failed for shop %s", shop_id_str)
finally:
session.close()
finally:
with _reforge_locks_guard:
_reforge_locks.pop(shop_id_str, None)
with _reforge_guard:
_reforge_running.pop(shop_id_str, None)
_reforge_dirty.pop(shop_id_str, None)
t = threading.Thread(target=_reforge, daemon=True)
t.start()