Changeset 14f54a7 in Klonkt


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@…>

Files:
5 edited

Legend:

Unmodified
Added
Removed
  • CHANGELOG.de.md

    r6dd1a3f r14f54a7  
    99- **Boosts, die ihr Cover verloren hatten, bekommen es automatisch zurück.**
    1010  Beiträge, die du vor dem Cover-Fix geboostet hast, waren ohne Artwork
    11   gespeichert; sie werden beim nächsten Neustart einmalig aufgefrischt. Einen
    12   Beitrag erneut zu boosten aktualisiert jetzt ebenfalls die gespeicherte Kopie
    13   (Cover, Inhalt) — ein veralteter Boost lässt sich also auch von Hand heilen.
     11  gespeichert; sie werden beim nächsten Neustart einmalig aufgefrischt. Ist der
     12  Heimserver eines Beitrags in dem Moment kurz nicht erreichbar, wird es bei den
     13  nächsten Neustarts erneut versucht statt endgültig übersprungen. Einen Beitrag
     14  erneut zu boosten aktualisiert jetzt ebenfalls die gespeicherte Kopie (Cover,
     15  Inhalt) — aus dem Feed wie von der Interact-Seite.
    1416
    1517## [1.3.3] — 2026-07-03
  • CHANGELOG.md

    r6dd1a3f r14f54a7  
    99- **Boosts that lost their cover get it back automatically.** Posts you boosted
    1010  before the cover fix were cached without their artwork; they are refreshed
    11   once on the next restart. Boosting a post again now also refreshes its cached
    12   copy (cover, content), so a stale boost can always be healed by hand too.
     11  once on the next restart. If a post's home server is briefly unreachable at
     12  that moment, it is retried on the next restarts instead of being skipped for
     13  good. Boosting a post again now also refreshes its cached copy (cover,
     14  content) — from the feed as well as the interact page.
    1315
    1416## [1.3.3] — 2026-07-03
  • CHANGELOG.nl.md

    r6dd1a3f r14f54a7  
    99- **Boosts die hun cover kwijt waren krijgen 'm automatisch terug.** Posts die
    1010  je vóór de cover-fix boostte waren zonder artwork opgeslagen; die worden bij
    11   de volgende herstart eenmalig ververst. Een post opnieuw boosten ververst nu
    12   ook de opgeslagen kopie (cover, inhoud), dus een oude boost is altijd ook
    13   handmatig te genezen.
     11  de volgende herstart eenmalig ververst. Is de thuis-server van een post op dat
     12  moment even onbereikbaar, dan wordt het bij volgende herstarts opnieuw
     13  geprobeerd in plaats van voorgoed overgeslagen. Een post opnieuw boosten
     14  ververst nu ook de opgeslagen kopie (cover, inhoud) — vanuit de feed én de
     15  interact-pagina.
    1416
    1517## [1.3.3] — 2026-07-03
  • src/routes/posts.js

    r6dd1a3f r14f54a7  
    867867    on = !ActivityPubService.getTimelineReaction(site.slug, note).boosted;
    868868    try { await ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    869     if (on) ActivityPubService.markBoosted(site.slug, note); else ActivityPubService.unmarkBoosted(site.slug, note);
     869    if (on) {
     870      ActivityPubService.markBoosted(site.slug, note); // instant UI state
     871      // Fire-and-forget: re-resolve the note so the cached row is refreshed
     872      // (cover/content) — boosting again heals a stale copy from EVERY boost
     873      // path, not just the interact page.
     874      ActivityPubService.resolveRemoteNote(note)
     875        .then((n) => { if (n) ActivityPubService.upsertBoostedNote(site.slug, n); })
     876        .catch(() => { /* best-effort */ });
     877    } else {
     878      ActivityPubService.unmarkBoosted(site.slug, note);
     879    }
    870880  }
    871881  if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
  • 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.