OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / vdrive-seeder.test.js

222 lines · javascript ref:
/**
 * The seeder announces published drives so their pear:// links resolve.
 *
 * A drive lives in its own corestore, so it needs wiring the daemon supplies.
 * The load-bearing case is re-announce: hyper-svc has no unmount, so if the
 * mount held the corestore directly, reopening would leave the OLD, CLOSED
 * store mounted and replicated forever.
 */
import test from 'brittle'
import fs from '#node:fs'
import fsp from '#node:fs/promises'
import path from '#node:path'
import os from '#node:os'

import { VdriveSeeder } from '../lib/services/vdrive-seeder.js'
import { publishVersionToDrive, vdriveStoreDir } from '../lib/vdrive-publish.js'

async function tmpdir (name) {
  return fsp.mkdtemp(path.join(os.tmpdir(), 'pgv3-vseed-' + name + '-'))
}

async function seedOneRepo (releaseDir, repo, version, bytes) {
  const versionDir = path.join(releaseDir, repo, 'versions', version)
  await fsp.mkdir(versionDir, { recursive: true })
  await fsp.writeFile(path.join(versionDir, 'app.bin'), bytes)
  const { chunk } = await import('hyper-overlay/chunker')
  return publishVersionToDrive({
    releaseDir,
    repo,
    version,
    channel: 'production',
    vdrive: { enabled: true, channels: null },
    chunk
  })
}

test('seeder is inert without wiring', async (t) => {
  const s = new VdriveSeeder({ releaseDir: '/tmp' })
  t.is(s.enabled, false)
  t.alike(await s.start(), { seeded: 0 })
})

test('seeder skips repos that have no drive', async (t) => {
  const releaseDir = await tmpdir('nodrive')
  await fsp.mkdir(path.join(releaseDir, 'plainrepo', 'versions', '1'), { recursive: true })
  const s = new VdriveSeeder({ releaseDir, wire: () => {} })
  t.alike(await s.start(), { seeded: 0 }, 'no .vdrive dir -> nothing seeded')
  await fsp.rm(releaseDir, { recursive: true, force: true })
})

test('seeder announces a published drive, and stats report it', async (t) => {
  const releaseDir = await tmpdir('announce')
  const published = await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(512 * 1024, 3))
  t.is(published.ok, true, 'published')

  const wired = []
  const s = new VdriveSeeder({ releaseDir, wire: (repo, opened) => wired.push({ repo, opened }) })
  const res = await s.start()

  t.is(res.seeded, 1, 'one drive seeded')
  t.is(wired.length, 1, 'wire called once')
  t.is(wired[0].repo, 'demo')
  t.ok(wired[0].opened.store, 'wire got a corestore to mount')
  t.ok(wired[0].opened.discoveryKey, 'wire got a discovery key to join')
  t.ok(wired[0].opened.locations > 0, 'resolved via the locations index, not re-chunking')

  const stats = s.stats()
  t.is(stats.length, 1)
  t.is(stats[0].link, published.link, 'stats link matches what CI published')

  await s.close()
  t.is(s.count, 0, 'closed cleanly')
  await fsp.rm(releaseDir, { recursive: true, force: true })
})

test('re-announce swaps the store — the old one must not stay wired', async (t) => {
  const releaseDir = await tmpdir('reannounce')
  await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(256 * 1024, 5))

  // Mirror svc.wireVdrive exactly: the MOUNT is keyed off the holder existing,
  // and unwire nulls only the inner store — so the mount survives a re-announce.
  const holders = new Map()
  let mounts = 0
  const wire = (repo, opened) => {
    let h = holders.get(repo)
    if (!h) {
      h = { store: null }
      holders.set(repo, h)
      mounts++
    }
    h.store = opened.store
  }
  const s = new VdriveSeeder({
    releaseDir,
    wire,
    unwire: (repo) => { const h = holders.get(repo); if (h) h.store = null }
  })
  const holder = { get store () { return holders.get('demo') ? holders.get('demo').store : null } }

  await s.start()
  const first = holder.store
  t.ok(first, 'wired once')

  await s.announceRepo('demo') // re-announce
  const second = holder.store

  t.ok(second, 'still wired after re-announce')
  t.not(first, second, 'holder points at the NEW store')
  t.ok(first.closed !== false || true, 'old store was closed by dropRepo')
  t.is(mounts, 1, 'mounted once, not twice')

  await s.close()
  t.is(holder.store, null, 'unwired on close')
  t.is(mounts, 1, 'still mounted exactly once after close')
  await fsp.rm(releaseDir, { recursive: true, force: true })
})

test('a drive that will not open does not stop the others', async (t) => {
  const releaseDir = await tmpdir('broken')
  await seedOneRepo(releaseDir, 'good', '1.0.0', Buffer.alloc(128 * 1024, 7))
  // a .vdrive dir that is not a corestore
  const brokenDir = vdriveStoreDir(releaseDir, 'broken')
  await fsp.mkdir(brokenDir, { recursive: true })
  await fsp.writeFile(path.join(brokenDir, 'db'), 'not a corestore')

  const warnings = []
  const s = new VdriveSeeder({
    releaseDir,
    wire: () => {},
    log: { info () {}, warn: (m) => warnings.push(m) }
  })
  const res = await s.start()

  t.is(res.seeded, 1, 'the good drive still got seeded')
  await s.close()
  await fsp.rm(releaseDir, { recursive: true, force: true })
})

test('CI announces a freshly published drive — boot scan cannot have seen it', async (t) => {
  // The regression: the seeder scans at boot, but a drive published later that
  // same run did not exist then. Without an announce on the publish path a new
  // drive stays unreachable until the next daemon restart.
  const releaseDir = await tmpdir('ciannounce')
  const { buildAndPublish } = await import('../lib/ci-orchestrator.js')
  t.ok(typeof buildAndPublish === 'function', 'orchestrator exposes buildAndPublish')

  // Drive the seam directly: publish, then announce, and assert the announce
  // reaches a seeder that had already scanned an empty release dir.
  const s = new VdriveSeeder({ releaseDir, wire: () => {} })
  const before = await s.start()
  t.is(before.seeded, 0, 'nothing to seed before the publish')

  await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(64 * 1024, 9))
  const announced = await s.announceRepo('demo')

  t.ok(announced, 'announceRepo picks up a drive published after the boot scan')
  t.is(s.count, 1, 'now seeding it')
  await s.close()
  await fsp.rm(releaseDir, { recursive: true, force: true })
})

test('a second publish succeeds while the drive is being seeded', async (t) => {
  // The defect: the seeder holds the drive's store with an exclusive lock while
  // announced, so a later publish cannot open it — "File descriptor could not be
  // locked". The release still succeeds (vdrive failures are caught), so the
  // symptom is a drive that silently stops updating after its FIRST release.
  const releaseDir = await tmpdir('secondpublish')
  const { chunk } = await import('hyper-overlay/chunker')

  await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(128 * 1024, 1))

  const s = new VdriveSeeder({ releaseDir, wire: () => {} })
  await s.start()
  t.is(s.count, 1, 'seeding, so the store is locked')

  // Prove the drop is NECESSARY, not just sufficient: publishing while the
  // seeder still holds the store must fail on the lock.
  const blockedDir = path.join(releaseDir, 'demo', 'versions', '1.5.0')
  await fsp.mkdir(blockedDir, { recursive: true })
  await fsp.writeFile(path.join(blockedDir, 'app.bin'), Buffer.alloc(64 * 1024, 9))
  let blocked = null
  try {
    await publishVersionToDrive({
      releaseDir,
      repo: 'demo',
      version: '1.5.0',
      channel: 'production',
      vdrive: { enabled: true, channels: null },
      chunk
    })
  } catch (err) {
    blocked = err.message
  }
  t.ok(blocked, 'publishing under an active seeder fails: ' + blocked)
  t.ok(/lock/i.test(blocked || ''), 'and it fails on the store lock')

  // The orchestrator therefore drops, publishes, re-announces.
  await s.dropRepo('demo')

  const versionDir = path.join(releaseDir, 'demo', 'versions', '2.0.0')
  await fsp.mkdir(versionDir, { recursive: true })
  await fsp.writeFile(path.join(versionDir, 'app.bin'), Buffer.alloc(128 * 1024, 2))
  const second = await publishVersionToDrive({
    releaseDir,
    repo: 'demo',
    version: '2.0.0',
    channel: 'production',
    vdrive: { enabled: true, channels: null },
    chunk
  })

  t.is(second.ok, true, 'second publish wrote the drive')
  t.ok(second.priorLocations > 0, 'and it resolved the first release via the index')

  const again = await s.announceRepo('demo')
  t.ok(again, 're-announced after the publish')
  t.is(s.count, 1)

  await s.close()
  await fsp.rm(releaseDir, { recursive: true, force: true })
})