Ignore:
Timestamp:
07/04/2026 05:31:45 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
6ec0433
Parents:
6dd1a3f
Message:

fix(ap): self-heal retries until a clean pass; feed re-boost also refreshes

Scale fix (think 1M instances, not this one row): the previous remedies
still had two gaps that only showed up per-user.

  • selfHealTimeline consumed its version even when some origins were unreachable at that exact moment - phone-hosted instances are offline routinely, so their notes stayed stale forever with no retry. The version is now only persisted after a CLEAN pass (0 unreachable); otherwise it retries on the next boots and gives up after 5 attempts (dead origins answer 404/410 and are pruned, so no infinite loop). SELFHEAL_VERSION 6 -> 7 so already-healed instances rerun with the new semantics.
  • The /news feed boost only did markBoosted, so "boost again to heal a stale copy" silently worked from the interact page but not the feed. Boosting from the feed now also re-resolves the note (fire-and-forget) and refreshes the cached row via upsertBoostedNote.

Retry state machine verified in isolation (offline-then-reachable ->
heals; permanently dead -> gives up after 5 passes, then skips). Suite
45/45.

  • src/services/ActivityPubService.js - clean-pass gating + attempts counter (app_settings, no schema change); SELFHEAL_VERSION 7
  • src/routes/posts.js - /news/boost refreshes the cached row on boost
  • CHANGELOG(.nl/.de).md - extended the Unreleased bullet (3 languages)

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r6dd1a3f r14f54a7  
    18781878// during a flux window, e.g. a fleet-wide update), and drops notes that are gone
    18791879// (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync.
    1880 const SELFHEAL_VERSION = 6; // v6: re-fetch so boosted notes cached coverless (pre image-fallback in resolveRemoteNote) pick up their cover
     1880const SELFHEAL_VERSION = 7; // v7: rerun v6 with retry-until-clean semantics (origins briefly offline no longer stay stale forever)
    18811881async function fetchNoteAP(url) {
    18821882  try {
     
    20522052    let rows = [];
    20532053    try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }
    2054     let healed = 0;
     2054    let healed = 0, failed = 0;
    20552055    for (const r of rows) {
    20562056      try {
    20572057        const note = await fetchNoteAP(r.id);
    20582058        if (note === 404) { db.prepare('DELETE FROM ap_timeline WHERE id = ?').run(r.id); healed++; continue; }
    2059         if (!note || typeof note !== 'object') continue;
     2059        if (!note || typeof note !== 'object') { failed++; continue; } // origin unreachable right now
    20602060        const html = HtmlSanitizerService.sanitize(note.content || '');
    20612061        const media = mediaFromNote(note);
     
    20672067          healed++;
    20682068        }
    2069       } catch { /* per-note best-effort */ }
    2070     }
    2071     try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run('selfheal_version', String(SELFHEAL_VERSION)); } catch { /* ignore */ }
    2072     if (rows.length) console.log(`[AP] self-heal v${SELFHEAL_VERSION}: ${healed}/${rows.length} timeline notes`);
     2069      } catch { failed++; /* per-note best-effort */ }
     2070    }
     2071    // Only mark this version DONE after a clean pass. Some origins are briefly
     2072    // offline exactly when we heal (phone-hosted instances!): skipping them and
     2073    // consuming the version would leave those rows stale forever. Instead retry
     2074    // on the next boots, giving up after a few attempts (permanently-dead
     2075    // origins answer 404/410 and are deleted above, so they don't loop).
     2076    const setSetting = (k, v) => { try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run(k, String(v)); } catch { /* ignore */ } };
     2077    let attempts = 0;
     2078    try { const a = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('selfheal_attempts'); attempts = a ? (parseInt(a.value, 10) || 0) : 0; } catch { /* ignore */ }
     2079    if (failed === 0 || attempts >= 4) {
     2080      setSetting('selfheal_version', SELFHEAL_VERSION);
     2081      setSetting('selfheal_attempts', 0);
     2082    } else {
     2083      setSetting('selfheal_attempts', attempts + 1);
     2084    }
     2085    if (rows.length) console.log(`[AP] self-heal v${SELFHEAL_VERSION}: ${healed}/${rows.length} timeline notes${failed ? ` (${failed} unreachable — will retry next boot)` : ''}`);
    20732086  } catch { /* never block boot */ } finally { _selfHealing = false; }
    20742087}
Note: See TracChangeset for help on using the changeset viewer.