OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 commits / f911f91

0.1.216: debounce + serialize the org-room repo re-sweep
The org-room update handler was fire-and-forget: const onUpdate = () => { mountOrgGroupedRepos(orgName, deps).catch(() => {}) } room.base.on('update', onUpdate) so every view advance started ANOTHER full sweep of that org's repos while earlier sweeps were still in flight. The `mounted` Set dedups the mount ACTION but not the sweep: each pass still walks the org's repo list calling ensureFor(), which opens a per-repo tracker autobase and runs a reconcile. On an ORIGIN this is harmless — org rooms only advance when someone writes. On a MIRROR every org room is a replication target, so during catch-up all 11 advance continuously and sweeps stack without bound. Measured on stortron immediately after 0.1.215: 294 org-mount lines in a single boot against a normal 117 (the same count tabertron produces), daemon pinned at ~134% CPU, boot never reaching its banner and never accept()ing a connection. 0.1.215's announce backoff never fired that boot ("retrying in" count 0) because the announce is downstream of this and was never reached — i.e. this is a distinct re-entry path, not a regression of the last fix. Fix mirrors 0.1.215: a `sweeping` in-flight flag with a `resweep` coalescing flag (so an update during a sweep queues exactly one follow-up rather than a new concurrent sweep), behind a 1500ms debounce timer. The timer is the load-bearing part — setTimeout is a MACROTASK, so the loop is handed back between sweeps. Awaiting alone drains only the microtask queue and still starves accept()/SIGCHLD (0.1.180, 0.1.214, 0.1.215). 1500ms matches attachSshResyncTrigger and scheduleMirrorReannounce so every autobase-update-driven path in the daemon now coalesces on one interval. Tests: org-grouped-repos, org-event-log, migrate-orgs, migrate-real-orgs, migrate-org-issues-linked-core all pass; lib/handlers/orgs.js imports clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f911f9143a772d712725c4a9b70d9a3c2be087a9
by plankatron <fleeky@gmail.com> · 2026-08-09T03:42:57.000Z

Changed files (2) +53 -2

lib/handlers/orgs.js +52 -1
……@@ -11,6 +11,12 @@ import { resolveUserId } from '../auth/user-resolver.js'
1111import { mountOrgGroupedRepos } from '../services/org-grouped-repos.js'
1212import { assertValidRepoName } from '../repo-name.js'
1313
14// Debounce for the org-room re-sweep. Matches attachSshResyncTrigger and
15// scheduleMirrorReannounce so every autobase-update-driven path in the daemon
16// coalesces on the same interval. See autoMountOrgRepos for why this must be a
17// timer (macrotask) and not a bare await.
18const ORG_RESWEEP_DEBOUNCE_MS = 1500
19
1420export function orgHandlers (svc) {
1521 const orgs = () => svc.modules.orgs
1622 const registry = () => svc.identityRegistry
……@@ -130,7 +136,52 @@ export function orgHandlers (svc) {
130136 const room = orgs().getOrgRoom(orgName)
131137 if (room && room.base && typeof room.base.on === 'function' && !svc._orgRepoWatchers.has(orgName)) {
132138 svc._orgRepoWatchers.add(orgName)
133 const onUpdate = () => { mountOrgGroupedRepos(orgName, deps).catch(() => {}) }
139
140 // DEBOUNCED + SERIALIZED. The previous handler was fire-and-forget, so
141 // every org-room view advance started ANOTHER full sweep of that org's
142 // repos while earlier sweeps were still running. `mounted` dedups the
143 // mount ACTION, but not the sweep: each pass still walks the org's repo
144 // list calling ensureFor(), which opens a per-repo tracker autobase and
145 // runs a reconcile.
146 //
147 // On an ORIGIN this is harmless — org rooms only advance when someone
148 // writes. On a MIRROR every org room is a replication target, so during
149 // catch-up all 11 advance continuously and sweeps stack without bound.
150 // Measured on stortron: 294 org-mount lines in one boot against a normal
151 // 117, with the daemon pinned at 100% CPU and boot never reaching its
152 // banner.
153 //
154 // setTimeout is deliberate, not cosmetic: it is a MACROTASK, so the loop
155 // is handed back between sweeps. Awaiting alone would only drain the
156 // microtask queue and still starve accept()/SIGCHLD — the same trap
157 // documented in 0.1.180, 0.1.214 and 0.1.215.
158 let sweepTimer = null
159 let sweeping = false
160 let resweep = false
161
162 const runSweep = async () => {
163 if (sweeping) { resweep = true; return } // coalesce into the running pass
164 sweeping = true
165 try {
166 do {
167 resweep = false
168 await mountOrgGroupedRepos(orgName, deps)
169 } while (resweep)
170 } catch {
171 } finally {
172 sweeping = false
173 }
174 }
175
176 const onUpdate = () => {
177 if (sweepTimer) return // a sweep is already scheduled
178 sweepTimer = setTimeout(() => {
179 sweepTimer = null
180 runSweep()
181 }, ORG_RESWEEP_DEBOUNCE_MS)
182 if (sweepTimer && typeof sweepTimer.unref === 'function') sweepTimer.unref()
183 }
184
134185 room.base.on('update', onUpdate)
135186 }
136187 await mountOrgGroupedRepos(orgName, deps)
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pear-git-v3",
3 "version": "0.1.215",
3 "version": "0.1.216",
44 "description": "P2P git forge — multi-writer issue/org/release/peer-mirror daemon. Built bottom-up after the failed v2.",
55 "type": "module",
66 "main": "index.js",