@saeeol/core 7.3.2 → 7.3.5
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/package.json +6 -2
- package/sst-env.d.ts +0 -10
- package/test/effect/cross-spawn-spawner.test.ts +0 -423
- package/test/effect/observability.test.ts +0 -46
- package/test/filesystem/filesystem.test.ts +0 -338
- package/test/fixture/effect-flock-worker.ts +0 -60
- package/test/fixture/flock-worker.ts +0 -72
- package/test/fixture/tmpdir.ts +0 -13
- package/test/global.test.ts +0 -16
- package/test/lib/effect.ts +0 -53
- package/test/npm-config.test.ts +0 -51
- package/test/npm.test.ts +0 -91
- package/test/saeeol/core-utils/filesystem-containment.test.ts +0 -13
- package/test/saeeol/filesystem-containment.test.ts +0 -13
- package/test/util/effect-flock.test.ts +0 -386
- package/test/util/flock.test.ts +0 -426
package/test/util/flock.test.ts
DELETED
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import fs from "fs/promises"
|
|
3
|
-
import { spawn } from "child_process"
|
|
4
|
-
import path from "path"
|
|
5
|
-
import os from "os"
|
|
6
|
-
import { Flock } from "@saeeol/core/util/flock"
|
|
7
|
-
import { Hash } from "@saeeol/core/util/hash"
|
|
8
|
-
|
|
9
|
-
type Msg = {
|
|
10
|
-
key: string
|
|
11
|
-
dir: string
|
|
12
|
-
staleMs?: number
|
|
13
|
-
timeoutMs?: number
|
|
14
|
-
baseDelayMs?: number
|
|
15
|
-
maxDelayMs?: number
|
|
16
|
-
holdMs?: number
|
|
17
|
-
ready?: string
|
|
18
|
-
active?: string
|
|
19
|
-
done?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const root = path.join(import.meta.dir, "../..")
|
|
23
|
-
const worker = path.join(import.meta.dir, "../fixture/flock-worker.ts")
|
|
24
|
-
|
|
25
|
-
async function tmpdir() {
|
|
26
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "flock-test-"))
|
|
27
|
-
return {
|
|
28
|
-
path: dir,
|
|
29
|
-
async [Symbol.asyncDispose]() {
|
|
30
|
-
await fs.rm(dir, { recursive: true, force: true })
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function lock(dir: string, key: string) {
|
|
36
|
-
return path.join(dir, Hash.fast(key) + ".lock")
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function sleep(ms: number) {
|
|
40
|
-
return new Promise<void>((resolve) => {
|
|
41
|
-
setTimeout(resolve, ms)
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function exists(file: string) {
|
|
46
|
-
return fs
|
|
47
|
-
.stat(file)
|
|
48
|
-
.then(() => true)
|
|
49
|
-
.catch(() => false)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function wait(file: string, timeout = 3_000) {
|
|
53
|
-
const stop = Date.now() + timeout
|
|
54
|
-
while (Date.now() < stop) {
|
|
55
|
-
if (await exists(file)) return
|
|
56
|
-
await sleep(20)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
throw new Error(`Timed out waiting for file: ${file}`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function run(msg: Msg) {
|
|
63
|
-
return new Promise<{ code: number; stdout: Buffer; stderr: Buffer }>((resolve) => {
|
|
64
|
-
const proc = spawn(process.execPath, [worker, JSON.stringify(msg)], {
|
|
65
|
-
cwd: root,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const stdout: Buffer[] = []
|
|
69
|
-
const stderr: Buffer[] = []
|
|
70
|
-
|
|
71
|
-
proc.stdout?.on("data", (data) => stdout.push(Buffer.from(data)))
|
|
72
|
-
proc.stderr?.on("data", (data) => stderr.push(Buffer.from(data)))
|
|
73
|
-
|
|
74
|
-
proc.on("close", (code) => {
|
|
75
|
-
resolve({
|
|
76
|
-
code: code ?? 1,
|
|
77
|
-
stdout: Buffer.concat(stdout),
|
|
78
|
-
stderr: Buffer.concat(stderr),
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function spawnWorker(msg: Msg) {
|
|
85
|
-
return spawn(process.execPath, [worker, JSON.stringify(msg)], {
|
|
86
|
-
cwd: root,
|
|
87
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function stopWorker(proc: ReturnType<typeof spawnWorker>) {
|
|
92
|
-
if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve()
|
|
93
|
-
|
|
94
|
-
if (process.platform !== "win32" || !proc.pid) {
|
|
95
|
-
proc.kill()
|
|
96
|
-
return Promise.resolve()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return new Promise<void>((resolve) => {
|
|
100
|
-
const killProc = spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"])
|
|
101
|
-
killProc.on("close", () => {
|
|
102
|
-
proc.kill()
|
|
103
|
-
resolve()
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function readJson<T>(p: string): Promise<T> {
|
|
109
|
-
return JSON.parse(await fs.readFile(p, "utf8"))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
describe("util.flock", () => {
|
|
113
|
-
test("enforces mutual exclusion under process contention", async () => {
|
|
114
|
-
await using tmp = await tmpdir()
|
|
115
|
-
const dir = path.join(tmp.path, "locks")
|
|
116
|
-
const done = path.join(tmp.path, "done.log")
|
|
117
|
-
const active = path.join(tmp.path, "active")
|
|
118
|
-
const key = "flock:stress"
|
|
119
|
-
const n = 16
|
|
120
|
-
|
|
121
|
-
const out = await Promise.all(
|
|
122
|
-
Array.from({ length: n }, () =>
|
|
123
|
-
run({
|
|
124
|
-
key,
|
|
125
|
-
dir,
|
|
126
|
-
done,
|
|
127
|
-
active,
|
|
128
|
-
holdMs: 30,
|
|
129
|
-
staleMs: 1_000,
|
|
130
|
-
timeoutMs: 15_000,
|
|
131
|
-
}),
|
|
132
|
-
),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
expect(out.map((x) => x.code)).toEqual(Array.from({ length: n }, () => 0))
|
|
136
|
-
expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([])
|
|
137
|
-
|
|
138
|
-
const lines = (await fs.readFile(done, "utf8"))
|
|
139
|
-
.split("\n")
|
|
140
|
-
.map((x) => x.trim())
|
|
141
|
-
.filter(Boolean)
|
|
142
|
-
expect(lines.length).toBe(n)
|
|
143
|
-
}, 20_000)
|
|
144
|
-
|
|
145
|
-
test("times out while waiting when lock is still healthy", async () => {
|
|
146
|
-
await using tmp = await tmpdir()
|
|
147
|
-
const dir = path.join(tmp.path, "locks")
|
|
148
|
-
const key = "flock:timeout"
|
|
149
|
-
const ready = path.join(tmp.path, "ready")
|
|
150
|
-
const proc = spawnWorker({
|
|
151
|
-
key,
|
|
152
|
-
dir,
|
|
153
|
-
ready,
|
|
154
|
-
holdMs: 20_000,
|
|
155
|
-
staleMs: 10_000,
|
|
156
|
-
timeoutMs: 30_000,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
await wait(ready, 5_000)
|
|
161
|
-
const seen: string[] = []
|
|
162
|
-
const err = await Flock.withLock(key, async () => {}, {
|
|
163
|
-
dir,
|
|
164
|
-
staleMs: 10_000,
|
|
165
|
-
timeoutMs: 1_000,
|
|
166
|
-
onWait: (tick) => {
|
|
167
|
-
seen.push(tick.key)
|
|
168
|
-
},
|
|
169
|
-
}).catch((err) => err)
|
|
170
|
-
|
|
171
|
-
expect(err).toBeInstanceOf(Error)
|
|
172
|
-
if (!(err instanceof Error)) throw err
|
|
173
|
-
expect(err.message).toContain("Timed out waiting for lock")
|
|
174
|
-
expect(seen.length).toBeGreaterThan(0)
|
|
175
|
-
expect(seen.every((x) => x === key)).toBe(true)
|
|
176
|
-
} finally {
|
|
177
|
-
await stopWorker(proc).catch(() => undefined)
|
|
178
|
-
await new Promise((resolve) => proc.on("close", resolve))
|
|
179
|
-
}
|
|
180
|
-
}, 15_000)
|
|
181
|
-
|
|
182
|
-
test("recovers after a crashed lock owner", async () => {
|
|
183
|
-
await using tmp = await tmpdir()
|
|
184
|
-
const dir = path.join(tmp.path, "locks")
|
|
185
|
-
const key = "flock:crash"
|
|
186
|
-
const ready = path.join(tmp.path, "ready")
|
|
187
|
-
const proc = spawnWorker({
|
|
188
|
-
key,
|
|
189
|
-
dir,
|
|
190
|
-
ready,
|
|
191
|
-
holdMs: 20_000,
|
|
192
|
-
staleMs: 500,
|
|
193
|
-
timeoutMs: 30_000,
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
await wait(ready, 5_000)
|
|
197
|
-
await stopWorker(proc)
|
|
198
|
-
await new Promise((resolve) => proc.on("close", resolve))
|
|
199
|
-
|
|
200
|
-
let hit = false
|
|
201
|
-
await Flock.withLock(
|
|
202
|
-
key,
|
|
203
|
-
async () => {
|
|
204
|
-
hit = true
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
dir,
|
|
208
|
-
staleMs: 500,
|
|
209
|
-
timeoutMs: 8_000,
|
|
210
|
-
},
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
expect(hit).toBe(true)
|
|
214
|
-
}, 20_000)
|
|
215
|
-
|
|
216
|
-
test("breaks stale lock dirs when heartbeat is missing", async () => {
|
|
217
|
-
await using tmp = await tmpdir()
|
|
218
|
-
const dir = path.join(tmp.path, "locks")
|
|
219
|
-
const key = "flock:missing-heartbeat"
|
|
220
|
-
const lockDir = lock(dir, key)
|
|
221
|
-
|
|
222
|
-
await fs.mkdir(lockDir, { recursive: true })
|
|
223
|
-
const old = new Date(Date.now() - 2_000)
|
|
224
|
-
await fs.utimes(lockDir, old, old)
|
|
225
|
-
|
|
226
|
-
let hit = false
|
|
227
|
-
await Flock.withLock(
|
|
228
|
-
key,
|
|
229
|
-
async () => {
|
|
230
|
-
hit = true
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
dir,
|
|
234
|
-
staleMs: 200,
|
|
235
|
-
timeoutMs: 3_000,
|
|
236
|
-
},
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
expect(hit).toBe(true)
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test("recovers when a stale breaker claim was left behind", async () => {
|
|
243
|
-
await using tmp = await tmpdir()
|
|
244
|
-
const dir = path.join(tmp.path, "locks")
|
|
245
|
-
const key = "flock:stale-breaker"
|
|
246
|
-
const lockDir = lock(dir, key)
|
|
247
|
-
const breaker = lockDir + ".breaker"
|
|
248
|
-
|
|
249
|
-
await fs.mkdir(lockDir, { recursive: true })
|
|
250
|
-
await fs.mkdir(breaker)
|
|
251
|
-
|
|
252
|
-
const old = new Date(Date.now() - 2_000)
|
|
253
|
-
await fs.utimes(lockDir, old, old)
|
|
254
|
-
await fs.utimes(breaker, old, old)
|
|
255
|
-
|
|
256
|
-
let hit = false
|
|
257
|
-
await Flock.withLock(
|
|
258
|
-
key,
|
|
259
|
-
async () => {
|
|
260
|
-
hit = true
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
dir,
|
|
264
|
-
staleMs: 200,
|
|
265
|
-
timeoutMs: 3_000,
|
|
266
|
-
},
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
expect(hit).toBe(true)
|
|
270
|
-
expect(await exists(breaker)).toBe(false)
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
test("fails clearly if lock dir is removed while held", async () => {
|
|
274
|
-
await using tmp = await tmpdir()
|
|
275
|
-
const dir = path.join(tmp.path, "locks")
|
|
276
|
-
const key = "flock:compromised"
|
|
277
|
-
const lockDir = lock(dir, key)
|
|
278
|
-
|
|
279
|
-
const err = await Flock.withLock(
|
|
280
|
-
key,
|
|
281
|
-
async () => {
|
|
282
|
-
await fs.rm(lockDir, {
|
|
283
|
-
recursive: true,
|
|
284
|
-
force: true,
|
|
285
|
-
})
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
dir,
|
|
289
|
-
staleMs: 1_000,
|
|
290
|
-
timeoutMs: 3_000,
|
|
291
|
-
},
|
|
292
|
-
).catch((err) => err)
|
|
293
|
-
|
|
294
|
-
expect(err).toBeInstanceOf(Error)
|
|
295
|
-
if (!(err instanceof Error)) throw err
|
|
296
|
-
expect(err.message).toContain("compromised")
|
|
297
|
-
|
|
298
|
-
let hit = false
|
|
299
|
-
await Flock.withLock(
|
|
300
|
-
key,
|
|
301
|
-
async () => {
|
|
302
|
-
hit = true
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
dir,
|
|
306
|
-
staleMs: 200,
|
|
307
|
-
timeoutMs: 3_000,
|
|
308
|
-
},
|
|
309
|
-
)
|
|
310
|
-
expect(hit).toBe(true)
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
test("writes owner metadata while lock is held", async () => {
|
|
314
|
-
await using tmp = await tmpdir()
|
|
315
|
-
const dir = path.join(tmp.path, "locks")
|
|
316
|
-
const key = "flock:meta"
|
|
317
|
-
const file = path.join(lock(dir, key), "meta.json")
|
|
318
|
-
|
|
319
|
-
await Flock.withLock(
|
|
320
|
-
key,
|
|
321
|
-
async () => {
|
|
322
|
-
const json = await readJson<{
|
|
323
|
-
token?: unknown
|
|
324
|
-
pid?: unknown
|
|
325
|
-
hostname?: unknown
|
|
326
|
-
createdAt?: unknown
|
|
327
|
-
}>(file)
|
|
328
|
-
|
|
329
|
-
expect(typeof json.token).toBe("string")
|
|
330
|
-
expect(typeof json.pid).toBe("number")
|
|
331
|
-
expect(typeof json.hostname).toBe("string")
|
|
332
|
-
expect(typeof json.createdAt).toBe("string")
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
dir,
|
|
336
|
-
staleMs: 1_000,
|
|
337
|
-
timeoutMs: 3_000,
|
|
338
|
-
},
|
|
339
|
-
)
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
test("supports acquire with await using", async () => {
|
|
343
|
-
await using tmp = await tmpdir()
|
|
344
|
-
const dir = path.join(tmp.path, "locks")
|
|
345
|
-
const key = "flock:acquire"
|
|
346
|
-
const lockDir = lock(dir, key)
|
|
347
|
-
|
|
348
|
-
{
|
|
349
|
-
await using _ = await Flock.acquire(key, {
|
|
350
|
-
dir,
|
|
351
|
-
staleMs: 1_000,
|
|
352
|
-
timeoutMs: 3_000,
|
|
353
|
-
})
|
|
354
|
-
expect(await exists(lockDir)).toBe(true)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
expect(await exists(lockDir)).toBe(false)
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
test("refuses token mismatch release and recovers from stale", async () => {
|
|
361
|
-
await using tmp = await tmpdir()
|
|
362
|
-
const dir = path.join(tmp.path, "locks")
|
|
363
|
-
const key = "flock:token"
|
|
364
|
-
const lockDir = lock(dir, key)
|
|
365
|
-
const meta = path.join(lockDir, "meta.json")
|
|
366
|
-
|
|
367
|
-
const err = await Flock.withLock(
|
|
368
|
-
key,
|
|
369
|
-
async () => {
|
|
370
|
-
const json = await readJson<{ token?: string }>(meta)
|
|
371
|
-
json.token = "tampered"
|
|
372
|
-
await fs.writeFile(meta, JSON.stringify(json, null, 2))
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
dir,
|
|
376
|
-
staleMs: 500,
|
|
377
|
-
timeoutMs: 3_000,
|
|
378
|
-
},
|
|
379
|
-
).catch((err) => err)
|
|
380
|
-
|
|
381
|
-
expect(err).toBeInstanceOf(Error)
|
|
382
|
-
if (!(err instanceof Error)) throw err
|
|
383
|
-
expect(err.message).toContain("token mismatch")
|
|
384
|
-
expect(await exists(lockDir)).toBe(true)
|
|
385
|
-
|
|
386
|
-
let hit = false
|
|
387
|
-
await Flock.withLock(
|
|
388
|
-
key,
|
|
389
|
-
async () => {
|
|
390
|
-
hit = true
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
dir,
|
|
394
|
-
staleMs: 500,
|
|
395
|
-
timeoutMs: 6_000,
|
|
396
|
-
},
|
|
397
|
-
)
|
|
398
|
-
expect(hit).toBe(true)
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
test("fails clearly on unwritable lock roots", async () => {
|
|
402
|
-
if (process.platform === "win32") return
|
|
403
|
-
|
|
404
|
-
await using tmp = await tmpdir()
|
|
405
|
-
const dir = path.join(tmp.path, "locks")
|
|
406
|
-
const key = "flock:perm"
|
|
407
|
-
|
|
408
|
-
await fs.mkdir(dir, { recursive: true })
|
|
409
|
-
await fs.chmod(dir, 0o500)
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
const err = await Flock.withLock(key, async () => {}, {
|
|
413
|
-
dir,
|
|
414
|
-
staleMs: 100,
|
|
415
|
-
timeoutMs: 500,
|
|
416
|
-
}).catch((err) => err)
|
|
417
|
-
|
|
418
|
-
expect(err).toBeInstanceOf(Error)
|
|
419
|
-
if (!(err instanceof Error)) throw err
|
|
420
|
-
const text = err.message
|
|
421
|
-
expect(text.includes("EACCES") || text.includes("EPERM")).toBe(true)
|
|
422
|
-
} finally {
|
|
423
|
-
await fs.chmod(dir, 0o700)
|
|
424
|
-
}
|
|
425
|
-
})
|
|
426
|
-
})
|