cherry-pick(state): lock-wrapped appends for journal, audit, workflow-logger
Cherry-pick of gsd-build/gsd-2 53babec29 — lock-wrapped append half.
Wraps appends to .sf/journal/, .sf/audit/events.jsonl, and the
workflow-logger error log in withFileLockSync (onLocked: skip),
preserving best-effort semantics while preventing torn writes
under contention.
Companion to the atomic-write half landed in 3df56cb94. Path-renames
(gsdRoot→sfRoot, gsd-db→sf-db) preserved during conflict resolution.
Co-Authored-By: Jeremy <jeremy@fluxlabs.net>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f1f4b840e1
commit
22d4579690
3 changed files with 60 additions and 6 deletions
|
|
@ -12,10 +12,19 @@
|
|||
* - Silent failure: journal writes never throw — absence of events is the failure signal
|
||||
*/
|
||||
|
||||
import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
||||
import {
|
||||
appendFileSync,
|
||||
closeSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { sfRoot } from "./paths.js";
|
||||
import { isStaleWrite } from "./auto/turn-epoch.js";
|
||||
import { withFileLockSync } from "./file-lock.js";
|
||||
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
||||
import { isAuditEnvelopeEnabled } from "./uok/audit-toggle.js";
|
||||
|
||||
|
|
@ -102,7 +111,19 @@ export function emitJournalEvent(basePath: string, entry: JournalEntry): void {
|
|||
mkdirSync(journalDir, { recursive: true });
|
||||
const dateStr = entry.ts.slice(0, 10);
|
||||
const filePath = join(journalDir, `${dateStr}.jsonl`);
|
||||
appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
||||
// Ensure file exists so proper-lockfile can acquire a lock against it.
|
||||
if (!existsSync(filePath)) closeSync(openSync(filePath, "a"));
|
||||
// onLocked: "skip" — journal writes are best-effort. POSIX O_APPEND
|
||||
// atomicity still protects small entries; the lock mainly serializes
|
||||
// larger writes and gives cross-process exclusivity on platforms where
|
||||
// O_APPEND semantics are weaker (Windows).
|
||||
withFileLockSync(
|
||||
filePath,
|
||||
() => {
|
||||
appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
||||
},
|
||||
{ onLocked: "skip" },
|
||||
);
|
||||
} catch {
|
||||
// Silent failure — journal must never break auto-mode
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { appendFileSync, mkdirSync } from "node:fs";
|
||||
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { sfRoot } from "../paths.js";
|
||||
import { isDbAvailable, insertAuditEvent } from "../sf-db.js";
|
||||
import { withFileLockSync } from "../file-lock.js";
|
||||
import type { AuditEventEnvelope } from "./contracts.js";
|
||||
|
||||
function auditLogPath(basePath: string): string {
|
||||
|
|
@ -37,7 +38,21 @@ export function buildAuditEnvelope(args: {
|
|||
export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope): void {
|
||||
try {
|
||||
ensureAuditDir(basePath);
|
||||
appendFileSync(auditLogPath(basePath), `${JSON.stringify(event)}\n`, "utf-8");
|
||||
const path = auditLogPath(basePath);
|
||||
// proper-lockfile requires the target file to exist before locking.
|
||||
// Touch it via open(O_APPEND|O_CREAT) so the first writer wins the race
|
||||
// atomically at the kernel level.
|
||||
if (!existsSync(path)) closeSync(openSync(path, "a"));
|
||||
// onLocked: "skip" — audit writes are best-effort; under heavy contention
|
||||
// POSIX O_APPEND atomicity still protects small line writes, so skipping
|
||||
// the lock rather than stalling orchestration is the correct tradeoff.
|
||||
withFileLockSync(
|
||||
path,
|
||||
() => {
|
||||
appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
|
||||
},
|
||||
{ onLocked: "skip" },
|
||||
);
|
||||
} catch {
|
||||
// Best-effort: audit writes must never break orchestration.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,17 @@
|
|||
// the start of each unit to prevent log bleed between units running in the same
|
||||
// Node process.
|
||||
|
||||
import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import {
|
||||
appendFileSync,
|
||||
closeSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { withFileLockSync } from "./file-lock.js";
|
||||
import { appendNotification } from "./notification-store.js";
|
||||
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
||||
import { isAuditEnvelopeEnabled } from "./uok/audit-toggle.js";
|
||||
|
|
@ -308,8 +316,18 @@ function _push(
|
|||
try {
|
||||
const auditDir = join(_auditBasePath, ".sf");
|
||||
mkdirSync(auditDir, { recursive: true });
|
||||
const auditPath = join(auditDir, "audit-log.jsonl");
|
||||
const sanitized = _sanitizeForAudit(entry);
|
||||
appendFileSync(join(auditDir, "audit-log.jsonl"), JSON.stringify(sanitized) + "\n", "utf-8");
|
||||
// Ensure file exists so proper-lockfile can acquire a lock against it.
|
||||
if (!existsSync(auditPath)) closeSync(openSync(auditPath, "a"));
|
||||
// onLocked: "skip" — never block error logging on lock contention.
|
||||
withFileLockSync(
|
||||
auditPath,
|
||||
() => {
|
||||
appendFileSync(auditPath, JSON.stringify(sanitized) + "\n", "utf-8");
|
||||
},
|
||||
{ onLocked: "skip" },
|
||||
);
|
||||
} catch (auditErr) {
|
||||
// Best-effort — never let audit write failures bubble up
|
||||
_writeStderr(`[sf:audit] failed to persist log entry: ${(auditErr as Error).message}\n`);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue