OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / lib / reindex-overlay.js

80 lines · javascript ref:
// reindex-overlay — OPERATOR REPAIR for a node whose hyper-overlay INDEX is missing or stale.
//
// hyper-overlay serves a content request by resolving oid -> disk path through IN-MEMORY maps that only
// registerFile() fills, and the persisted FileIndex stores {contentHash,size,mtime} with NO disk path.
// OverlayRepoStore.ready() re-seeds those maps from the persisted index at boot, which is the cheap
// normal path — but it can only re-seed what the index already knows. When the INDEX itself is
// impoverished (files written before overlay indexing existed, or an index rebuilt/lost), the only fix
// is to walk the files and re-register them. Measured on tabertron: 51 of ~131 record paths indexed.
//
// This is not cosmetic. Byte resolution GATES SIGNING — the forward-only sign gate skips while a
// converged pointer's bytes are unresolved — so an impoverished index silently stops a co-owner signing
// at all, reporting only "no connected peer served this oid". That cost 75+ hours on wj62.
//
// The capability shipped in atproto-store with NO trigger (no RPC, no CLI): unreachable. This is the
// trigger. It runs against the LIVE daemon over its owner-gated UDS, so no restart is needed.
//
// Covers records AND blobs (OverlayRepoStore.reindexOverlay walks records/ only, so a node repaired with
// that alone would resume serving records while silently still failing every blob), plus a backfill of
// shared blob pointers for locally-held blobs that were never announced.
//
// SAFETY: purely additive. Re-registration re-asserts identical index entries; the pointer backfill skips
// any cid a co-owner already published and never deletes. Nothing is removed or overwritten.
import fs from 'bare-fs'
import { Client } from 'hyper-svc'
import { loadConfig } from './config.js'
import { sockFor } from './control-socket.js' // shared with `diag` — one definition, no drift

async function reindexOne (sock, did) {
  const client = await Client.connectUds(sock)
  try {
    const r = await client.call('reindex-overlay', {})
    console.log(JSON.stringify({ did, ...r }))
    return r
  } finally { try { await client.close() } catch {} }
}

// args = CLI args AFTER the subcommand: ['did:plc:...'|'--all', '--storage', '<path>']. Returns exit code.
export async function reindexOverlayCommand (args = []) {
  const cfg = loadConfig()
  const sIdx = args.indexOf('--storage')
  const storage = sIdx >= 0 ? args[sIdx + 1] : cfg.storage
  const all = args.includes('--all')
  const one = args.find((a) => a.startsWith('did:'))
  const hosted = (cfg.dids || []).map((d) => d.did)

  if (!one && !all) {
    console.error('usage: hyper-proto-peer reindex-overlay <did>|--all [--storage <path>]')
    console.error('  Re-registers every record + blob FILE with the overlay so peers can fetch them by oid,')
    console.error('  and announces any local blob that has no shared pointer yet. Runs against the LIVE')
    console.error('  daemon over its UDS — no restart needed. Additive: nothing is deleted or overwritten.')
    console.error('  hosted DIDs:', JSON.stringify(hosted))
    return 1
  }

  const targets = all ? hosted : [one]
  if (!targets.length) { console.error('reindex-overlay: no hosted DIDs in config'); return 1 }

  let failed = 0
  for (const did of targets) {
    const sock = sockFor(cfg, storage, did)
    if (!fs.existsSync(sock)) {
      console.error('reindex-overlay: control socket not found at ' + sock)
      console.error('  Is the daemon running and hosting ' + did + '? (this repairs the LIVE store.)')
      failed++
      continue
    }
    try {
      await reindexOne(sock, did)
    } catch (e) {
      console.error('reindex-overlay: FAILED for ' + did + ': ' + e.message)
      failed++
    }
  }
  // `unknownMeta` counts blob files with no local metadata: registered (so the bytes are peer-fetchable)
  // but deliberately NOT given a guessed mimeType. They recover their mime when the shared pointer
  // replicates. `pointers.skipped: not-writable` is expected on a non-admitted device — peers converge
  // pointers by replication rather than publishing their own.
  return failed ? 1 : 0
}