gitnexus 1.6.10-rc.18 → 1.6.10-rc.19
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.
- package/README.md +2 -0
- package/dist/cli/i18n/en.d.ts +1 -1
- package/dist/cli/i18n/en.js +1 -1
- package/dist/cli/i18n/resources.d.ts +1 -1
- package/dist/cli/i18n/zh-CN.js +1 -1
- package/dist/core/ingestion/languages/cpp/captures.js +157 -74
- package/dist/core/ingestion/workers/worker-pool.d.ts +14 -0
- package/dist/core/ingestion/workers/worker-pool.js +85 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -535,6 +535,8 @@ Three env vars expose the pool's resilience layers (respawn budget, cumulative-t
|
|
|
535
535
|
| `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT` | `3` | Max replacement spawns per slot before the slot is dropped from the active rotation. |
|
|
536
536
|
| `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Bounds exponentially-growing retry waits. |
|
|
537
537
|
| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` | `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, dispatches require a fresh pool. |
|
|
538
|
+
| `GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS` | `30000` | Max wait at pool shutdown for a retired worker still inside native code — terminated at its next JS-safe point instead of mid-native-call, which would abort the process (`Napi::Error`, #2432). |
|
|
539
|
+
| `GITNEXUS_CPP_CAPTURE_BUDGET_MS` | `20000` | Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning (#2432). `0` expires immediately. |
|
|
538
540
|
|
|
539
541
|
### Graph cleanup tuning
|
|
540
542
|
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -243,5 +243,5 @@ export declare const en: {
|
|
|
243
243
|
readonly 'help.option.group.contracts.type': "Filter by contract type";
|
|
244
244
|
readonly 'help.option.group.contracts.repo': "Filter by repo";
|
|
245
245
|
readonly 'help.option.group.contracts.unmatched': "Show only unmatched contracts";
|
|
246
|
-
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
246
|
+
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
247
247
|
};
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -243,5 +243,5 @@ export const en = {
|
|
|
243
243
|
'help.option.group.contracts.type': 'Filter by contract type',
|
|
244
244
|
'help.option.group.contracts.repo': 'Filter by repo',
|
|
245
245
|
'help.option.group.contracts.unmatched': 'Show only unmatched contracts',
|
|
246
|
-
'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).',
|
|
246
|
+
'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).',
|
|
247
247
|
};
|
|
@@ -244,7 +244,7 @@ export declare const cliResources: {
|
|
|
244
244
|
readonly 'help.option.group.contracts.type': "Filter by contract type";
|
|
245
245
|
readonly 'help.option.group.contracts.repo': "Filter by repo";
|
|
246
246
|
readonly 'help.option.group.contracts.unmatched': "Show only unmatched contracts";
|
|
247
|
-
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
247
|
+
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
248
248
|
};
|
|
249
249
|
readonly 'zh-CN': {
|
|
250
250
|
'common.notIndexed': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -243,5 +243,5 @@ export const zhCN = {
|
|
|
243
243
|
'help.option.group.contracts.type': '按契约类型过滤',
|
|
244
244
|
'help.option.group.contracts.repo': '按仓库过滤',
|
|
245
245
|
'help.option.group.contracts.unmatched': '仅显示未匹配契约',
|
|
246
|
-
'help.analyze.environment': '\n环境变量:\n GITNEXUS_NO_GITIGNORE=1 跳过 .gitignore 解析(仍读取 .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N 覆盖大文件跳过阈值(KB)。默认 512,最大 32768。\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker 空闲超时(毫秒)。默认 30000。\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL 自动 checkpoint 阈值(字节,默认 67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker 作业字节预算。默认 8388608。\n GITNEXUS_WORKER_POOL_SIZE=N 解析 worker 数量覆盖值。默认 cores-1,最多 16。\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N 并发进行中的解析分块数。默认 2。\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N 每个 slot 丢弃前允许的最大替换进程数。默认 3。\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N 每个作业的总重试墙钟时间。默认 5 倍子批次超时。\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N 每个 slot 触发熔断的死亡次数。默认 max(3, poolSize)。\n GITNEXUS_EMBEDDING_THREADS=N 限制 --embeddings 的本地 ONNX CPU 线程数。\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N exact-scan 回退的最大嵌入分块数。默认 10000。\n GITNEXUS_VECTOR_MAX_DISTANCE=N 语义/向量搜索接受的最大余弦距离(0 < N <= 2;超出则钳制为 2)。MCP 默认 0.6,其他路径默认 0.5。\n\n当参数和对应环境变量同时提供时,参数优先。\n\n提示:`.gitnexusignore` 支持 `.gitignore` 风格的取反。比如添加\n `!__tests__/` 可以索引默认自动过滤的目录(#771)。',
|
|
246
|
+
'help.analyze.environment': '\n环境变量:\n GITNEXUS_NO_GITIGNORE=1 跳过 .gitignore 解析(仍读取 .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N 覆盖大文件跳过阈值(KB)。默认 512,最大 32768。\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker 空闲超时(毫秒)。默认 30000。\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL 自动 checkpoint 阈值(字节,默认 67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker 作业字节预算。默认 8388608。\n GITNEXUS_WORKER_POOL_SIZE=N 解析 worker 数量覆盖值。默认 cores-1,最多 16。\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N 并发进行中的解析分块数。默认 2。\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N 每个 slot 丢弃前允许的最大替换进程数。默认 3。\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N 每个作业的总重试墙钟时间。默认 5 倍子批次超时。\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N 每个 slot 触发熔断的死亡次数。默认 max(3, poolSize)。\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N 线程池关闭时等待仍在原生代码中的已退役 worker 的最长时间(到达安全点后再终止,避免进程级 abort)。默认 30000。\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N C++ 捕获提取的每文件墙钟预算;超出后该文件保留部分捕获并输出警告。默认 20000。\n GITNEXUS_EMBEDDING_THREADS=N 限制 --embeddings 的本地 ONNX CPU 线程数。\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N exact-scan 回退的最大嵌入分块数。默认 10000。\n GITNEXUS_VECTOR_MAX_DISTANCE=N 语义/向量搜索接受的最大余弦距离(0 < N <= 2;超出则钳制为 2)。MCP 默认 0.6,其他路径默认 0.5。\n\n当参数和对应环境变量同时提供时,参数优先。\n\n提示:`.gitnexusignore` 支持 `.gitignore` 风格的取反。比如添加\n `!__tests__/` 可以索引默认自动过滤的目录(#771)。',
|
|
247
247
|
};
|
|
@@ -12,6 +12,27 @@ import { markCppInlineNamespaceRange } from './inline-namespaces.js';
|
|
|
12
12
|
import { extractCppTemplateConstraints } from './constraint-extractor.js';
|
|
13
13
|
import { captureCppMemberLookupFacts } from './member-lookup.js';
|
|
14
14
|
import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js';
|
|
15
|
+
import { logger } from '../../../logger.js';
|
|
16
|
+
/**
|
|
17
|
+
* Per-file wall-clock budget for the capture-emit loop (#2432). A worker
|
|
18
|
+
* thread stuck in this loop cannot be terminated safely (terminating a
|
|
19
|
+
* thread mid-N-API call aborts the whole process with Napi::Error), so the
|
|
20
|
+
* loop must bound itself: on breach we return the captures accumulated so
|
|
21
|
+
* far with a warning — degraded coverage for one file, never a crash or a
|
|
22
|
+
* thrown error (a throw here would make the language-group catch drop every
|
|
23
|
+
* remaining file in the batch).
|
|
24
|
+
*
|
|
25
|
+
* `GITNEXUS_CPP_CAPTURE_BUDGET_MS`: unset/invalid/negative → 20000; explicit
|
|
26
|
+
* 0 → expires immediately (deterministic test hook).
|
|
27
|
+
*/
|
|
28
|
+
const CPP_CAPTURE_BUDGET_DEFAULT_MS = 20_000;
|
|
29
|
+
function cppCaptureBudgetMs() {
|
|
30
|
+
const raw = process.env.GITNEXUS_CPP_CAPTURE_BUDGET_MS;
|
|
31
|
+
if (raw === undefined || raw === '')
|
|
32
|
+
return CPP_CAPTURE_BUDGET_DEFAULT_MS;
|
|
33
|
+
const parsed = Number(raw);
|
|
34
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : CPP_CAPTURE_BUDGET_DEFAULT_MS;
|
|
35
|
+
}
|
|
15
36
|
export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
|
|
16
37
|
let tree = cachedTree;
|
|
17
38
|
if (tree === undefined) {
|
|
@@ -21,10 +42,27 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
21
42
|
}
|
|
22
43
|
const rawMatches = getCppScopeQuery().matches(tree.rootNode);
|
|
23
44
|
const out = [];
|
|
45
|
+
// #2432: reset the per-file lookup index. The identifier-argument type
|
|
46
|
+
// lookups below used to re-walk the AST per identifier (full-tree DFS in
|
|
47
|
+
// isKnownEnumName, per-scope declaration scans) — O(calls × args × treeSize)
|
|
48
|
+
// per file, 151s on a 194KB file that parses in 46ms. The index makes each
|
|
49
|
+
// lookup O(1) after a single lazily-built pass.
|
|
50
|
+
resetCppFileLookupIndex();
|
|
24
51
|
// Track ranges where typedef-struct/enum was captured as its concrete type
|
|
25
52
|
// so we can suppress the duplicate @declaration.typedef match.
|
|
26
53
|
const concreteTypedefRanges = new Set();
|
|
54
|
+
// #2432: per-file deadline for the loop below (see cppCaptureBudgetMs).
|
|
55
|
+
// Checked every 64 matches — post-index a single iteration is microseconds,
|
|
56
|
+
// so the check granularity costs nothing and bounds the drift past the
|
|
57
|
+
// deadline to well under a second.
|
|
58
|
+
const budgetMs = cppCaptureBudgetMs();
|
|
59
|
+
const deadline = Date.now() + budgetMs;
|
|
60
|
+
let matchIndex = 0;
|
|
27
61
|
for (const m of rawMatches) {
|
|
62
|
+
if ((matchIndex++ & 63) === 0 && Date.now() >= deadline) {
|
|
63
|
+
logger.warn({ filePath, budgetMs, processedMatches: matchIndex - 1, totalMatches: rawMatches.length }, `C++ capture extraction exceeded its ${budgetMs}ms budget for ${filePath}; returning partial captures for this file (#2432).`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
28
66
|
const grouped = {};
|
|
29
67
|
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
|
|
30
68
|
// hands us each matched node as `c.node`, so anchors resolve via a
|
|
@@ -960,6 +998,47 @@ function inferCppBracedInitType(node) {
|
|
|
960
998
|
? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}:${elementTypes.length}`
|
|
961
999
|
: `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length}`;
|
|
962
1000
|
}
|
|
1001
|
+
let fileLookupIndex = {
|
|
1002
|
+
enumNames: null,
|
|
1003
|
+
scopeDecls: new Map(),
|
|
1004
|
+
fnParams: new Map(),
|
|
1005
|
+
};
|
|
1006
|
+
function resetCppFileLookupIndex() {
|
|
1007
|
+
fileLookupIndex = { enumNames: null, scopeDecls: new Map(), fnParams: new Map() };
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* First-declaration-wins map of the scope's `declaration` children that
|
|
1011
|
+
* carry a concrete (non-placeholder) type and a nameable declarator —
|
|
1012
|
+
* exactly the entries the replaced per-identifier scans could match.
|
|
1013
|
+
*/
|
|
1014
|
+
function scopeDeclarationsFor(scope) {
|
|
1015
|
+
const cached = fileLookupIndex.scopeDecls.get(scope.id);
|
|
1016
|
+
if (cached !== undefined)
|
|
1017
|
+
return cached;
|
|
1018
|
+
const decls = new Map();
|
|
1019
|
+
for (let i = 0; i < scope.childCount; i++) {
|
|
1020
|
+
const stmt = scope.child(i);
|
|
1021
|
+
if (stmt === null || stmt.type !== 'declaration')
|
|
1022
|
+
continue;
|
|
1023
|
+
const typeNode = stmt.childForFieldName('type');
|
|
1024
|
+
if (typeNode === null)
|
|
1025
|
+
continue;
|
|
1026
|
+
if (typeNode.type === 'placeholder_type_specifier')
|
|
1027
|
+
continue;
|
|
1028
|
+
const declarator = stmt.childForFieldName('declarator');
|
|
1029
|
+
if (declarator === null)
|
|
1030
|
+
continue;
|
|
1031
|
+
const nameChild = declaredNameNode(declarator);
|
|
1032
|
+
if (nameChild === null)
|
|
1033
|
+
continue;
|
|
1034
|
+
const name = extractDeclaratorLeafName(nameChild);
|
|
1035
|
+
if (name === '' || decls.has(name))
|
|
1036
|
+
continue;
|
|
1037
|
+
decls.set(name, stmt);
|
|
1038
|
+
}
|
|
1039
|
+
fileLookupIndex.scopeDecls.set(scope.id, decls);
|
|
1040
|
+
return decls;
|
|
1041
|
+
}
|
|
963
1042
|
/**
|
|
964
1043
|
* Look up the declared type of a variable by scanning sibling declarations
|
|
965
1044
|
* in the enclosing compound_statement (function body). Handles:
|
|
@@ -991,27 +1070,14 @@ function lookupDeclaredTypeForIdentifier(identNode) {
|
|
|
991
1070
|
const paramType = lookupFunctionParameterType(scope, varName);
|
|
992
1071
|
if (paramType !== '')
|
|
993
1072
|
return paramType;
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
// Skip auto/placeholder types — those need chain-follow, not literal
|
|
1003
|
-
if (typeNode.type === 'placeholder_type_specifier')
|
|
1004
|
-
continue;
|
|
1005
|
-
// Check init_declarator children for the variable name
|
|
1006
|
-
const declarator = stmt.childForFieldName('declarator');
|
|
1007
|
-
if (declarator === null)
|
|
1008
|
-
continue;
|
|
1009
|
-
const nameChild = declaredNameNode(declarator);
|
|
1010
|
-
if (nameChild !== null && extractDeclaratorLeafName(nameChild) === varName) {
|
|
1011
|
-
return normalizeCppTypeText(typeNode.text);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
return '';
|
|
1073
|
+
// Indexed scope-declaration lookup (#2432; was a per-identifier scan).
|
|
1074
|
+
const stmt = scopeDeclarationsFor(scope).get(varName);
|
|
1075
|
+
if (stmt === undefined)
|
|
1076
|
+
return '';
|
|
1077
|
+
const typeNode = stmt.childForFieldName('type');
|
|
1078
|
+
if (typeNode === null)
|
|
1079
|
+
return '';
|
|
1080
|
+
return normalizeCppTypeText(typeNode.text);
|
|
1015
1081
|
}
|
|
1016
1082
|
function lookupDeclaredTypeClassForIdentifier(identNode) {
|
|
1017
1083
|
const varName = identNode.text;
|
|
@@ -1026,28 +1092,20 @@ function lookupDeclaredTypeClassForIdentifier(identNode) {
|
|
|
1026
1092
|
const paramTypeClass = lookupFunctionParameterTypeClass(scope, varName, identNode);
|
|
1027
1093
|
if (paramTypeClass !== undefined)
|
|
1028
1094
|
return paramTypeClass;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const nameChild = declaredNameNode(declarator);
|
|
1042
|
-
if (nameChild === null || extractDeclaratorLeafName(nameChild) !== varName)
|
|
1043
|
-
continue;
|
|
1044
|
-
const typeClass = classifyCppParameterType(typeNode.text, nameChild.text, stmt.text.replace(/;\s*$/, ''));
|
|
1045
|
-
if (isKnownEnumName(identNode, typeClass.base)) {
|
|
1046
|
-
return { ...typeClass, base: `enum:${typeClass.base}` };
|
|
1047
|
-
}
|
|
1048
|
-
return typeClass;
|
|
1095
|
+
// Indexed scope-declaration lookup (#2432; was a per-identifier scan).
|
|
1096
|
+
const stmt = scopeDeclarationsFor(scope).get(varName);
|
|
1097
|
+
if (stmt === undefined)
|
|
1098
|
+
return unknownTypeClass('unknown');
|
|
1099
|
+
const typeNode = stmt.childForFieldName('type');
|
|
1100
|
+
const declarator = stmt.childForFieldName('declarator');
|
|
1101
|
+
const nameChild = declarator !== null ? declaredNameNode(declarator) : null;
|
|
1102
|
+
if (typeNode === null || nameChild === null)
|
|
1103
|
+
return unknownTypeClass('unknown');
|
|
1104
|
+
const typeClass = classifyCppParameterType(typeNode.text, nameChild.text, stmt.text.replace(/;\s*$/, ''));
|
|
1105
|
+
if (isKnownEnumName(identNode, typeClass.base)) {
|
|
1106
|
+
return { ...typeClass, base: `enum:${typeClass.base}` };
|
|
1049
1107
|
}
|
|
1050
|
-
return
|
|
1108
|
+
return typeClass;
|
|
1051
1109
|
}
|
|
1052
1110
|
function lookupFunctionParameterType(scope, varName) {
|
|
1053
1111
|
const param = findEnclosingFunctionParameter(scope, varName);
|
|
@@ -1078,27 +1136,45 @@ function findEnclosingFunctionParameter(scope, varName) {
|
|
|
1078
1136
|
let node = scope.parent;
|
|
1079
1137
|
while (node !== null) {
|
|
1080
1138
|
if (node.type === 'function_definition' || node.type === 'function_declarator') {
|
|
1081
|
-
|
|
1082
|
-
? node
|
|
1083
|
-
: findFirstDescendantOfType(node, 'function_declarator');
|
|
1084
|
-
const params = fnDecl?.childForFieldName('parameters') ?? null;
|
|
1085
|
-
if (params !== null) {
|
|
1086
|
-
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1087
|
-
const param = params.namedChild(i);
|
|
1088
|
-
if (param === null || param.type !== 'parameter_declaration')
|
|
1089
|
-
continue;
|
|
1090
|
-
const declarator = param.childForFieldName('declarator');
|
|
1091
|
-
if (declarator !== null && extractDeclaratorLeafName(declarator) === varName) {
|
|
1092
|
-
return param;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return null;
|
|
1139
|
+
return enclosingFunctionParametersFor(node)?.get(varName) ?? null;
|
|
1097
1140
|
}
|
|
1098
1141
|
node = node.parent;
|
|
1099
1142
|
}
|
|
1100
1143
|
return null;
|
|
1101
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* First-wins map of a function's `parameter_declaration`s by declarator
|
|
1147
|
+
* leaf name (#2432; was a per-identifier scan). `null` when the function
|
|
1148
|
+
* has no parameter list — the caller returns null without walking further
|
|
1149
|
+
* up, preserving the replaced scan's early-return.
|
|
1150
|
+
*/
|
|
1151
|
+
function enclosingFunctionParametersFor(fnNode) {
|
|
1152
|
+
const cached = fileLookupIndex.fnParams.get(fnNode.id);
|
|
1153
|
+
if (cached !== undefined)
|
|
1154
|
+
return cached;
|
|
1155
|
+
const fnDecl = fnNode.type === 'function_declarator'
|
|
1156
|
+
? fnNode
|
|
1157
|
+
: findFirstDescendantOfType(fnNode, 'function_declarator');
|
|
1158
|
+
const params = fnDecl?.childForFieldName('parameters') ?? null;
|
|
1159
|
+
let index = null;
|
|
1160
|
+
if (params !== null) {
|
|
1161
|
+
index = new Map();
|
|
1162
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1163
|
+
const param = params.namedChild(i);
|
|
1164
|
+
if (param === null || param.type !== 'parameter_declaration')
|
|
1165
|
+
continue;
|
|
1166
|
+
const declarator = param.childForFieldName('declarator');
|
|
1167
|
+
if (declarator === null)
|
|
1168
|
+
continue;
|
|
1169
|
+
const name = extractDeclaratorLeafName(declarator);
|
|
1170
|
+
if (name === '' || index.has(name))
|
|
1171
|
+
continue;
|
|
1172
|
+
index.set(name, param);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
fileLookupIndex.fnParams.set(fnNode.id, index);
|
|
1176
|
+
return index;
|
|
1177
|
+
}
|
|
1102
1178
|
function declaredNameNode(declarator) {
|
|
1103
1179
|
if (declarator.type !== 'init_declarator')
|
|
1104
1180
|
return declarator;
|
|
@@ -1126,24 +1202,31 @@ function normalizeCppTypeText(text) {
|
|
|
1126
1202
|
function isKnownEnumName(node, typeName) {
|
|
1127
1203
|
if (typeName === '' || typeName === 'unknown')
|
|
1128
1204
|
return false;
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1205
|
+
// One full-tree DFS per FILE (lazy), not per identifier argument — the
|
|
1206
|
+
// per-identifier walk here was the dominant cost of #2432 (87s of a 151s
|
|
1207
|
+
// extraction on a file that parses in 46ms).
|
|
1208
|
+
if (fileLookupIndex.enumNames === null) {
|
|
1209
|
+
let root = node;
|
|
1210
|
+
while (root.parent !== null)
|
|
1211
|
+
root = root.parent;
|
|
1212
|
+
const names = new Set();
|
|
1213
|
+
const stack = [root];
|
|
1214
|
+
while (stack.length > 0) {
|
|
1215
|
+
const cur = stack.pop();
|
|
1216
|
+
if (cur.type === 'enum_specifier') {
|
|
1217
|
+
const name = cur.childForFieldName('name');
|
|
1218
|
+
if (name !== null)
|
|
1219
|
+
names.add(name.text);
|
|
1220
|
+
}
|
|
1221
|
+
for (let i = 0; i < cur.childCount; i++) {
|
|
1222
|
+
const child = cur.child(i);
|
|
1223
|
+
if (child !== null)
|
|
1224
|
+
stack.push(child);
|
|
1225
|
+
}
|
|
1144
1226
|
}
|
|
1227
|
+
fileLookupIndex.enumNames = names;
|
|
1145
1228
|
}
|
|
1146
|
-
return
|
|
1229
|
+
return fileLookupIndex.enumNames.has(typeName);
|
|
1147
1230
|
}
|
|
1148
1231
|
/**
|
|
1149
1232
|
* Detect whether a `namespace_definition` AST node is inline.
|
|
@@ -135,6 +135,19 @@ export interface WorkerPoolOptions {
|
|
|
135
135
|
pdg?: boolean;
|
|
136
136
|
/** Per-function source-line cap for worker-side CFG construction (0 ⇒ no cap). */
|
|
137
137
|
pdgMaxFunctionLines?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Max wall time `terminate()` waits for a retired worker that has NOT yet
|
|
140
|
+
* reached a JS-visible safe point before giving up on terminating it
|
|
141
|
+
* (#2432). Terminating a worker thread that is inside an N-API call aborts
|
|
142
|
+
* the whole process (`Napi::Error` → `std::terminate` → SIGABRT) — and the
|
|
143
|
+
* same abort fires at plain process exit, so the drain is what makes
|
|
144
|
+
* shutdown safe. On expiry the worker is left running (unref'd, with its
|
|
145
|
+
* at-safe-point terminate listener still armed) and a diagnostic is logged.
|
|
146
|
+
* Default 30000ms — above the C++ capture budget
|
|
147
|
+
* (`GITNEXUS_CPP_CAPTURE_BUDGET_MS`, 20000ms) so the drain converges for
|
|
148
|
+
* the known pathological class. 0 ⇒ no wait (test hook).
|
|
149
|
+
*/
|
|
150
|
+
shutdownDrainMs?: number;
|
|
138
151
|
}
|
|
139
152
|
export declare class WorkerPoolDispatchError extends Error {
|
|
140
153
|
/**
|
|
@@ -193,6 +206,7 @@ interface ResolvedWorkerPoolOptions {
|
|
|
193
206
|
maxRespawnsPerSlot: number;
|
|
194
207
|
maxCumulativeTimeoutMs: number;
|
|
195
208
|
consecutiveFailureThreshold: number;
|
|
209
|
+
shutdownDrainMs: number;
|
|
196
210
|
}
|
|
197
211
|
export declare function resolveWorkerPoolOptions(options?: WorkerPoolOptions, poolSize?: number): ResolvedWorkerPoolOptions;
|
|
198
212
|
/**
|
|
@@ -231,6 +231,8 @@ function nonNegativeInteger(value) {
|
|
|
231
231
|
? Math.floor(parsed)
|
|
232
232
|
: undefined;
|
|
233
233
|
}
|
|
234
|
+
/** See {@link WorkerPoolOptions.shutdownDrainMs}. */
|
|
235
|
+
const DEFAULT_SHUTDOWN_DRAIN_MS = 30_000;
|
|
234
236
|
export function resolveWorkerPoolOptions(options = {}, poolSize) {
|
|
235
237
|
const subBatchIdleTimeoutMs = positiveInteger(options.subBatchIdleTimeoutMs) ??
|
|
236
238
|
positiveInteger(process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS) ??
|
|
@@ -252,6 +254,9 @@ export function resolveWorkerPoolOptions(options = {}, poolSize) {
|
|
|
252
254
|
consecutiveFailureThreshold: positiveInteger(options.consecutiveFailureThreshold) ??
|
|
253
255
|
positiveInteger(process.env.GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD) ??
|
|
254
256
|
Math.max(DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD_FLOOR, poolSize ?? 0),
|
|
257
|
+
shutdownDrainMs: nonNegativeInteger(options.shutdownDrainMs) ??
|
|
258
|
+
nonNegativeInteger(process.env.GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS) ??
|
|
259
|
+
DEFAULT_SHUTDOWN_DRAIN_MS,
|
|
255
260
|
};
|
|
256
261
|
}
|
|
257
262
|
/**
|
|
@@ -608,13 +613,50 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
608
613
|
// Set by `terminate()` (below). Also read by the self-healing startup loop so
|
|
609
614
|
// a terminate during startup aborts pending backoff/retries (#1741).
|
|
610
615
|
let terminated = false;
|
|
616
|
+
/** Resolves `true` when `promise` settles within `ms`, else `false`. The
|
|
617
|
+
* timer is unref'd so an expiring drain never holds the process open. */
|
|
618
|
+
const settledWithin = (promise, ms) => {
|
|
619
|
+
if (ms <= 0)
|
|
620
|
+
return Promise.resolve(false);
|
|
621
|
+
return new Promise((resolve) => {
|
|
622
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
623
|
+
timer.unref?.();
|
|
624
|
+
void promise.then(() => {
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
resolve(true);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
};
|
|
611
630
|
const terminateTrackedWorkers = async (liveWorkers) => {
|
|
612
631
|
const retired = Array.from(retiredWorkers);
|
|
613
632
|
await Promise.all([
|
|
614
633
|
...liveWorkers.map((worker) => worker?.terminate().catch(() => undefined)),
|
|
615
|
-
...retired.map((record) =>
|
|
634
|
+
...retired.map(async (record) => {
|
|
635
|
+
// #2432: a retired worker that has not reached a JS-visible safe
|
|
636
|
+
// point may be inside an N-API call — terminating it aborts the
|
|
637
|
+
// WHOLE process (`Napi::Error` → std::terminate → SIGABRT). Drain:
|
|
638
|
+
// wait (bounded) for its safe point; on expiry leave it running —
|
|
639
|
+
// it is unref'd and its at-safe-point terminate listener stays
|
|
640
|
+
// armed — and log which file wedged it.
|
|
641
|
+
if (!record.safeToTerminate) {
|
|
642
|
+
const drained = await settledWithin(record.safePoint, poolOptions.shutdownDrainMs);
|
|
643
|
+
if (!drained) {
|
|
644
|
+
logger.warn({
|
|
645
|
+
workerIndex: record.workerIndex,
|
|
646
|
+
reason: record.reason,
|
|
647
|
+
drainMs: poolOptions.shutdownDrainMs,
|
|
648
|
+
}, `Worker ${record.workerIndex} is still inside native code after the ` +
|
|
649
|
+
`${poolOptions.shutdownDrainMs}ms shutdown drain; leaving it un-terminated ` +
|
|
650
|
+
`to avoid a native abort (#2432). It will be terminated at its next safe point.`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
await record.terminate();
|
|
655
|
+
}),
|
|
616
656
|
]);
|
|
617
|
-
|
|
657
|
+
// Undrained records stay tracked so a repeated shutdown call can retry
|
|
658
|
+
// their (now possibly safe) terminate; record.terminate() removes each
|
|
659
|
+
// drained record via its cleanup.
|
|
618
660
|
};
|
|
619
661
|
for (let i = 0; i < size; i++) {
|
|
620
662
|
workers[i] = spawnAndCapture(workerUrl);
|
|
@@ -788,6 +830,18 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
788
830
|
const retireWorkerAfterTimeout = (worker, workerIndex, reason) => {
|
|
789
831
|
let cleaned = false;
|
|
790
832
|
let terminateStarted = false;
|
|
833
|
+
let resolveSafePoint;
|
|
834
|
+
const safePoint = new Promise((resolve) => {
|
|
835
|
+
resolveSafePoint = resolve;
|
|
836
|
+
});
|
|
837
|
+
// A message/messageerror proves the worker is executing JS again; an
|
|
838
|
+
// exit/error means the thread is gone. Either way `worker.terminate()`
|
|
839
|
+
// can no longer land mid-N-API call (#2432), so shutdown's drain may
|
|
840
|
+
// stop waiting.
|
|
841
|
+
function markSafeToTerminate() {
|
|
842
|
+
record.safeToTerminate = true;
|
|
843
|
+
resolveSafePoint();
|
|
844
|
+
}
|
|
791
845
|
function cleanupRetired() {
|
|
792
846
|
if (cleaned)
|
|
793
847
|
return;
|
|
@@ -806,6 +860,7 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
806
860
|
await worker.terminate().catch(() => undefined);
|
|
807
861
|
}
|
|
808
862
|
function terminateWhenBackInJs() {
|
|
863
|
+
markSafeToTerminate();
|
|
809
864
|
void terminateRetired();
|
|
810
865
|
}
|
|
811
866
|
function onRetiredMessage(raw) {
|
|
@@ -816,8 +871,14 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
816
871
|
terminateWhenBackInJs();
|
|
817
872
|
}
|
|
818
873
|
}
|
|
819
|
-
const onRetiredError = () =>
|
|
820
|
-
|
|
874
|
+
const onRetiredError = () => {
|
|
875
|
+
markSafeToTerminate();
|
|
876
|
+
cleanupRetired();
|
|
877
|
+
};
|
|
878
|
+
const onRetiredExit = () => {
|
|
879
|
+
markSafeToTerminate();
|
|
880
|
+
cleanupRetired();
|
|
881
|
+
};
|
|
821
882
|
const onRetiredMessageError = () => terminateWhenBackInJs();
|
|
822
883
|
const record = {
|
|
823
884
|
worker,
|
|
@@ -825,6 +886,8 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
825
886
|
reason,
|
|
826
887
|
cleanup: cleanupRetired,
|
|
827
888
|
terminate: terminateRetired,
|
|
889
|
+
safeToTerminate: false,
|
|
890
|
+
safePoint,
|
|
828
891
|
};
|
|
829
892
|
retiredWorkers.add(record);
|
|
830
893
|
worker.on('message', onRetiredMessage);
|
|
@@ -890,8 +953,25 @@ export const createWorkerPool = (workerUrl, poolSize, options) => {
|
|
|
890
953
|
const liveWorkers = workers.slice();
|
|
891
954
|
for (let i = 0; i < workers.length; i++)
|
|
892
955
|
workers[i] = undefined;
|
|
956
|
+
// #2432: a live worker with a job in flight may be inside an N-API
|
|
957
|
+
// call — direct terminate risks the same native abort as the retired
|
|
958
|
+
// case. Route busy workers through the retire path (terminate at
|
|
959
|
+
// their next JS-visible safe point); idle workers are parked in the
|
|
960
|
+
// JS event loop and terminate safely right away.
|
|
961
|
+
const idleWorkers = [];
|
|
962
|
+
for (let i = 0; i < liveWorkers.length; i++) {
|
|
963
|
+
const worker = liveWorkers[i];
|
|
964
|
+
if (worker === undefined)
|
|
965
|
+
continue;
|
|
966
|
+
if (busySlots.has(i)) {
|
|
967
|
+
retireWorkerAfterTimeout(worker, i, 'circuit breaker tripped with job in flight');
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
idleWorkers.push(worker);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
893
973
|
activeSlots.clear();
|
|
894
|
-
void terminateTrackedWorkers(
|
|
974
|
+
void terminateTrackedWorkers(idleWorkers);
|
|
895
975
|
};
|
|
896
976
|
const maybeDone = () => {
|
|
897
977
|
if (stopped)
|
package/package.json
CHANGED