mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 10:55:57 +00:00
* feat(process): make the regex worker heap request configurable REA_REGEX_WORKER_HEAP_REQUEST_MIB keeps the 128 MiB default and refuses values outside 8 to 16384 MiB. A positive Node --max-old-space-size still overrides the request and is not clamped by the setting. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): set the regex worker heap floor to the measured startup bound On Node 22.23.4 a 5 MiB old-generation request aborts the regex worker during startup, and 6 MiB starts it and returns a match. The setting floor is that measured bound. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): raise the regex worker heap floor to 7 MiB 6 MiB passes configuration but returns ERR_WORKER_OUT_OF_MEMORY for a trivial match on supported Linux Node 24 and Node 26.0.0. 7 MiB returned that match on the measured 22, 24, and 26 lines. A clean-process test runs the production worker at the floor. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
185 lines
5.8 KiB
TypeScript
185 lines
5.8 KiB
TypeScript
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisResourceConstraintError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { err, type Result } from "../domain/result.js";
|
|
import { SharedStartupAttempt } from "../process/SharedStartupAttempt.js";
|
|
import {
|
|
DEFAULT_REGEX_MATCHING_DEADLINE,
|
|
type RegexMatchingDeadline,
|
|
} from "../config/regexMatchingDeadline.js";
|
|
import {
|
|
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
|
|
type RegexWorkerHeapRequest,
|
|
} from "../config/regexWorkerHeapRequest.js";
|
|
import { matchRegexValues } from "../process/RegexMatcher.js";
|
|
import type { KunaJsonDocumentReader } from "./KunaJsonStream.js";
|
|
import { KUNA_DIAGNOSTIC_BYTES } from "./KunaLimits.js";
|
|
import { KunaRunSlots } from "./KunaRunSlots.js";
|
|
import type {
|
|
KunaTargetCommands,
|
|
KunaTargetCommandRequest,
|
|
KunaTargetCommandSnapshot,
|
|
} from "./KunaTargetCommands.js";
|
|
|
|
/** One shared producer; individual callers own only their wait. */
|
|
export type KunaPendingQuery<T> = SharedStartupAttempt<
|
|
Result<T, AnalysisError>
|
|
>;
|
|
|
|
type RegexQuery = Omit<Parameters<typeof matchRegexValues>[0], "values"> & {
|
|
readonly values: () => readonly string[];
|
|
};
|
|
|
|
/** Shared admission, cancellation and cleanup for session metadata queries. */
|
|
export class KunaQueryWork {
|
|
readonly #nativeSlots = new KunaRunSlots(1);
|
|
readonly #regexSlots = new KunaRunSlots(1);
|
|
|
|
constructor(
|
|
private readonly commands: KunaTargetCommands,
|
|
private readonly lifetime: {
|
|
readonly signal: AbortSignal;
|
|
readonly own: <T>(work: Promise<T>) => Promise<T>;
|
|
readonly runSlots: KunaRunSlots;
|
|
},
|
|
private readonly deadline: RegexMatchingDeadline = DEFAULT_REGEX_MATCHING_DEADLINE,
|
|
private readonly heapRequest: RegexWorkerHeapRequest = DEFAULT_REGEX_WORKER_HEAP_REQUEST,
|
|
) {}
|
|
|
|
/** Unproven process cleanup retains both leases in the native command owner. */
|
|
async run(
|
|
request: Omit<KunaTargetCommandRequest, "releaseRun">,
|
|
): Promise<Result<KunaTargetCommandSnapshot, AnalysisError>> {
|
|
const local = await this.#nativeSlots.acquire(request.signal);
|
|
if (local === undefined)
|
|
return err(
|
|
new AnalysisCancelledError(request.operation, {
|
|
cause: request.signal.reason,
|
|
}),
|
|
);
|
|
let release: (() => void) | undefined;
|
|
try {
|
|
const foreground = await this.lifetime.runSlots.acquire(request.signal);
|
|
if (foreground === undefined) {
|
|
local();
|
|
return err(
|
|
new AnalysisCancelledError(request.operation, {
|
|
cause: request.signal.reason,
|
|
}),
|
|
);
|
|
}
|
|
release = () => {
|
|
foreground();
|
|
local();
|
|
};
|
|
return await this.commands.run({ ...request, releaseRun: release });
|
|
} catch (cause: unknown) {
|
|
if (release === undefined) local();
|
|
else release();
|
|
throw cause;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run a whole-program command whose stdout `reader` validates as it
|
|
* arrives. A rejected document stops Kuna and reports why; diagnostics
|
|
* beyond their buffer recur for this target and executable, so they are
|
|
* passed to `retain` for the session to keep.
|
|
*/
|
|
async scan<Parsed>(
|
|
request: Omit<
|
|
KunaTargetCommandRequest,
|
|
"releaseRun" | "outputBytes" | "stdout" | "signal"
|
|
>,
|
|
reader: KunaJsonDocumentReader<Parsed>,
|
|
signal: AbortSignal,
|
|
retain: (failure: AnalysisError) => void,
|
|
): Promise<Result<Parsed, AnalysisError>> {
|
|
const run = await this.run({
|
|
...request,
|
|
outputBytes: KUNA_DIAGNOSTIC_BYTES,
|
|
stdout: reader,
|
|
signal: AbortSignal.any([signal, reader.rejected]),
|
|
});
|
|
if (run.ok) return reader.finish(run.value.input);
|
|
const rejection = reader.rejection;
|
|
if (
|
|
rejection !== undefined &&
|
|
run.error instanceof AnalysisCancelledError &&
|
|
!signal.aborted
|
|
)
|
|
return err(rejection);
|
|
if (
|
|
run.error instanceof AnalysisResourceConstraintError &&
|
|
run.error.resource === "memory"
|
|
)
|
|
retain(run.error);
|
|
return run;
|
|
}
|
|
|
|
/** Bound inventory copies and regex workers; close cancels and drains both. */
|
|
match(input: RegexQuery): ReturnType<typeof matchRegexValues> {
|
|
const signal = AbortSignal.any([
|
|
this.lifetime.signal,
|
|
...(input.signal === undefined ? [] : [input.signal]),
|
|
]);
|
|
return this.lifetime.own(this.#match(input, signal));
|
|
}
|
|
|
|
async #match(
|
|
input: RegexQuery,
|
|
signal: AbortSignal,
|
|
): ReturnType<typeof matchRegexValues> {
|
|
const release = await this.#regexSlots.acquire(signal);
|
|
if (release === undefined)
|
|
return err(
|
|
new AnalysisCancelledError(input.operation, { cause: signal.reason }),
|
|
);
|
|
try {
|
|
return await matchRegexValues({
|
|
...input,
|
|
values: input.values(),
|
|
signal,
|
|
deadline: this.deadline,
|
|
heapRequest: this.heapRequest,
|
|
});
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
|
|
start<T>(
|
|
load: (signal: AbortSignal) => Promise<Result<T, AnalysisError>>,
|
|
accept: (value: T) => void,
|
|
): KunaPendingQuery<T> {
|
|
const pending = new SharedStartupAttempt(
|
|
(signal) => this.lifetime.own(load(signal)),
|
|
this.lifetime.signal,
|
|
);
|
|
void pending.promise.then(
|
|
(result) => {
|
|
if (result.ok && !pending.signal.aborted) accept(result.value);
|
|
},
|
|
() => {},
|
|
);
|
|
return pending;
|
|
}
|
|
|
|
async wait<T>(
|
|
pending: KunaPendingQuery<T>,
|
|
signal: AbortSignal | undefined,
|
|
operation: string,
|
|
): Promise<Result<T, AnalysisError>> {
|
|
const waited = await pending.wait(signal);
|
|
if (waited.status === "completed") return waited.value;
|
|
// Final departure retains all producer failures, including a typed
|
|
// cancellation with captured facts or incomplete cleanup.
|
|
if (waited.status === "stopped" && !waited.value.ok) return waited.value;
|
|
return err(
|
|
new AnalysisCancelledError(operation, { cause: signal?.reason }),
|
|
);
|
|
}
|
|
}
|