gitnexus 1.6.8-rc.32 → 1.6.8-rc.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RegistryEntry } from '../../storage/repo-manager.js';
|
|
2
|
-
import type { GroupConfig, RepoHandle, RepoSnapshot, StoredContract, CrossLink } from './types.js';
|
|
2
|
+
import type { GroupConfig, RepoHandle, RepoSnapshot, StoredContract, CrossLink, GroupManifestLink } from './types.js';
|
|
3
3
|
export interface SyncOptions {
|
|
4
4
|
extractorOverride?: ((repo: RepoHandle) => Promise<StoredContract[]>) | (() => Promise<StoredContract[]>);
|
|
5
5
|
resolveRepoHandle?: (registryName: string, groupPath: string) => Promise<RepoHandle | null>;
|
|
@@ -18,4 +18,30 @@ export interface SyncResult {
|
|
|
18
18
|
repoSnapshots: Record<string, RepoSnapshot>;
|
|
19
19
|
}
|
|
20
20
|
export declare function stableRepoPoolId(entry: RegistryEntry, allEntries: RegistryEntry[]): string;
|
|
21
|
+
/** A batch of manifest links whose referenced in-group repos fit one resident window. */
|
|
22
|
+
export interface ManifestWindow {
|
|
23
|
+
links: GroupManifestLink[];
|
|
24
|
+
/** In-group repos (group paths) this window's links reference; size ≤ maxResident. */
|
|
25
|
+
repos: Set<string>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Partition manifest links into windows so each window references at most
|
|
29
|
+
* `maxResident` distinct in-group repos. Manifest resolution then materializes
|
|
30
|
+
* only one window's repos at a time, bounding peak pool residency regardless of
|
|
31
|
+
* group size (PR #2191 review, Finding 3 — windowed deferred resolution).
|
|
32
|
+
*
|
|
33
|
+
* Each link references ≤2 in-group repos, so every link fits a window when
|
|
34
|
+
* `maxResident ≥ 2`. Links are pre-sorted by their referenced-repo key so links
|
|
35
|
+
* sharing a repo land in contiguous windows — combined with release-not-close
|
|
36
|
+
* pooling, a hub repo stays warm across the windows that reference it (its
|
|
37
|
+
* lease is released, not closed, so the next window's initLbug fast-paths it).
|
|
38
|
+
* Every link lands in EXACTLY one window (a true partition): downstream
|
|
39
|
+
* dedupeCrossLinks dedupes cross-links but not contracts, so a link in two
|
|
40
|
+
* windows would emit duplicate contracts nothing absorbs.
|
|
41
|
+
*
|
|
42
|
+
* Repos not in `knownRepos` (dangling / unresolved) add 0 to a window's repo
|
|
43
|
+
* budget — the link is still placed (so it yields synthetic-UID contracts), it
|
|
44
|
+
* just consumes no residency.
|
|
45
|
+
*/
|
|
46
|
+
export declare function partitionManifestWindows(links: GroupManifestLink[], knownRepos: Set<string>, maxResident: number): ManifestWindow[];
|
|
21
47
|
export declare function syncGroup(config: GroupConfig, opts?: SyncOptions): Promise<SyncResult>;
|
package/dist/core/group/sync.js
CHANGED
|
Binary file
|
|
@@ -37,6 +37,41 @@ export { realStdoutWrite, realStderrWrite, setActiveStdoutWrite } from '../../mc
|
|
|
37
37
|
* Call this during long-running operations to prevent the connection from being closed.
|
|
38
38
|
*/
|
|
39
39
|
export declare const touchRepo: (repoId: string) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Acquire one eviction-exemption lease on a repo (LRU + idle timeout) by
|
|
42
|
+
* incrementing its reference count. The repoId must match the key passed to
|
|
43
|
+
* initLbug (e.g. group sync leases by handle.id — the same id it inits with).
|
|
44
|
+
* Leasing a repoId before it enters the pool is allowed and protects the entry
|
|
45
|
+
* once it is created, but the lease does NOT survive a teardown: closeOne
|
|
46
|
+
* force-clears the count, so a later re-init of the same repoId starts
|
|
47
|
+
* unpinned. Each pinRepo MUST be balanced by exactly one release (the repo
|
|
48
|
+
* stays exempt until the last lease is released). See the pinnedRepos docstring
|
|
49
|
+
* for the full contract.
|
|
50
|
+
*
|
|
51
|
+
* Returns a `release` disposer (mirroring addPoolCloseListener) that releases
|
|
52
|
+
* THIS lease exactly once — calling it twice is a no-op, so it can never
|
|
53
|
+
* over-decrement a sibling holder's count. Prefer the disposer
|
|
54
|
+
* (`const release = pinRepo(id); try { … } finally { release(); }`) so the
|
|
55
|
+
* pin/release pair is leak-proof; unpinRepo remains available for callers that
|
|
56
|
+
* pair explicitly.
|
|
57
|
+
*/
|
|
58
|
+
export declare const pinRepo: (repoId: string) => (() => void);
|
|
59
|
+
/**
|
|
60
|
+
* Release one eviction-exemption lease on a repo. The repo becomes eligible for
|
|
61
|
+
* automatic eviction again only once its count reaches 0 (the key is deleted).
|
|
62
|
+
* Idempotent at the floor: releasing a repo with no active lease is a no-op (no
|
|
63
|
+
* negative counts). Does NOT close the repo's pool.
|
|
64
|
+
*/
|
|
65
|
+
export declare const unpinRepo: (repoId: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Maximum number of repos a bounded multi-repo operation (e.g. group sync's
|
|
68
|
+
* windowed manifest resolution) should hold resident at once. Equals
|
|
69
|
+
* MAX_POOL_SIZE today, but exposed under an intent-named accessor so callers
|
|
70
|
+
* size their working set against "max repos a bounded op should hold" rather
|
|
71
|
+
* than coupling to the LRU eviction-cap constant, which may be tuned
|
|
72
|
+
* independently.
|
|
73
|
+
*/
|
|
74
|
+
export declare const getMaxResidentRepos: () => number;
|
|
40
75
|
/**
|
|
41
76
|
* Silence stdout by replacing process.stdout.write with a no-op.
|
|
42
77
|
* Uses a reference counter so nested silence/restore pairs are safe.
|
|
@@ -39,6 +39,30 @@ const MAX_POOL_SIZE = 5;
|
|
|
39
39
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
40
40
|
/** Max connections per repo (caps concurrent queries per repo) */
|
|
41
41
|
const MAX_CONNS_PER_REPO = 8;
|
|
42
|
+
/**
|
|
43
|
+
* Repos exempt from AUTOMATIC eviction (LRU + idle timeout) until explicitly
|
|
44
|
+
* unpinned. Used by bounded multi-repo operations like `group sync`, which
|
|
45
|
+
* initializes one pool per repo and then resolves cross-repo manifest/workspace
|
|
46
|
+
* links against ALL of those pools after the init loop. Without pinning, a
|
|
47
|
+
* group larger than MAX_POOL_SIZE would LRU-evict the earliest repos before
|
|
48
|
+
* resolution runs, leaving the deferred executor closures pointing at dead pool
|
|
49
|
+
* entries (issue #2189).
|
|
50
|
+
*
|
|
51
|
+
* Pins are REFERENCE-COUNTED: the map holds repoId → active lease count. This
|
|
52
|
+
* lets overlapping holders (two windows of one sync, or two concurrent
|
|
53
|
+
* `group sync` calls sharing a repo) coexist safely — the repo stays exempt
|
|
54
|
+
* until the LAST holder releases. A boolean Set could not represent "two
|
|
55
|
+
* holders," so the first release would wrongly clear a pin another holder still
|
|
56
|
+
* needs (PR #2191 review, Finding 1).
|
|
57
|
+
*
|
|
58
|
+
* Pins block only automatic eviction (LRU + idle). Explicit teardown
|
|
59
|
+
* (closeOne / closeLbug) always closes the entry and force-clears its count —
|
|
60
|
+
* teardown is authoritative. A present key always means count ≥ 1. While every
|
|
61
|
+
* pooled repo is pinned, evictLRU finds no eligible victim and the pool may
|
|
62
|
+
* transiently exceed MAX_POOL_SIZE — the same soft-cap behavior that already
|
|
63
|
+
* occurs when every entry is checked out.
|
|
64
|
+
*/
|
|
65
|
+
const pinnedRepos = new Map();
|
|
42
66
|
// Behavior-neutral RSS tracing for the FTS evict→reload memory repro
|
|
43
67
|
// (gitnexus/scripts/bench/fts-evict-reload-rss.mjs). Two invariants keep it safe
|
|
44
68
|
// in the pool init/close hot path: it writes ONLY to stderr (stdout is the MCP
|
|
@@ -77,6 +101,8 @@ function ensureIdleTimer() {
|
|
|
77
101
|
idleTimer = setInterval(() => {
|
|
78
102
|
const now = Date.now();
|
|
79
103
|
for (const [repoId, entry] of pool) {
|
|
104
|
+
if (pinnedRepos.has(repoId))
|
|
105
|
+
continue;
|
|
80
106
|
if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
|
|
81
107
|
closeOne(repoId);
|
|
82
108
|
}
|
|
@@ -97,7 +123,64 @@ export const touchRepo = (repoId) => {
|
|
|
97
123
|
}
|
|
98
124
|
};
|
|
99
125
|
/**
|
|
100
|
-
*
|
|
126
|
+
* Acquire one eviction-exemption lease on a repo (LRU + idle timeout) by
|
|
127
|
+
* incrementing its reference count. The repoId must match the key passed to
|
|
128
|
+
* initLbug (e.g. group sync leases by handle.id — the same id it inits with).
|
|
129
|
+
* Leasing a repoId before it enters the pool is allowed and protects the entry
|
|
130
|
+
* once it is created, but the lease does NOT survive a teardown: closeOne
|
|
131
|
+
* force-clears the count, so a later re-init of the same repoId starts
|
|
132
|
+
* unpinned. Each pinRepo MUST be balanced by exactly one release (the repo
|
|
133
|
+
* stays exempt until the last lease is released). See the pinnedRepos docstring
|
|
134
|
+
* for the full contract.
|
|
135
|
+
*
|
|
136
|
+
* Returns a `release` disposer (mirroring addPoolCloseListener) that releases
|
|
137
|
+
* THIS lease exactly once — calling it twice is a no-op, so it can never
|
|
138
|
+
* over-decrement a sibling holder's count. Prefer the disposer
|
|
139
|
+
* (`const release = pinRepo(id); try { … } finally { release(); }`) so the
|
|
140
|
+
* pin/release pair is leak-proof; unpinRepo remains available for callers that
|
|
141
|
+
* pair explicitly.
|
|
142
|
+
*/
|
|
143
|
+
export const pinRepo = (repoId) => {
|
|
144
|
+
pinnedRepos.set(repoId, (pinnedRepos.get(repoId) ?? 0) + 1);
|
|
145
|
+
let released = false;
|
|
146
|
+
return () => {
|
|
147
|
+
if (released)
|
|
148
|
+
return;
|
|
149
|
+
released = true;
|
|
150
|
+
unpinRepo(repoId);
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Release one eviction-exemption lease on a repo. The repo becomes eligible for
|
|
155
|
+
* automatic eviction again only once its count reaches 0 (the key is deleted).
|
|
156
|
+
* Idempotent at the floor: releasing a repo with no active lease is a no-op (no
|
|
157
|
+
* negative counts). Does NOT close the repo's pool.
|
|
158
|
+
*/
|
|
159
|
+
export const unpinRepo = (repoId) => {
|
|
160
|
+
const count = pinnedRepos.get(repoId);
|
|
161
|
+
if (count === undefined)
|
|
162
|
+
return;
|
|
163
|
+
if (count <= 1) {
|
|
164
|
+
pinnedRepos.delete(repoId);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
pinnedRepos.set(repoId, count - 1);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Maximum number of repos a bounded multi-repo operation (e.g. group sync's
|
|
172
|
+
* windowed manifest resolution) should hold resident at once. Equals
|
|
173
|
+
* MAX_POOL_SIZE today, but exposed under an intent-named accessor so callers
|
|
174
|
+
* size their working set against "max repos a bounded op should hold" rather
|
|
175
|
+
* than coupling to the LRU eviction-cap constant, which may be tuned
|
|
176
|
+
* independently.
|
|
177
|
+
*/
|
|
178
|
+
export const getMaxResidentRepos = () => MAX_POOL_SIZE;
|
|
179
|
+
/**
|
|
180
|
+
* Evict the least-recently-used repo if pool is at capacity.
|
|
181
|
+
* Pinned repos are never chosen as the eviction victim — when every eligible
|
|
182
|
+
* entry is pinned, no eviction occurs and the pool transiently exceeds
|
|
183
|
+
* MAX_POOL_SIZE (see the pinnedRepos docstring).
|
|
101
184
|
*/
|
|
102
185
|
function evictLRU() {
|
|
103
186
|
if (pool.size < MAX_POOL_SIZE)
|
|
@@ -105,6 +188,8 @@ function evictLRU() {
|
|
|
105
188
|
let oldestId = null;
|
|
106
189
|
let oldestTime = Infinity;
|
|
107
190
|
for (const [id, entry] of pool) {
|
|
191
|
+
if (pinnedRepos.has(id))
|
|
192
|
+
continue;
|
|
108
193
|
if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
|
|
109
194
|
oldestTime = entry.lastUsed;
|
|
110
195
|
oldestId = id;
|
|
@@ -167,6 +252,10 @@ function closeOne(repoId) {
|
|
|
167
252
|
}
|
|
168
253
|
}
|
|
169
254
|
pool.delete(repoId);
|
|
255
|
+
// Clear any eviction pin — the entry is gone, so the pin is meaningless and
|
|
256
|
+
// would otherwise leak across operations in a long-lived process. Teardown
|
|
257
|
+
// is authoritative: an explicit close always wins over a pin.
|
|
258
|
+
pinnedRepos.delete(repoId);
|
|
170
259
|
// Notify listeners AFTER the pool entry is gone so any cache-invalidation
|
|
171
260
|
// they perform is consistent with `isLbugReady(repoId) === false`.
|
|
172
261
|
for (const listener of poolCloseListeners) {
|
package/package.json
CHANGED