Summary
On a multi-threaded Harper cluster, a deployed Next.js app (no prebuilt) fails to serve — every route 404s. system.log shows the on-startup build failing in a worker thread:
Error building Next.js application <app>: Error: ENOENT: no such file or directory,
errno: -2, code: 'ENOENT', syscall: 'open',
path: '.../components/<app>/.next/BUILD_ID'
process.exit(1) called in worker thread 3 — ignored to keep Harper alive
The build worker exits 1, the plugin's catch marks the build "failure" and returns without calling serve() (src/plugin.ts ~L256–264), so nothing serves.
Root cause: concurrent next build across worker threads
handleApplication runs in every Harper worker thread, and the build dedup in build() (src/plugin.ts L267–286) is a post-hoc check, not a pre-build lock:
const buildInfo = await databases.harperfast_nextjs.nextjs_build_info.get(scope.appName);
if (buildInfo && Date.now() - buildInfo.getUpdatedTime() < 5000) {
if (buildInfo.status === 'failure') return;
if (buildInfo.status === 'success') { /* skip if BUILD_ID matches */ }
}
// otherwise fall through and build…
await next.build(…) // L303/314/326
const buildId = readFileSync(join(dir, '.next', 'BUILD_ID')) // L338–339 ← throws ENOENT
On a fresh deploy/restart there is no (or a >5s stale) build-info record, so all worker threads fall through and run next.build() concurrently into the same .next. next build clears/rewrites .next at the start, so one worker deletes .next/BUILD_ID while another reads it → ENOENT → exit(1) → no serve. There is no "claim building → others wait" step, so simultaneous starts (the common case) are never serialized.
@harperfast/vite already solves this — port its lock
The sibling plugin has src/buildLock.ts → withBuildLock(scope, build): a worker claims { status: 'building' } in the shared vite_build_info table before compiling; other workers observe the fresh claim and poll-wait, then skip (only one worker ever builds), with a stale-claim timeout for crashed builds. Its comment even says it "mirrors the @harperfast/nextjs build-info pattern" — but the nextjs pattern lacks the pre-build claim + wait, so it doesn't actually serialize concurrent starts. Adopting the same claim-then-wait lock in build() here should fix it.
Reproduce
npm create harper@latest my-app --template nextjs (the create-harper Next.js templates).
- Deploy to a multi-threaded cluster (no prebuilt):
harper deploy_component . restart=true replicated=true.
- Every route 404s;
system.log (System log, not hdb.log) shows the ENOENT above.
- Workarounds that confirm the cause:
THREADS_COUNT=1 (no concurrency) serves fine, and prebuilt: true + a shipped .next (no server build) serves fine.
Env: @harperfast/nextjs 2.2.1, next 16.2.11, harper 5.1.22, multi-thread cluster.
Related build issues: #51 (Turbopack can't resolve import 'harper'), #37 (build vs. RocksDB lock).
Summary
On a multi-threaded Harper cluster, a deployed Next.js app (no
prebuilt) fails to serve — every route 404s.system.logshows the on-startup build failing in a worker thread:The build worker exits 1, the plugin's catch marks the build "failure" and returns without calling
serve()(src/plugin.ts~L256–264), so nothing serves.Root cause: concurrent
next buildacross worker threadshandleApplicationruns in every Harper worker thread, and the build dedup inbuild()(src/plugin.tsL267–286) is a post-hoc check, not a pre-build lock:On a fresh deploy/restart there is no (or a >5s stale) build-info record, so all worker threads fall through and run
next.build()concurrently into the same.next.next buildclears/rewrites.nextat the start, so one worker deletes.next/BUILD_IDwhile another reads it →ENOENT→exit(1)→ no serve. There is no "claim building → others wait" step, so simultaneous starts (the common case) are never serialized.@harperfast/vitealready solves this — port its lockThe sibling plugin has
src/buildLock.ts→withBuildLock(scope, build): a worker claims{ status: 'building' }in the sharedvite_build_infotable before compiling; other workers observe the fresh claim and poll-wait, then skip (only one worker ever builds), with a stale-claim timeout for crashed builds. Its comment even says it "mirrors the@harperfast/nextjsbuild-info pattern" — but the nextjs pattern lacks the pre-build claim + wait, so it doesn't actually serialize concurrent starts. Adopting the same claim-then-wait lock inbuild()here should fix it.Reproduce
npm create harper@latest my-app --template nextjs(the create-harper Next.js templates).harper deploy_component . restart=true replicated=true.system.log(System log, not hdb.log) shows the ENOENT above.THREADS_COUNT=1(no concurrency) serves fine, andprebuilt: true+ a shipped.next(no server build) serves fine.Env:
@harperfast/nextjs2.2.1,next16.2.11,harper5.1.22, multi-thread cluster.Related build issues: #51 (Turbopack can't resolve
import 'harper'), #37 (build vs. RocksDB lock).