mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
fix(providers): preserve bounded artifact reads and cleanup owners
Refs #4373, #4455, #4457, #4458. Use canonical artifact ownership in Go, Rust, SQLite and PE readers and their failure consumers. Admit PE read budgets before allocation and ignore zero-extent sections in interval ownership. Close Rust resources through CLI and MCP lifecycles.
This commit is contained in:
1 parent
542fa84957
commit
effb71f59c
23 files changed
+1068
-231
No files matched your search
@@ -1,4 +1,9 @@
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
ArtifactFileCleanupFailure,
|
||||
bindArtifactFileCleanup,
|
||||
artifactFileCancellationCause,
|
||||
} from "../artifacts/ArtifactFileOperation.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
AnalysisError,
|
||||
@@ -49,25 +54,32 @@ export const sqliteDatabaseFailure = (
|
||||
const operation = "inspect_sqlite_database";
|
||||
if (cause instanceof AnalysisError) return cause;
|
||||
if (cause instanceof ArtifactReaderFailure) {
|
||||
if (cause.cleanup !== undefined)
|
||||
return new ProviderCleanupError(
|
||||
if (cause.cleanup !== undefined) {
|
||||
const physicalRead =
|
||||
cause instanceof ArtifactFileCleanupFailure
|
||||
? cause.fileCleanupFailure.outcome
|
||||
: undefined;
|
||||
const previous =
|
||||
physicalRead?.kind === "failed"
|
||||
? sqliteDatabaseFailure(physicalRead.cause, path, phase)
|
||||
: signal?.aborted
|
||||
? new AnalysisCancelledError(operation, { cause: signal.reason })
|
||||
: physicalRead?.kind === "completed" || cause.cause === undefined
|
||||
? undefined
|
||||
: sqliteDatabaseFailure(cause.cause, path, phase);
|
||||
const error = new ProviderCleanupError(
|
||||
SQLITE_PROVIDER_IDENTITY.id,
|
||||
cause.cleanup.resources,
|
||||
{
|
||||
reason: cause.cleanup.reason,
|
||||
previous_error:
|
||||
cause.cause === undefined
|
||||
? null
|
||||
: projectAnalysisError(
|
||||
signal?.aborted
|
||||
? new AnalysisCancelledError(operation, {
|
||||
cause: cause.cause,
|
||||
})
|
||||
: sqliteDatabaseFailure(cause.cause, path, phase),
|
||||
),
|
||||
previous === undefined ? null : projectAnalysisError(previous),
|
||||
},
|
||||
{ operation, cause },
|
||||
);
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
return error;
|
||||
}
|
||||
if (cause.reason === "io" && cause.cause !== undefined)
|
||||
return sqliteDatabaseFailure(cause.cause, path, phase, signal);
|
||||
if (cause.reason === "integrity")
|
||||
@@ -75,7 +87,9 @@ export const sqliteDatabaseFailure = (
|
||||
cause,
|
||||
});
|
||||
if (cause.reason === "cancelled")
|
||||
return new AnalysisCancelledError(operation, { cause });
|
||||
return new AnalysisCancelledError(operation, {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
});
|
||||
return new AnalysisInputError(operation, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/sqliteDatabase.js";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
import {
|
||||
withArtifactFileHandle,
|
||||
assertArtifactReadActive,
|
||||
} from "../artifacts/ArtifactFileOperation.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
type StableArtifactFileSystem,
|
||||
@@ -180,61 +183,51 @@ const verifyInactiveJournal = async (
|
||||
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
||||
),
|
||||
);
|
||||
const owner = new OwnedFileHandle(file);
|
||||
let failure: unknown;
|
||||
try {
|
||||
if (!sameState(before, await file.stat({ bigint: true })))
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
`SQLite journal changed before open: ${path}`,
|
||||
);
|
||||
// TRUNCATE commits leave an empty file; PERSIST commits zero the complete
|
||||
// 28-byte fixed header. A new active journal may zero only its magic bytes.
|
||||
const header = Buffer.alloc(28);
|
||||
let bytesRead = 0;
|
||||
if (before.size > 0n) {
|
||||
while (bytesRead < header.length) {
|
||||
signal?.throwIfAborted();
|
||||
const read = await file.read(
|
||||
header,
|
||||
bytesRead,
|
||||
header.length - bytesRead,
|
||||
bytesRead,
|
||||
await withArtifactFileHandle(
|
||||
path,
|
||||
file,
|
||||
resourceScope,
|
||||
signal,
|
||||
async (file) => {
|
||||
if (!sameState(before, await file.stat({ bigint: true })))
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
`SQLite journal changed before open: ${path}`,
|
||||
);
|
||||
if (read.bytesRead === 0) break;
|
||||
bytesRead += read.bytesRead;
|
||||
// TRUNCATE commits leave an empty file; PERSIST commits zero the complete
|
||||
// 28-byte fixed header. A new active journal may zero only its magic bytes.
|
||||
const header = Buffer.alloc(28);
|
||||
let bytesRead = 0;
|
||||
if (before.size > 0n) {
|
||||
while (bytesRead < header.length) {
|
||||
assertArtifactReadActive(signal);
|
||||
const read = await file.read(
|
||||
header,
|
||||
bytesRead,
|
||||
header.length - bytesRead,
|
||||
bytesRead,
|
||||
);
|
||||
if (read.bytesRead === 0) break;
|
||||
bytesRead += read.bytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sameState(before, await file.stat({ bigint: true })))
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
`SQLite journal changed during inspection: ${path}`,
|
||||
);
|
||||
if (
|
||||
before.size !== 0n &&
|
||||
(bytesRead !== header.length || header.some((byte) => byte !== 0))
|
||||
)
|
||||
throw new ArtifactReaderFailure(
|
||||
phase === "initial" ? "format" : "integrity",
|
||||
phase === "initial"
|
||||
? `Rollback journal may require recovery: ${path}. Supply a clean offline snapshot; only empty journals or a fully zeroed header are supported.`
|
||||
: `Rollback journal became active or malformed during database acquisition: ${path}`,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
failure = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
const cleanup = await resourceScope.release({
|
||||
kind: "file-handle",
|
||||
resource: path,
|
||||
handle: owner,
|
||||
});
|
||||
if (cleanup.kind === "failed")
|
||||
throw ArtifactReaderFailure.withCleanup(
|
||||
failure ?? cleanup.cause,
|
||||
ArtifactReaderFailure.cleanupObservation(cleanup.cause, path),
|
||||
);
|
||||
}
|
||||
if (!sameState(before, await file.stat({ bigint: true })))
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
`SQLite journal changed during inspection: ${path}`,
|
||||
);
|
||||
if (
|
||||
before.size !== 0n &&
|
||||
(bytesRead !== header.length || header.some((byte) => byte !== 0))
|
||||
)
|
||||
throw new ArtifactReaderFailure(
|
||||
phase === "initial" ? "format" : "integrity",
|
||||
phase === "initial"
|
||||
? `Rollback journal may require recovery: ${path}. Supply a clean offline snapshot; only empty journals or a fully zeroed header are supported.`
|
||||
: `Rollback journal became active or malformed during database acquisition: ${path}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Absence is a caller value to correct, not malformed path or content.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
bindArtifactFileCleanup,
|
||||
ArtifactFileCleanupFailure,
|
||||
artifactFileCancellationCause,
|
||||
} from "../artifacts/ArtifactFileOperation.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
@@ -176,13 +181,30 @@ export const executeSqliteDatabaseWorker = async (request: {
|
||||
? { cleanup: cause.cleanup }
|
||||
: {}),
|
||||
};
|
||||
if (request.signal?.aborted)
|
||||
throw new AnalysisCancelledError(operation, options);
|
||||
throw new AnalysisOutputError(
|
||||
if (
|
||||
cause instanceof ArtifactReaderFailure &&
|
||||
(cause.reason === "cancelled" ||
|
||||
(cause instanceof ArtifactFileCleanupFailure &&
|
||||
cause.fileCleanupFailure.outcome.kind === "completed" &&
|
||||
request.signal?.aborted))
|
||||
) {
|
||||
const error = new AnalysisCancelledError(operation, {
|
||||
...options,
|
||||
cause:
|
||||
cause.reason === "cancelled"
|
||||
? artifactFileCancellationCause(cause)
|
||||
: request.signal?.reason,
|
||||
});
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
throw error;
|
||||
}
|
||||
const error = new AnalysisOutputError(
|
||||
operation,
|
||||
`Owned SQLite worker reply is missing or malformed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
options,
|
||||
);
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
throw error;
|
||||
}
|
||||
if (!reply.ok) throw rejectedReply(reply, capturedOutput);
|
||||
return reply.inspection;
|
||||
|
||||
Reference in new issue
Block a user