@agoric/internal 0.3.3-dev-3dacdc3.0.3dacdc3 → 0.3.3-dev-3189429.0.3189429
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 +4 -4
- package/src/build-cache-types.d.ts +33 -0
- package/src/build-cache-types.d.ts.map +1 -0
- package/src/build-cache-types.js +42 -0
- package/src/build-cache.d.ts +12 -0
- package/src/build-cache.d.ts.map +1 -0
- package/src/build-cache.js +184 -0
- package/src/types-index.d.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/internal",
|
|
3
|
-
"version": "0.3.3-dev-
|
|
3
|
+
"version": "0.3.3-dev-3189429.0.3189429",
|
|
4
4
|
"description": "Externally unsupported utilities internal to agoric-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"lint:types": "yarn run -T tsc"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@agoric/base-zone": "0.1.1-dev-
|
|
23
|
+
"@agoric/base-zone": "0.1.1-dev-3189429.0.3189429",
|
|
24
24
|
"@endo/cache-map": "^1.1.0",
|
|
25
25
|
"@endo/common": "^1.2.13",
|
|
26
26
|
"@endo/compartment-mapper": "^1.6.3",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"jessie.js": "^0.3.4"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@agoric/cosmic-proto": "0.4.1-dev-
|
|
41
|
+
"@agoric/cosmic-proto": "0.4.1-dev-3189429.0.3189429",
|
|
42
42
|
"@endo/exo": "^1.5.12",
|
|
43
43
|
"@endo/init": "^1.1.12",
|
|
44
44
|
"@endo/ses-ava": "^1.3.2",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"typeCoverage": {
|
|
67
67
|
"atLeast": 92.84
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "3189429dea5ba7004a95cf73d8b6203b48f0b2e9"
|
|
70
70
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface LockLifecycleEvent {
|
|
2
|
+
key: string;
|
|
3
|
+
lockPath: string;
|
|
4
|
+
type: 'lock-acquired' | 'lock-released';
|
|
5
|
+
}
|
|
6
|
+
export interface LockWaitingEvent {
|
|
7
|
+
key: string;
|
|
8
|
+
lockPath: string;
|
|
9
|
+
type: 'lock-waiting';
|
|
10
|
+
waitedMs: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LockBrokenEvent {
|
|
13
|
+
ageMs?: number;
|
|
14
|
+
key: string;
|
|
15
|
+
lockPath: string;
|
|
16
|
+
ownerPid?: number;
|
|
17
|
+
reason: 'dead-owner' | 'stale-age';
|
|
18
|
+
staleLockMs?: number;
|
|
19
|
+
type: 'lock-broken';
|
|
20
|
+
}
|
|
21
|
+
export type BuildCacheEvent = LockLifecycleEvent | LockWaitingEvent | LockBrokenEvent;
|
|
22
|
+
export interface DirectoryLockPowers {
|
|
23
|
+
acquireTimeoutMs: number;
|
|
24
|
+
delayMs: (ms: number) => Promise<unknown>;
|
|
25
|
+
fs: Pick<typeof import('node:fs/promises'), 'mkdir' | 'readFile' | 'rm' | 'stat' | 'writeFile'>;
|
|
26
|
+
isPidAlive: (pid: number) => boolean;
|
|
27
|
+
lockRoot: string;
|
|
28
|
+
now: () => number;
|
|
29
|
+
onEvent?: (event: BuildCacheEvent) => void;
|
|
30
|
+
pid: number;
|
|
31
|
+
staleLockMs: number;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=build-cache-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-cache-types.d.ts","sourceRoot":"","sources":["build-cache-types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,eAAe,GAAG,eAAe,CAAC;CACzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,WAAW,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,aAAa,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,GACvB,kBAAkB,GAClB,gBAAgB,GAChB,eAAe,CAAC;AAEpB,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,EAAE,EAAE,IAAI,CACN,cAAc,kBAAkB,CAAC,EACjC,OAAO,GAAG,UAAU,GAAG,IAAI,GAAG,MAAM,GAAG,WAAW,CACnD,CAAC;IACF,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function makeDirectoryLock(powers: DirectoryLockPowers): {
|
|
2
|
+
withLock: (key: string, body: () => Promise<any>) => Promise<any>;
|
|
3
|
+
};
|
|
4
|
+
export function writeFileAtomic({ fs, filePath, data, now, pid }: {
|
|
5
|
+
fs: Pick<typeof import("node:fs/promises"), "rename" | "writeFile">;
|
|
6
|
+
filePath: string;
|
|
7
|
+
data: string;
|
|
8
|
+
now: () => number;
|
|
9
|
+
pid: number;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
import type { DirectoryLockPowers } from './build-cache-types.js';
|
|
12
|
+
//# sourceMappingURL=build-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-cache.d.ts","sourceRoot":"","sources":["build-cache.js"],"names":[],"mappings":"AAUO,0CAFI,mBAAmB;oBAkEjB,MAAM,QACN,MAAM,OAAO,CAAC,GAAG,CAAC;EAsF9B;AAgBM,kEAVI;IACN,EAAE,EAAE,IAAI,CAAC,iCAA0B,EAAE,QAAQ,GAAG,WAAW,CAAC,CAAC;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,iBASH;yCAlLsD,wBAAwB"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @import {BuildCacheEvent, DirectoryLockPowers} from './build-cache-types.js';
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {DirectoryLockPowers} powers
|
|
10
|
+
*/
|
|
11
|
+
export const makeDirectoryLock = powers => {
|
|
12
|
+
const {
|
|
13
|
+
fs,
|
|
14
|
+
delayMs,
|
|
15
|
+
now,
|
|
16
|
+
pid,
|
|
17
|
+
isPidAlive,
|
|
18
|
+
lockRoot,
|
|
19
|
+
staleLockMs,
|
|
20
|
+
acquireTimeoutMs,
|
|
21
|
+
onEvent = () => {},
|
|
22
|
+
} = powers;
|
|
23
|
+
const safeEmit = event => {
|
|
24
|
+
try {
|
|
25
|
+
onEvent(event);
|
|
26
|
+
} catch {
|
|
27
|
+
// Event sinks must not interfere with cache lock correctness.
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @param {string} dpath */
|
|
32
|
+
const mkdirUnlessExists = async dpath => {
|
|
33
|
+
await null; // avoid accidentally catching synchronous exceptions
|
|
34
|
+
try {
|
|
35
|
+
await fs.mkdir(dpath);
|
|
36
|
+
return true;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const e = /** @type {NodeJS.ErrnoException} */ (err);
|
|
39
|
+
if (e.code === 'EEXIST') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** @param {string} fpath */
|
|
47
|
+
const statUnlessMissing = async fpath => {
|
|
48
|
+
await null; // avoid accidentally catching synchronous exceptions
|
|
49
|
+
try {
|
|
50
|
+
return await fs.stat(fpath);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const e = /** @type {NodeJS.ErrnoException} */ (err);
|
|
53
|
+
if (e.code === 'ENOENT') {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** @param {string} fpath */
|
|
61
|
+
const readUnlessMissing = async fpath => {
|
|
62
|
+
await null; // avoid accidentally catching synchronous exceptions
|
|
63
|
+
try {
|
|
64
|
+
return await fs.readFile(fpath, 'utf8');
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const e = /** @type {NodeJS.ErrnoException} */ (err);
|
|
67
|
+
if (e.code === 'ENOENT') {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} key
|
|
76
|
+
* @param {() => Promise<any>} body
|
|
77
|
+
*/
|
|
78
|
+
const withLock = async (key, body) => {
|
|
79
|
+
const lockPath = path.join(lockRoot, `${encodeURIComponent(key)}.lock`);
|
|
80
|
+
const ownerPath = path.join(lockPath, 'owner.json');
|
|
81
|
+
await fs.mkdir(lockRoot, { recursive: true });
|
|
82
|
+
const started = now();
|
|
83
|
+
|
|
84
|
+
const maybeBreakStaleLock = async () => {
|
|
85
|
+
const ownerText = await readUnlessMissing(ownerPath);
|
|
86
|
+
if (ownerText) {
|
|
87
|
+
try {
|
|
88
|
+
const ownerInfo = JSON.parse(ownerText);
|
|
89
|
+
if (
|
|
90
|
+
ownerInfo &&
|
|
91
|
+
typeof ownerInfo === 'object' &&
|
|
92
|
+
Number.isInteger(ownerInfo.pid) &&
|
|
93
|
+
!isPidAlive(ownerInfo.pid)
|
|
94
|
+
) {
|
|
95
|
+
await fs.rm(lockPath, { recursive: true, force: true });
|
|
96
|
+
safeEmit({
|
|
97
|
+
type: 'lock-broken',
|
|
98
|
+
key,
|
|
99
|
+
lockPath,
|
|
100
|
+
reason: 'dead-owner',
|
|
101
|
+
ownerPid: ownerInfo.pid,
|
|
102
|
+
});
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// rely on age-based lock breaking for malformed owner files
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lockStats = await statUnlessMissing(lockPath);
|
|
111
|
+
if (!lockStats) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const ageMs = now() - lockStats.mtimeMs;
|
|
115
|
+
if (ageMs >= staleLockMs) {
|
|
116
|
+
await fs.rm(lockPath, { recursive: true, force: true });
|
|
117
|
+
safeEmit({
|
|
118
|
+
type: 'lock-broken',
|
|
119
|
+
key,
|
|
120
|
+
lockPath,
|
|
121
|
+
reason: 'stale-age',
|
|
122
|
+
ageMs,
|
|
123
|
+
staleLockMs,
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
for (;;) {
|
|
132
|
+
if (await mkdirUnlessExists(lockPath)) {
|
|
133
|
+
await fs.writeFile(
|
|
134
|
+
ownerPath,
|
|
135
|
+
JSON.stringify({ pid, createdAt: now() }),
|
|
136
|
+
'utf8',
|
|
137
|
+
);
|
|
138
|
+
safeEmit({ type: 'lock-acquired', key, lockPath });
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const recovered = await maybeBreakStaleLock();
|
|
142
|
+
if (recovered) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const waitedMs = now() - started;
|
|
146
|
+
safeEmit({ type: 'lock-waiting', key, lockPath, waitedMs });
|
|
147
|
+
if (waitedMs >= acquireTimeoutMs) {
|
|
148
|
+
throw Error(`Timed out waiting for cache lock ${lockPath}`);
|
|
149
|
+
}
|
|
150
|
+
await delayMs(20);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
return await body();
|
|
155
|
+
} finally {
|
|
156
|
+
await fs.rm(lockPath, { recursive: true, force: true });
|
|
157
|
+
safeEmit({ type: 'lock-released', key, lockPath });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return harden({ withLock });
|
|
162
|
+
};
|
|
163
|
+
harden(makeDirectoryLock);
|
|
164
|
+
|
|
165
|
+
let atomicWriteSequence = 0;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {{
|
|
169
|
+
* fs: Pick<import('node:fs/promises'), 'rename' | 'writeFile'>;
|
|
170
|
+
* filePath: string;
|
|
171
|
+
* data: string;
|
|
172
|
+
* now: () => number;
|
|
173
|
+
* pid: number;
|
|
174
|
+
* }} options
|
|
175
|
+
* @throws {NodeJS.ErrnoException} when a unique temp file cannot be created
|
|
176
|
+
* or the atomic rename fails.
|
|
177
|
+
*/
|
|
178
|
+
export const writeFileAtomic = async ({ fs, filePath, data, now, pid }) => {
|
|
179
|
+
atomicWriteSequence += 1;
|
|
180
|
+
const tempPath = `${filePath}.${pid}.${now()}.${atomicWriteSequence}.tmp`;
|
|
181
|
+
await fs.writeFile(tempPath, data, { flag: 'wx' });
|
|
182
|
+
await fs.rename(tempPath, filePath);
|
|
183
|
+
};
|
|
184
|
+
harden(writeFileAtomic);
|
package/src/types-index.d.ts
CHANGED