Antispam: a dead engine marked the account bootstrapped anyway
`_teach` signalled failure by negating the count taught so far. That works for every case but the one that matters: when the FIRST message is refused nothing has been taught yet, and `-0` is `0` — not negative. The caller's `< 0` test then read a total engine outage as a clean run of zero, recorded `_bootstrapped` for the account, and the hourly sweep skipped it from then on. The account most in need of a warm start was the one guaranteed never to get one. Which is precisely what happened alongside the missing controller password: every learn was refused 401, and each account was quietly retired on its first sweep. Return None for "the engine stopped accepting these" so failure cannot be confused with a count, and decline to mark an account whose samples were all unreadable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
@@ -51,7 +51,15 @@ MAX_HAM_RATIO = 3
|
||||
_DONE_KEY = "_bootstrapped"
|
||||
|
||||
|
||||
async def _teach(user_id: uuid.UUID, ids: list[uuid.UUID], *, spam: bool) -> int:
|
||||
async def _teach(user_id: uuid.UUID, ids: list[uuid.UUID], *, spam: bool) -> int | None:
|
||||
"""Number of messages learned, or None if the engine stopped accepting them.
|
||||
|
||||
None rather than a negative count: the count was once negated to signal
|
||||
failure, which works for every case except the one that matters. If the
|
||||
FIRST message is refused, nothing has been taught yet, `-0` is `0`, and the
|
||||
caller's `< 0` test reads a total engine outage as a clean run of zero — then
|
||||
marks the account bootstrapped, so it is never retried.
|
||||
"""
|
||||
taught = 0
|
||||
for message_id in ids:
|
||||
raw = await _SDK.messages.raw(user_id, message_id)
|
||||
@@ -60,7 +68,7 @@ async def _teach(user_id: uuid.UUID, ids: list[uuid.UUID], *, spam: bool) -> int
|
||||
if not await rspamd.learn(raw, user_key=str(user_id), spam=spam):
|
||||
# Engine gone. Stop rather than grind through hundreds of failures;
|
||||
# the account stays unmarked, so a later run picks it up again.
|
||||
return -taught
|
||||
return None
|
||||
taught += 1
|
||||
await asyncio.sleep(PAUSE_SECONDS)
|
||||
return taught
|
||||
@@ -106,17 +114,24 @@ async def bootstrap_account(
|
||||
ham_ids = ham_ids[: len(spam_ids) * MAX_HAM_RATIO]
|
||||
|
||||
spam_taught = await _teach(uid, spam_ids, spam=True)
|
||||
if spam_taught < 0:
|
||||
if spam_taught is None:
|
||||
logger.warning(
|
||||
"antispam bootstrap for %s stopped: engine unavailable", account_id
|
||||
)
|
||||
return
|
||||
ham_taught = await _teach(uid, ham_ids, spam=False)
|
||||
if ham_taught < 0:
|
||||
if ham_taught is None:
|
||||
logger.warning(
|
||||
"antispam bootstrap for %s stopped: engine unavailable", account_id
|
||||
)
|
||||
return
|
||||
if spam_taught == 0:
|
||||
# Every sample was unreadable. Nothing was learned, so do not record a
|
||||
# warm start that did not happen.
|
||||
logger.warning(
|
||||
"antispam bootstrap for %s: no samples could be read", account_id
|
||||
)
|
||||
return
|
||||
|
||||
await _SDK.kv(uid).set(_DONE_KEY, [*done, account_id])
|
||||
logger.info(
|
||||
|
||||
Reference in New Issue
Block a user