mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-05 20:52:22 +01:00
Merge branch 'dev' into review/pewds-pr6738-terra-20261027
Integrate PR #6739 foundation (backend security, bridge hardening, Python 3.11 test fixture correction) with PR #6738 remediations. - Reconcile routes/email/email_routes.py harmonizing _LIST_LOCK and _LIST_EPOCH with #6738 cache invalidation and thread-safe mutation. - Preserve editor-session validation, legacy-browser tool restrictions, image-tool authorization, and email cache concurrency invariants.
This commit is contained in:
@@ -1827,21 +1827,25 @@ def setup_email_routes():
|
||||
# a cached message body.
|
||||
return (account_id or "", folder, str(uid), owner)
|
||||
|
||||
def _list_cache_get(key):
|
||||
v = _LIST_CACHE.get(key)
|
||||
if not v: return None
|
||||
if v[0] < _time.monotonic():
|
||||
_LIST_CACHE.pop(key, None)
|
||||
return None
|
||||
return v[1]
|
||||
|
||||
_LIST_STALE = {}
|
||||
_LIST_CACHE_LOCK = _threading.RLock()
|
||||
_LIST_LOCK = _threading.RLock()
|
||||
_LIST_CACHE_LOCK = _LIST_LOCK
|
||||
# Invalidation fences reads already in flight, including background refresh.
|
||||
_LIST_CACHE_GENERATION = [0]
|
||||
_LIST_EPOCH = [0]
|
||||
_LIST_CACHE_GENERATION = _LIST_EPOCH
|
||||
|
||||
def _list_cache_get(key):
|
||||
with _LIST_LOCK:
|
||||
v = _LIST_CACHE.get(key)
|
||||
if not v: return None
|
||||
if v[0] < _time.monotonic():
|
||||
_LIST_CACHE.pop(key, None)
|
||||
return None
|
||||
return v[1]
|
||||
|
||||
def _list_stale_get(key):
|
||||
return _LIST_STALE.get(key)
|
||||
with _LIST_LOCK:
|
||||
return _LIST_STALE.get(key)
|
||||
|
||||
def _list_stale_put(key, value):
|
||||
_LIST_STALE[key] = value
|
||||
@@ -1852,8 +1856,9 @@ def setup_email_routes():
|
||||
_LIST_REFRESHING = set()
|
||||
|
||||
def _list_cache_put(key, value):
|
||||
with _LIST_CACHE_LOCK:
|
||||
with _LIST_LOCK:
|
||||
_LIST_CACHE[key] = (_time.monotonic() + _LIST_TTL, value)
|
||||
# Also snapshot the last-good response for stale-while-revalidate.
|
||||
_list_stale_put(key, value)
|
||||
if len(_LIST_CACHE) > 64:
|
||||
for k in list(_LIST_CACHE.keys())[:-32]:
|
||||
@@ -1865,18 +1870,18 @@ def setup_email_routes():
|
||||
Runs the blocking IMAP fetch in a daemon thread, then repopulates both
|
||||
the TTL cache and the stale snapshot so the next list view is warm.
|
||||
"""
|
||||
with _LIST_CACHE_LOCK:
|
||||
with _LIST_LOCK:
|
||||
if cache_key in _LIST_REFRESHING:
|
||||
return
|
||||
_LIST_REFRESHING.add(cache_key)
|
||||
generation = _LIST_CACHE_GENERATION[0]
|
||||
epoch = _LIST_EPOCH[0]
|
||||
|
||||
def _run():
|
||||
try:
|
||||
refreshed = _list_emails_sync(*sync_args)
|
||||
if refreshed and not refreshed.get("error"):
|
||||
with _LIST_CACHE_LOCK:
|
||||
if generation != _LIST_CACHE_GENERATION[0]:
|
||||
with _LIST_LOCK:
|
||||
if epoch != _LIST_EPOCH[0]:
|
||||
return
|
||||
_list_cache_put(cache_key, refreshed)
|
||||
try:
|
||||
@@ -1887,7 +1892,7 @@ def setup_email_routes():
|
||||
except Exception as exc: # pragma: no cover - background best effort
|
||||
logger.debug("email list background refresh failed: %s", exc)
|
||||
finally:
|
||||
with _LIST_CACHE_LOCK:
|
||||
with _LIST_LOCK:
|
||||
_LIST_REFRESHING.discard(cache_key)
|
||||
|
||||
_threading.Thread(target=_run, daemon=True, name="email-list-refresh").start()
|
||||
@@ -1925,10 +1930,14 @@ def setup_email_routes():
|
||||
hook(account_id, folder)
|
||||
except Exception:
|
||||
pass
|
||||
with _LIST_CACHE_LOCK:
|
||||
_LIST_CACHE_GENERATION[0] += 1
|
||||
with _LIST_LOCK:
|
||||
_LIST_EPOCH[0] += 1
|
||||
if account_id is None and folder is None:
|
||||
_LIST_CACHE.clear()
|
||||
_LIST_STALE.clear()
|
||||
return
|
||||
for cache in (_LIST_CACHE, _LIST_STALE):
|
||||
for k in list(cache):
|
||||
for k in list(cache.keys()):
|
||||
k_acct = k[0] if len(k) > 0 else ""
|
||||
k_folder = k[1] if len(k) > 1 else ""
|
||||
if (account_id is None or k_acct == (account_id or "")) and \
|
||||
@@ -1936,15 +1945,60 @@ def setup_email_routes():
|
||||
cache.pop(k, None)
|
||||
|
||||
def _index_update_flags(owner, account_id, folder, uid, flag, add):
|
||||
with _LIST_CACHE_LOCK:
|
||||
with _LIST_LOCK:
|
||||
_email_index_update_flags(owner, account_id, folder, uid, flag, add)
|
||||
_invalidate_list_cache(account_id, folder)
|
||||
|
||||
def _index_delete(owner, account_id, folder, uid):
|
||||
with _LIST_CACHE_LOCK:
|
||||
with _LIST_LOCK:
|
||||
_email_index_delete(owner, account_id, folder, uid)
|
||||
_invalidate_list_cache(account_id, folder)
|
||||
|
||||
def _update_list_cache_seen(account_id, folder, uid, seen: bool):
|
||||
uid_s = str(uid)
|
||||
with _LIST_LOCK:
|
||||
for key, (expires_at, value) in list(_LIST_CACHE.items()):
|
||||
if key[0] != (account_id or "") or key[1] != folder:
|
||||
continue
|
||||
emails = list((value or {}).get("emails") or [])
|
||||
changed = False
|
||||
if seen and key[2] == "unread":
|
||||
kept = [e for e in emails if str((e or {}).get("uid") or "") != uid_s]
|
||||
if len(kept) != len(emails):
|
||||
value = dict(value)
|
||||
value["emails"] = kept
|
||||
value["total"] = max(0, int(value.get("total") or 0) - (len(emails) - len(kept)))
|
||||
changed = True
|
||||
else:
|
||||
for e in emails:
|
||||
if str((e or {}).get("uid") or "") == uid_s:
|
||||
e["is_read"] = bool(seen)
|
||||
changed = True
|
||||
if changed:
|
||||
_LIST_CACHE[key] = (expires_at, value)
|
||||
if key in _LIST_STALE:
|
||||
_LIST_STALE[key] = value
|
||||
|
||||
for key, value in list(_LIST_STALE.items()):
|
||||
if key[0] != (account_id or "") or key[1] != folder:
|
||||
continue
|
||||
emails = list((value or {}).get("emails") or [])
|
||||
changed = False
|
||||
if seen and key[2] == "unread":
|
||||
kept = [e for e in emails if str((e or {}).get("uid") or "") != uid_s]
|
||||
if len(kept) != len(emails):
|
||||
value = dict(value)
|
||||
value["emails"] = kept
|
||||
value["total"] = max(0, int(value.get("total") or 0) - (len(emails) - len(kept)))
|
||||
changed = True
|
||||
else:
|
||||
for e in emails:
|
||||
if str((e or {}).get("uid") or "") == uid_s:
|
||||
e["is_read"] = bool(seen)
|
||||
changed = True
|
||||
if changed:
|
||||
_LIST_STALE[key] = value
|
||||
|
||||
def _read_cache_get(key):
|
||||
v = _READ_CACHE.get(key)
|
||||
if not v: return None
|
||||
|
||||
Reference in New Issue
Block a user