OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-sidecar / test / deploy-index-persistence.test.js

105 lines · javascript ref:
// Persistent DeployServer fileIndex (root-cause fix for expensive cold-boot
// re-scan + fragile tree serving across restarts).
//
// hyper-deploy's DeployServer defaults to a fresh os.tmpdir() corestore on
// every open, so it re-hashes the ENTIRE served dir on every boot and only
// serves trees indexed by the current process. pgh-sidecar now passes a
// PERSISTENT corestore (opts.corestore) so the fileIndex survives restarts.
//
// This proves the mechanism the fix relies on:
//   1. After a first open, trees are registered (getTree returns entries).
//   2. Reopening with the SAME persistent corestore keeps trees registered
//      (durable across "restart") AND reuses cached hashes instead of
//      re-hashing (scan-cache reports hits, not all-misses).
// Contrast test: an EPHEMERAL (fresh) corestore re-hashes everything (all
// misses) on the second open — the pathology the fix removes.

import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Corestore from 'corestore'
import { DeployServer } from 'hyper-deploy'

let n = 0
function tmpDir (tag) {
  const d = path.join(os.tmpdir(), 'pgh-deployidx-' + tag + '-' + (n++) + '-' + Math.floor(os.uptime() * 1000))
  fs.mkdirSync(d, { recursive: true })
  return d
}

// Build a small content-addressed-ish served dir: a couple of "tree" subdirs
// with a few files each, enough that a re-hash is observable as misses.
function makeContentDir () {
  const dir = tmpDir('content')
  for (const treeName of ['aaaa', 'bbbb']) {
    const td = path.join(dir, treeName)
    fs.mkdirSync(path.join(td, 'lib'), { recursive: true })
    fs.writeFileSync(path.join(td, 'index.js'), '// ' + treeName + '\nmodule.exports = ' + JSON.stringify(treeName) + '\n')
    fs.writeFileSync(path.join(td, 'package.json'), JSON.stringify({ name: treeName, version: '1.0.0' }) + '\n')
    fs.writeFileSync(path.join(td, 'lib', 'a.js'), 'exports.a = 1\n'.repeat(50))
  }
  return dir
}

async function openServer (contentDir, storeDir) {
  const store = new Corestore(storeDir)
  const stats = { hits: 0, misses: 0 }
  const srv = new DeployServer(contentDir, { corestore: store, swarm: false, topic: Buffer.alloc(32, 1) })
  srv.on('scan-cache-stats', (s) => { stats.hits = s.hits; stats.misses = s.misses })
  await srv.ready()
  return { srv, store, stats }
}

test('persistent store: trees survive a restart and are not re-hashed', async (t) => {
  t.timeout(60000)
  const contentDir = makeContentDir()
  const storeDir = tmpDir('persist-store')

  // --- First open (cold): indexes + registers trees ---
  const a = await openServer(contentDir, storeDir)
  t.ok(a.srv.manifest, 'first open produced a manifest')
  const rootHash = a.srv.manifest.rootTreeHash
  t.ok(rootHash, 'root tree hash was published')
  const treeA = await a.srv._fileIndex.getTree(rootHash)
  t.ok(treeA && treeA.entries && treeA.entries.length > 0, 'getTree(root) returns entries after first open')
  t.is(a.stats.hits, 0, 'first (cold) open: no cache hits — everything hashed')
  t.ok(a.stats.misses > 0, 'first open hashed files (misses > 0)')
  await a.srv.close()
  await a.store.close() // DeployServer doesn't own a passed-in store — release the lock (this is why the sidecar's close() closes it explicitly)

  // --- Reopen with the SAME persistent store (simulates a daemon restart) ---
  const b = await openServer(contentDir, storeDir)
  const treeAafter = await b.srv._fileIndex.getTree(rootHash)
  t.ok(treeAafter && treeAafter.entries && treeAafter.entries.length > 0,
    'getTree(root) STILL returns entries after restart (durable serving)')
  t.ok(b.stats.hits > 0,
    'restart with persistent store REUSED cached hashes (hits > 0) — no full re-hash')
  await b.srv.close()
  await b.store.close()

  t.teardown(async () => {
    try { fs.rmSync(contentDir, { recursive: true, force: true }) } catch {}
    try { fs.rmSync(storeDir, { recursive: true, force: true }) } catch {}
  })
})

test('contrast: ephemeral (fresh) store re-hashes everything on second open', async (t) => {
  t.timeout(60000)
  const contentDir = makeContentDir()

  const a = await openServer(contentDir, tmpDir('ephem-1'))
  t.ok(a.stats.misses > 0, 'first open hashed files')
  await a.srv.close()
  await a.store.close()

  // Different store dir = the old ephemeral behavior.
  const b = await openServer(contentDir, tmpDir('ephem-2'))
  t.is(b.stats.hits, 0, 'fresh store: zero cache hits (re-hashes everything) — the pathology the fix removes')
  t.ok(b.stats.misses > 0, 'fresh store re-hashed all files')
  await b.srv.close()
  await b.store.close()

  t.teardown(async () => { try { fs.rmSync(contentDir, { recursive: true, force: true }) } catch {} })
})