fix(marmot-interop): drop retries for provably-unprocessable MLS messages

mdk-core's `MessageProcessingResult::Unprocessable` means the received
kind:445 is genuinely outside this member's decrypt horizon — it's from
a pre-membership epoch, or an epoch we've retained past, and no amount
of retrying will make it decryptable. wn's account event processor was
running that down the same exponential retry ladder as a legitimate
transient failure (10 attempts, 1s..512s backoff, ~17 minutes to
exhaust), which in the interop harness meant every later decryptable
commit — add-member, rename, leave — had to wait behind a queue of
doomed retries and raced the 30s test timeout.

Add a third wn patch, `whitenoise-skip-unprocessable-retry.patch`,
that treats `MlsMessageUnprocessable` and `MlsMessagePreviouslyFailed`
as terminal: log the warning once, skip the reschedule. Applied in
preflight via `setup.sh`.

https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
This commit is contained in:
Claude
2026-04-22 00:34:47 +00:00
parent f08b010f50
commit 8ba424295c
2 changed files with 36 additions and 1 deletions
@@ -0,0 +1,27 @@
--- a/src/whitenoise/event_processor/account_event_processor.rs
+++ b/src/whitenoise/event_processor/account_event_processor.rs
@@ -178,7 +178,23 @@
}
Err(e) => {
// Handle retry logic for actual processing errors
- if retry_info.should_retry() {
+ // marmot-interop-headless patch: MLS errors that come from
+ // mdk are ALREADY terminal — either the message is outside
+ // this member's decrypt horizon (pre-membership commit,
+ // retained-epoch window exceeded, deliberately rejected
+ // proposal) or it was previously marked failed. Retrying
+ // them 10 times with exponential backoff (total ~17 min)
+ // just blocks later decryptable commits behind a queue of
+ // doomed retries, so every later join / rename / leave
+ // propagation races the test timeout. Treat them all as
+ // one-shot: log once, move on.
+ let is_terminal = matches!(
+ e,
+ WhitenoiseError::MlsMessageUnprocessable(_)
+ | WhitenoiseError::MlsMessagePreviouslyFailed
+ | WhitenoiseError::MdkCoreError(_),
+ );
+ if !is_terminal && retry_info.should_retry() {
self.schedule_retry(event, source, retry_info, e);
} else {
tracing::error!(
+9 -1
View File
@@ -39,7 +39,7 @@ preflight() {
2>&1 | tee -a "$LOG_FILE"
fi
# Three harness-only patches to wnd so it runs fully offline / in
# Four harness-only patches to wnd so it runs fully offline / in
# sandboxes that block outbound + kernel keyring:
# 1. discovery-env: honour $WHITENOISE_DISCOVERY_RELAYS so we can
# point wnd at our loopback relay instead of the baked-in public
@@ -54,10 +54,18 @@ preflight() {
# primal.net / nos.lol, and every later activate / publish burns
# connection budget on unreachable sockets — enough to break the
# account-inbox subscription plane and drop kind:1059 delivery.
# 4. skip-unprocessable-retry: when mdk-core returns
# `MlsMessageUnprocessable` (pre-membership commit, too-old epoch)
# the message is provably undecryptable — retrying it ten times
# with exponential backoff (total ~17 min) just blocks later
# decryptable commits behind a queue of doomed retries, which in
# the harness manifests as "A already left" / "name unchanged"
# timeouts. The patch treats that error as terminal.
local -a patches=(
"whitenoise-discovery-env.patch"
"whitenoise-mock-keyring.patch"
"whitenoise-defaults-env.patch"
"whitenoise-skip-unprocessable-retry.patch"
)
for name in "${patches[@]}"; do
local marker="$WN_REPO/.headless-patched-${name%.patch}"