Skip to content

Commit 239718a

Browse files
committed
chore: improve metadata write safety
1 parent 7bc795e commit 239718a

File tree

1 file changed

+25
-11
lines changed

1 file changed

+25
-11
lines changed

cmp/compiler/src/metadata/manager.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,33 @@ export class MetadataManager {
9797
lastUpdated: new Date().toISOString(),
9898
};
9999

100+
// Per LLM writing to a file is not an atomic operation while rename is, so nobody should get partial content.
101+
// Sounds reasonable.
102+
const dir = path.dirname(this.filePath);
103+
const base = path.basename(this.filePath);
104+
105+
// Keep temp file in the same directory to maximize chance that rename is atomic
106+
const tmpPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}`);
107+
108+
const json = JSON.stringify(metadata, null, 2);
109+
100110
await withTimeout(
101-
fsPromises.writeFile(
102-
this.filePath,
103-
JSON.stringify(metadata, null, 2),
104-
"utf-8",
105-
),
111+
fsPromises.writeFile(tmpPath, json, "utf-8"),
106112
DEFAULT_TIMEOUTS.METADATA,
107-
"Save metadata",
113+
"Save metadata (tmp write)",
108114
);
115+
116+
try {
117+
// TODO (AleksandrSl 14/12/2025): LLM says that we may want to remove older file first for windows, but it seems lo work fine as is.
118+
await withTimeout(
119+
fsPromises.rename(tmpPath, this.filePath),
120+
DEFAULT_TIMEOUTS.METADATA,
121+
"Save metadata (atomic rename)",
122+
);
123+
} finally {
124+
// Best-effort cleanup if rename failed for some reason
125+
await fsPromises.unlink(tmpPath).catch(() => {});
126+
}
109127
}
110128

111129
/**
@@ -120,29 +138,25 @@ export class MetadataManager {
120138
): Promise<MetadataSchema> {
121139
const lockDir = path.dirname(this.filePath);
122140

123-
// Ensure directory exists before locking
124141
await fsPromises.mkdir(lockDir, { recursive: true });
125142

126-
// Create lock file if it doesn't exist (lockfile needs a file to lock)
127143
try {
128144
await fsPromises.access(this.filePath);
129145
} catch {
130-
// TODO (AleksandrSl 10/12/2025): Should I use another file as a lock?
131146
await fsPromises.writeFile(
132147
this.filePath,
133148
JSON.stringify(createEmptyMetadata(), null, 2),
134149
"utf-8",
135150
);
136151
}
137152

138-
// Acquire lock with retry options
139153
const release = await lockfile.lock(this.filePath, {
140154
retries: {
141155
retries: 10,
142156
minTimeout: 50,
143157
maxTimeout: 1000,
144158
},
145-
stale: 2000, // Consider lock stale after 5 seconds
159+
stale: 2000,
146160
});
147161

148162
try {

0 commit comments

Comments
 (0)