querysub 0.451.0 → 0.453.0
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/.claude/settings.local.json +12 -1
- package/bin/join-public.js +1 -0
- package/package.json +1 -1
- package/src/-a-archives/archiveCache.ts +53 -597
- package/src/-g-core-values/NodeCapabilities.ts +29 -28
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +24 -0
- package/src/0-path-value-core/pathValueCore.ts +1 -1
- package/src/2-proxy/PathValueProxyWatcher.ts +6 -6
- package/src/4-querysub/Querysub.ts +15 -13
- package/src/archiveapps/archiveGCEntry.tsx +1 -0
- package/src/archiveapps/archiveJoinEntry.ts +8 -2
- package/src/deployManager/LaunchTrackingHeader.tsx +65 -0
- package/src/deployManager/machineApplyMainCode.ts +140 -15
- package/src/deployManager/machineSchema.ts +82 -1
- package/src/diagnostics/NodeConnectionsPage.tsx +1 -1
- package/src/diagnostics/NodeViewer.tsx +15 -25
- package/src/diagnostics/debugger/mcp-server.ts +327 -53
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +64 -22
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +32 -1
- package/src/diagnostics/managementPages.tsx +8 -0
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +113 -0
- package/src/diagnostics/pathAuditer.ts +0 -6
- package/test.ts +2 -1
- package/src/misc/getParentProcessId.cs +0 -53
- package/src/misc/getParentProcessId.ts +0 -53
|
@@ -1,35 +1,50 @@
|
|
|
1
|
-
import { getStorageDir, getSubFolder } from "../fs";
|
|
2
1
|
import { Archives, createArchivesOverride } from "./archives";
|
|
3
2
|
import fs from "fs";
|
|
4
3
|
import os from "os";
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { nextId, sort, timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
7
6
|
import { cache, lazy } from "socket-function/src/caching";
|
|
8
7
|
import { runInParallel, runInSerial, runInfinitePoll } from "socket-function/src/batching";
|
|
9
8
|
import { sha256 } from "js-sha256";
|
|
10
|
-
import child_process from "child_process";
|
|
11
|
-
import { getPPID } from "../misc/getParentProcessId";
|
|
12
9
|
import { Args } from "socket-function/src/types";
|
|
13
|
-
import { getArchivesBackblaze } from "./archivesBackBlaze";
|
|
14
|
-
import { formatNumber } from "socket-function/src/formatting/format";
|
|
15
10
|
import { SizeLimiter } from "../diagnostics/SizeLimiter";
|
|
16
11
|
import { isPublic } from "../config";
|
|
17
|
-
import { measureWrap } from "socket-function/src/profiling/measure";
|
|
18
12
|
|
|
19
|
-
const SIZE_LIMIT = new SizeLimiter({
|
|
20
|
-
diskRoot:
|
|
13
|
+
const SIZE_LIMIT = lazy(() => new SizeLimiter({
|
|
14
|
+
diskRoot: getCacheDir(),
|
|
21
15
|
maxBytes: isPublic() ? 1024 * 1024 * 1024 * 250 : 1024 * 1024 * 1024 * 100,
|
|
22
16
|
// Anything less than this and we can't even load enough weights models for a single task
|
|
23
17
|
minBytes: 1024 * 1024 * 1024 * 8,
|
|
24
18
|
maxDiskFraction: 0.3,
|
|
25
|
-
|
|
19
|
+
// Add margin, as multiple processes don't sync the cache state often.
|
|
20
|
+
maxTotalDiskFraction: 0.75,
|
|
26
21
|
maxFiles: 1000 * 25,
|
|
27
|
-
});
|
|
22
|
+
}));
|
|
28
23
|
|
|
29
24
|
const UPDATE_METRICS_INTERVAL = timeInMinute * 30;
|
|
30
|
-
const
|
|
25
|
+
const ATIME_BUMP_INTERVAL = timeInMinute * 5;
|
|
26
|
+
|
|
27
|
+
const lastAtimeBump = new Map<string, number>();
|
|
28
|
+
function bumpAtimeIfStale(path: string) {
|
|
29
|
+
let now = Date.now();
|
|
30
|
+
let last = lastAtimeBump.get(path) ?? 0;
|
|
31
|
+
if (now - last < ATIME_BUMP_INTERVAL) return;
|
|
32
|
+
lastAtimeBump.set(path, now);
|
|
33
|
+
let when = new Date(now);
|
|
34
|
+
fs.promises.utimes(path, when, when).catch((e: any) => {
|
|
35
|
+
if (e?.code === "ENOENT") return;
|
|
36
|
+
console.warn(`Failed to bump atime for ${path}:`, (e as Error).stack ?? e);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getCacheDir = lazy(() => {
|
|
41
|
+
let dir = os.homedir().replaceAll("\\", "/") + "/.querysub-cache/";
|
|
42
|
+
if (!fs.existsSync(dir)) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
return dir;
|
|
46
|
+
});
|
|
31
47
|
|
|
32
|
-
const LOCK_SUFFIX = ".lock";
|
|
33
48
|
const TEMP_SUFFIX = ".tmp";
|
|
34
49
|
const TEMP_THRESHOLD = timeInHour * 3;
|
|
35
50
|
|
|
@@ -47,7 +62,7 @@ export function getArchiveCachePath(archives: Archives, key: string): string {
|
|
|
47
62
|
let name = fullFileName.replace(/[<>:"\/\\|?*\x00-\x1F]/g, "_");
|
|
48
63
|
// Remove spaces, as I think they are causing some issues if they are at the end
|
|
49
64
|
name = name.replaceAll(" ", "");
|
|
50
|
-
return
|
|
65
|
+
return getCacheDir() + hash.slice(0, 16) + "." + name.slice(0, 128) + CACHE_SUFFIX;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
const getDiskMetricsBase = async () => {
|
|
@@ -61,19 +76,9 @@ const getDiskMetricsBase = async () => {
|
|
|
61
76
|
|
|
62
77
|
let usedCacheBytes = 0;
|
|
63
78
|
let usedCacheFiles = 0;
|
|
64
|
-
let cacheFiles = await fs.promises.readdir(
|
|
79
|
+
let cacheFiles = await fs.promises.readdir(getCacheDir());
|
|
65
80
|
async function processFile(file: string) {
|
|
66
|
-
if (file.endsWith(
|
|
67
|
-
let base = file.slice(0, -LOCK_SUFFIX.length);
|
|
68
|
-
if (!fs.existsSync(base) && !await isLocked(cacheArchives2 + base)) {
|
|
69
|
-
try {
|
|
70
|
-
// NOTE: This races, it might not be locked when we check, but a lock
|
|
71
|
-
// might be added right here. But... what else can we do? We need
|
|
72
|
-
// to cleanup unused locks at some point...
|
|
73
|
-
await fs.promises.unlink(cacheArchives2 + file);
|
|
74
|
-
} catch { }
|
|
75
|
-
}
|
|
76
|
-
} else if (file.endsWith(CACHE_SUFFIX)) {
|
|
81
|
+
if (file.endsWith(CACHE_SUFFIX)) {
|
|
77
82
|
let info: fs.Stats | undefined;
|
|
78
83
|
try {
|
|
79
84
|
info = await fs.promises.stat(file);
|
|
@@ -90,11 +95,11 @@ const getDiskMetricsBase = async () => {
|
|
|
90
95
|
try {
|
|
91
96
|
// TEMP files, and... any files?
|
|
92
97
|
// If it's too old, delete it
|
|
93
|
-
let stat = await fs.promises.stat(
|
|
98
|
+
let stat = await fs.promises.stat(getCacheDir() + file);
|
|
94
99
|
let threshold = Date.now() - TEMP_THRESHOLD;
|
|
95
100
|
if (stat.mtimeMs < threshold) {
|
|
96
101
|
try {
|
|
97
|
-
await fs.promises.unlink(
|
|
102
|
+
await fs.promises.unlink(getCacheDir() + file);
|
|
98
103
|
} catch { }
|
|
99
104
|
}
|
|
100
105
|
// If we can't stat it, someone else deleted it, so that's fine...
|
|
@@ -104,10 +109,10 @@ const getDiskMetricsBase = async () => {
|
|
|
104
109
|
let processFileParallel = runInParallel({ parallelCount: 32 }, processFile);
|
|
105
110
|
await Promise.all(cacheFiles.map(processFileParallel));
|
|
106
111
|
|
|
107
|
-
let { remove, availableBytes, availableFiles } = await SIZE_LIMIT.limit(fileSizes);
|
|
112
|
+
let { remove, availableBytes, availableFiles } = await SIZE_LIMIT().limit(fileSizes);
|
|
108
113
|
for (let file of remove) {
|
|
109
114
|
try {
|
|
110
|
-
await fs.promises.unlink(
|
|
115
|
+
await fs.promises.unlink(getCacheDir() + file.path);
|
|
111
116
|
} catch { }
|
|
112
117
|
}
|
|
113
118
|
|
|
@@ -138,19 +143,25 @@ const getDiskMetricsBase = async () => {
|
|
|
138
143
|
let sourceTempFileStat = await fs.promises.stat(sourceTempFile);
|
|
139
144
|
availableBytes -= sourceTempFileStat.size;
|
|
140
145
|
availableFiles--;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
sort(fileSizes, x => x.time);
|
|
147
|
+
for (let i = 0; i < fileSizes.length;) {
|
|
148
|
+
if (availableBytes >= 0 && availableFiles >= 0) break;
|
|
149
|
+
let file = fileSizes[i];
|
|
150
|
+
let deleted = false;
|
|
151
|
+
try {
|
|
152
|
+
await fs.promises.unlink(file.path);
|
|
153
|
+
deleted = true;
|
|
154
|
+
} catch (e: any) {
|
|
155
|
+
if (e?.code === "ENOENT") {
|
|
156
|
+
deleted = true;
|
|
157
|
+
} else {
|
|
158
|
+
console.warn(`Failed to evict cache file ${file.path}:`, (e as Error).stack ?? e);
|
|
146
159
|
}
|
|
147
160
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (await deleteCacheFile(leastRecentlyAccessed.path)) {
|
|
153
|
-
removeFile(leastRecentlyAccessed.path);
|
|
161
|
+
if (deleted) {
|
|
162
|
+
removeFile(file.path);
|
|
163
|
+
} else {
|
|
164
|
+
i++;
|
|
154
165
|
}
|
|
155
166
|
}
|
|
156
167
|
fileSizes.push({
|
|
@@ -200,15 +211,10 @@ const getDiskMetricsBase = async () => {
|
|
|
200
211
|
}
|
|
201
212
|
if (buffer) {
|
|
202
213
|
updateAccessTime(path);
|
|
214
|
+
bumpAtimeIfStale(path);
|
|
203
215
|
}
|
|
204
216
|
return buffer;
|
|
205
217
|
}
|
|
206
|
-
async function getPathAndLock(archives: Archives, key: string): Promise<string> {
|
|
207
|
-
let path = getArchiveCachePath(archives, key);
|
|
208
|
-
await lockFile(path);
|
|
209
|
-
updateAccessTime(path);
|
|
210
|
-
return path;
|
|
211
|
-
}
|
|
212
218
|
async function delCacheFile(archives: Archives, key: string): Promise<void> {
|
|
213
219
|
let path = getArchiveCachePath(archives, key);
|
|
214
220
|
if (removeFile(path)) {
|
|
@@ -225,7 +231,6 @@ const getDiskMetricsBase = async () => {
|
|
|
225
231
|
addCacheFile: runInSerial(addCacheFile),
|
|
226
232
|
getCacheFile,
|
|
227
233
|
delCacheFile,
|
|
228
|
-
getPathAndLock,
|
|
229
234
|
};
|
|
230
235
|
};
|
|
231
236
|
let curSeqNum = 1;
|
|
@@ -242,23 +247,6 @@ const ensureDiskMetricsUpdated = lazy(() => {
|
|
|
242
247
|
});
|
|
243
248
|
});
|
|
244
249
|
|
|
245
|
-
export type LockFncs = {
|
|
246
|
-
// Get a path to the file locally, locking it so it won't be garbage collected until lockRegion completes
|
|
247
|
-
// - If the file isn't in the archives, returns undefined, and doesn't lock it
|
|
248
|
-
getPathAndLock(fileName: string): Promise<string | undefined>;
|
|
249
|
-
// Gets it, and only checks the cache, not the archives. Faster, but means it might be deleted in
|
|
250
|
-
// the archives and we will still use it locally.
|
|
251
|
-
getPathAndLockFast(fileName: string): Promise<string | undefined>;
|
|
252
|
-
getPathAndLockCacheOnly(fileName: string): Promise<string | undefined>;
|
|
253
|
-
// MOVES the sourceFileName to the archives (so sourceFileName will be deleted after this runs),
|
|
254
|
-
// and locks the archives file until lockRegion completes
|
|
255
|
-
moveFileAndLock(config: {
|
|
256
|
-
archivesFilename: string;
|
|
257
|
-
sourceFileName: string;
|
|
258
|
-
onlyCache?: boolean;
|
|
259
|
-
}): Promise<string>;
|
|
260
|
-
};
|
|
261
|
-
|
|
262
250
|
let cacheArchivesSymbol = Symbol("cacheArchives");
|
|
263
251
|
/** IMPORTANT! The cache assumes the files contents immutable, and they will only be created
|
|
264
252
|
* and deleted, never mutated.
|
|
@@ -266,14 +254,6 @@ let cacheArchivesSymbol = Symbol("cacheArchives");
|
|
|
266
254
|
export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
|
|
267
255
|
immutable?: boolean;
|
|
268
256
|
}): Archives & {
|
|
269
|
-
// NOTE: lockRegion / path based functions are preferred for external accesses, as they ensure files
|
|
270
|
-
// won't be garbage collected, and uses paths, which will be required for external processes.
|
|
271
|
-
// - Locks only protect the local cache. The values can still be deleted explicitly.
|
|
272
|
-
lockRegion<T>(
|
|
273
|
-
code: (
|
|
274
|
-
fncs: LockFncs
|
|
275
|
-
) => Promise<T>
|
|
276
|
-
): Promise<T>;
|
|
277
257
|
debugGetPath(key: string): string;
|
|
278
258
|
} {
|
|
279
259
|
if (cacheArchivesSymbol in archives) {
|
|
@@ -303,7 +283,6 @@ export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
|
|
|
303
283
|
await metrics.addCacheFile(archives, config.path, tempPath);
|
|
304
284
|
let cachePath = getArchiveCachePath(archives, config.path);
|
|
305
285
|
|
|
306
|
-
await lockFile(cachePath);
|
|
307
286
|
let pos = 0;
|
|
308
287
|
let cacheHandle: fs.promises.FileHandle = await fs.promises.open(cachePath, "r");
|
|
309
288
|
let data = Buffer.alloc(LARGE_FILE_CHUNK);
|
|
@@ -329,185 +308,8 @@ export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
|
|
|
329
308
|
});
|
|
330
309
|
} finally {
|
|
331
310
|
await cacheHandle.close();
|
|
332
|
-
await unlockFile(cachePath);
|
|
333
311
|
}
|
|
334
312
|
}
|
|
335
|
-
function createGetPathAndLock(locked: string[]) {
|
|
336
|
-
return async function getPathAndLock(fileName: string) {
|
|
337
|
-
const info = await archives.getInfo(fileName);
|
|
338
|
-
if (!info) return undefined;
|
|
339
|
-
|
|
340
|
-
let path = getArchiveCachePath(archives, fileName);
|
|
341
|
-
await lockFile(path);
|
|
342
|
-
locked.push(path);
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
let cacheStat = await fs.promises.stat(path);
|
|
346
|
-
// NOTE: We check the size, and not just the existence, in case the file
|
|
347
|
-
// is partially populated? Even though this isn't a real thing, as we rename
|
|
348
|
-
// when we copy it...
|
|
349
|
-
if (cacheStat.size === info.size) {
|
|
350
|
-
return path;
|
|
351
|
-
}
|
|
352
|
-
} catch {
|
|
353
|
-
// It doesn't exist, so read it in
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
let startRead = Date.now();
|
|
357
|
-
|
|
358
|
-
let readPos = 0;
|
|
359
|
-
let getNextDataBase = async (): Promise<Buffer | undefined> => {
|
|
360
|
-
if (readPos >= info.size) return undefined;
|
|
361
|
-
let curPos = readPos;
|
|
362
|
-
readPos += LARGE_FILE_CHUNK;
|
|
363
|
-
let end = Math.min(readPos, info.size);
|
|
364
|
-
let data = await archives.get(fileName, { range: { start: curPos, end } });
|
|
365
|
-
if (!data?.length) return undefined;
|
|
366
|
-
return data;
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
let nextWriteIndex = 0;
|
|
370
|
-
let buffers: (Buffer | { error: Error } | null | undefined | (() => void))[] = [];
|
|
371
|
-
|
|
372
|
-
async function getThread() {
|
|
373
|
-
while (true) {
|
|
374
|
-
let index = nextWriteIndex++;
|
|
375
|
-
let next: typeof buffers[number];
|
|
376
|
-
try {
|
|
377
|
-
next = await getNextDataBase();
|
|
378
|
-
} catch (e: any) {
|
|
379
|
-
next = { error: e };
|
|
380
|
-
}
|
|
381
|
-
let prev = buffers[index];
|
|
382
|
-
buffers[index] = next || null;
|
|
383
|
-
if (typeof prev === "function") {
|
|
384
|
-
prev();
|
|
385
|
-
}
|
|
386
|
-
if (!next || "error" in next) break;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
// Read in parallel, for faster read times, and so we can read from backblaze
|
|
390
|
-
// while writing to disk.
|
|
391
|
-
void list(8).map(getThread);
|
|
392
|
-
|
|
393
|
-
let nextReadIndex = 0;
|
|
394
|
-
let getNextData = async (): Promise<Buffer | undefined> => {
|
|
395
|
-
let index = nextReadIndex++;
|
|
396
|
-
if (buffers[index] === null) return undefined;
|
|
397
|
-
if (!buffers[index]) {
|
|
398
|
-
await new Promise<void>(resolve => {
|
|
399
|
-
buffers[index] = resolve;
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
let result = buffers[index] as any;
|
|
403
|
-
// Clear it, so we don't store all buffers in memory
|
|
404
|
-
buffers[index] = undefined;
|
|
405
|
-
return result;
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
let size = 0;
|
|
409
|
-
const tempPath = getTempFilePath();
|
|
410
|
-
let handle: fs.promises.FileHandle | undefined;
|
|
411
|
-
try {
|
|
412
|
-
handle = await fs.promises.open(tempPath, "w");
|
|
413
|
-
while (true) {
|
|
414
|
-
let data = await getNextData();
|
|
415
|
-
if (!data?.length) break;
|
|
416
|
-
await handle.write(data, 0, data.length, size);
|
|
417
|
-
size += data.length;
|
|
418
|
-
}
|
|
419
|
-
} finally {
|
|
420
|
-
if (handle) {
|
|
421
|
-
try {
|
|
422
|
-
await handle.close();
|
|
423
|
-
} catch { }
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
let totalRead = Date.now() - startRead;
|
|
427
|
-
console.log(`Read ${formatNumber(info.size)}B at ${formatNumber(info.size / totalRead)}B/s into cache (${fileName})`);
|
|
428
|
-
|
|
429
|
-
let metrics = await getDiskMetrics();
|
|
430
|
-
await metrics.addCacheFile(archives, fileName, tempPath);
|
|
431
|
-
|
|
432
|
-
return path;
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
function createGetPathAndLockFast(locked: string[]) {
|
|
436
|
-
return async function getPathAndLockFast(fileName: string) {
|
|
437
|
-
let path = getArchiveCachePath(archives, fileName);
|
|
438
|
-
await lockFile(path);
|
|
439
|
-
locked.push(path);
|
|
440
|
-
try {
|
|
441
|
-
// If it exists in the cache, just return the path. Otherwise, we might have to read it in
|
|
442
|
-
await fs.promises.stat(path);
|
|
443
|
-
return path;
|
|
444
|
-
} catch {
|
|
445
|
-
return createGetPathAndLock(locked)(fileName);
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
function createMovePathFromFileAndLock(locked: string[]) {
|
|
450
|
-
return async function movePathFromFileAndLock(config: {
|
|
451
|
-
archivesFilename: string;
|
|
452
|
-
sourceFileName: string;
|
|
453
|
-
onlyCache?: boolean;
|
|
454
|
-
}): Promise<string> {
|
|
455
|
-
let { archivesFilename, sourceFileName } = config;
|
|
456
|
-
if (!config.onlyCache) {
|
|
457
|
-
// NOTE: While we COULD use a rename to quickly move the file... we have to at least copy
|
|
458
|
-
// it to the underlying archives, so... moving it piece by piece is fine...
|
|
459
|
-
|
|
460
|
-
let handle: fs.promises.FileHandle = await fs.promises.open(sourceFileName, "r");
|
|
461
|
-
let data = Buffer.alloc(LARGE_FILE_CHUNK);
|
|
462
|
-
let pos = 0;
|
|
463
|
-
async function getNextData(): Promise<Buffer | undefined> {
|
|
464
|
-
try {
|
|
465
|
-
let read = await handle.read(data, 0, LARGE_FILE_CHUNK, pos);
|
|
466
|
-
if (read.bytesRead === 0) return undefined;
|
|
467
|
-
if (read.bytesRead < LARGE_FILE_CHUNK) {
|
|
468
|
-
data = data.slice(0, read.bytesRead);
|
|
469
|
-
}
|
|
470
|
-
pos += read.bytesRead;
|
|
471
|
-
return data;
|
|
472
|
-
} catch {
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
try {
|
|
477
|
-
await setLargeFile({
|
|
478
|
-
path: archivesFilename,
|
|
479
|
-
getNextData,
|
|
480
|
-
});
|
|
481
|
-
await handle.close();
|
|
482
|
-
handle = undefined as any;
|
|
483
|
-
await fs.promises.unlink(sourceFileName);
|
|
484
|
-
} finally {
|
|
485
|
-
if (handle) {
|
|
486
|
-
await handle.close();
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
} else {
|
|
490
|
-
await fs.promises.rename(sourceFileName, getArchiveCachePath(archives, archivesFilename));
|
|
491
|
-
}
|
|
492
|
-
let path = getArchiveCachePath(archives, archivesFilename);
|
|
493
|
-
await lockFile(path);
|
|
494
|
-
locked.push(path);
|
|
495
|
-
return path;
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
function createGetPathAndLockCacheOnly(locked: string[]) {
|
|
499
|
-
return async function getPathAndLockCacheOnly(fileName: string) {
|
|
500
|
-
let path = getArchiveCachePath(archives, fileName);
|
|
501
|
-
await lockFile(path);
|
|
502
|
-
locked.push(path);
|
|
503
|
-
try {
|
|
504
|
-
await fs.promises.stat(path);
|
|
505
|
-
return path;
|
|
506
|
-
} catch {
|
|
507
|
-
return undefined;
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
313
|
function debugGetPath(key: string) {
|
|
512
314
|
return getArchiveCachePath(archives, key);
|
|
513
315
|
}
|
|
@@ -592,351 +394,5 @@ export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
|
|
|
592
394
|
await archives.move(config);
|
|
593
395
|
},
|
|
594
396
|
getBaseArchives: () => archives.getBaseArchives?.() ?? ({ archives: archives, parentPath: "" }),
|
|
595
|
-
|
|
596
|
-
async lockRegion<T>(code: (fncs: LockFncs) => Promise<T>) {
|
|
597
|
-
let locked: string[] = [];
|
|
598
|
-
let fncs = {
|
|
599
|
-
getPathAndLock: createGetPathAndLock(locked),
|
|
600
|
-
getPathAndLockFast: createGetPathAndLockFast(locked),
|
|
601
|
-
moveFileAndLock: createMovePathFromFileAndLock(locked),
|
|
602
|
-
getPathAndLockCacheOnly: createGetPathAndLockCacheOnly(locked),
|
|
603
|
-
};
|
|
604
|
-
try {
|
|
605
|
-
return await code(fncs);
|
|
606
|
-
} finally {
|
|
607
|
-
for (let path of locked) {
|
|
608
|
-
await unlockFile(path);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
function lockFilePath(path: string): string {
|
|
617
|
-
return path + LOCK_SUFFIX;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function getUniqueId() {
|
|
621
|
-
return process.pid + " " + process.ppid;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
let lockCache = new Map<string, number>();
|
|
625
|
-
async function lockFile(path: string): Promise<void> {
|
|
626
|
-
// NOTE: If we don't stop ourself from locking it multiple times, a single process
|
|
627
|
-
// could use up the fill lock file limit, which would break the system.
|
|
628
|
-
let prevCount = lockCache.get(path);
|
|
629
|
-
if (prevCount) {
|
|
630
|
-
lockCache.set(path, prevCount + 1);
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
if (!prevCount) {
|
|
634
|
-
const lockPath = lockFilePath(path);
|
|
635
|
-
// NOTE: Locking is taking WAY too long, so... we're not going to wait. This should still
|
|
636
|
-
// be mostly fine, due to how we maintain transactions files.
|
|
637
|
-
lockRetryLoop(async function appendFile() {
|
|
638
|
-
await fs.promises.appendFile(lockPath, "lock " + getUniqueId() + "\n");
|
|
639
|
-
}).catch(e => console.error("Error appending file lock for", path, e));
|
|
640
|
-
}
|
|
641
|
-
prevCount = lockCache.get(path) || 0;
|
|
642
|
-
lockCache.set(path, prevCount + 1);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async function unlockFile(path: string): Promise<void> {
|
|
646
|
-
let prevCount = lockCache.get(path);
|
|
647
|
-
if (!prevCount) {
|
|
648
|
-
console.warn(`Unlocking a file that wasn't locked: ${path}`);
|
|
649
|
-
} else {
|
|
650
|
-
prevCount--;
|
|
651
|
-
lockCache.set(path, prevCount);
|
|
652
|
-
if (prevCount <= 0) {
|
|
653
|
-
lockCache.delete(path);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (!prevCount) {
|
|
657
|
-
await isLocked(path, { type: "unlock", lock: "lock " + getUniqueId() });
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
async function lockRetryLoop<T>(code: () => Promise<T>): Promise<T> {
|
|
662
|
-
while (true) {
|
|
663
|
-
try {
|
|
664
|
-
return await code();
|
|
665
|
-
} catch (error: any) {
|
|
666
|
-
if (error.code === "EBUSY") {
|
|
667
|
-
console.log("Lock file busy, retrying in 1 second");
|
|
668
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
throw error;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
async function getHandle(path: string) {
|
|
677
|
-
while (true) {
|
|
678
|
-
try {
|
|
679
|
-
return await fs.promises.open(path, "r+");
|
|
680
|
-
} catch (error: any) {
|
|
681
|
-
if (error.code === "ENOENT") {
|
|
682
|
-
try {
|
|
683
|
-
return await fs.promises.open(path, "wx+");
|
|
684
|
-
} catch (error: any) {
|
|
685
|
-
if (error.code === "EEXIST") {
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
throw error;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
throw error;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
/** NOTE: This inherently has a race condition, as anything you do based on isLocked
|
|
698
|
-
* could be invalidated by the time you do it. But... this should be good enough
|
|
699
|
-
* for our use case...
|
|
700
|
-
*/
|
|
701
|
-
async function isLocked(path: string, operation?: {
|
|
702
|
-
type: "delete";
|
|
703
|
-
} | {
|
|
704
|
-
type: "unlock";
|
|
705
|
-
lock: string;
|
|
706
|
-
}): Promise<number> {
|
|
707
|
-
const lockPath = lockFilePath(path);
|
|
708
|
-
|
|
709
|
-
async function filterToValidState(locks: string[]): Promise<string[]> {
|
|
710
|
-
let existingLocks: string[] = [];
|
|
711
|
-
for (let lock of locks) {
|
|
712
|
-
if (lock.startsWith("lock ")) {
|
|
713
|
-
let [_, pid, ppid] = lock.split(" ");
|
|
714
|
-
const invalid = (
|
|
715
|
-
!await isProcessAlive(+pid)
|
|
716
|
-
|| !await isProcessAlive(+ppid)
|
|
717
|
-
|| await getPPID(+pid) !== +ppid
|
|
718
|
-
);
|
|
719
|
-
await getPPID(+pid);
|
|
720
|
-
if (!invalid) {
|
|
721
|
-
existingLocks.push(lock);
|
|
722
|
-
}
|
|
723
|
-
} else {
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
return Array.from(existingLocks);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
return await lockRetryLoop(async function isLocked() {
|
|
731
|
-
let handle = await getHandle(lockPath);
|
|
732
|
-
try {
|
|
733
|
-
const { flock } = await import("fs-ext");
|
|
734
|
-
await new Promise<void>((r, e) => flock(handle.fd, "ex", err => err ? e(err) : r()));
|
|
735
|
-
let contents = (await handle.readFile()).toString();
|
|
736
|
-
let locks = contents.toString().replaceAll("\0", "").trim().split("\n").filter(x => x);
|
|
737
|
-
let validLocks = await filterToValidState(locks);
|
|
738
|
-
if (operation?.type === "unlock") {
|
|
739
|
-
let index = validLocks.indexOf(operation.lock);
|
|
740
|
-
if (index >= 0) {
|
|
741
|
-
validLocks.splice(index, 1);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (validLocks.length !== locks.length) {
|
|
745
|
-
//console.log(`Lock count changed from ${locks.length} to ${validLocks.length}`);
|
|
746
|
-
let newContents = Buffer.from(validLocks.map(x => x + "\n").join(""));
|
|
747
|
-
if (newContents.length < contents.length) {
|
|
748
|
-
newContents = Buffer.concat([newContents, Buffer.alloc(contents.length - newContents.length)]);
|
|
749
|
-
}
|
|
750
|
-
await handle.write(newContents, 0, newContents.length, 0);
|
|
751
|
-
} else {
|
|
752
|
-
//console.log(`Lock count unchanged: ${validLocks.length}`);
|
|
753
|
-
}
|
|
754
|
-
if (operation?.type === "delete") {
|
|
755
|
-
if (validLocks.length === 0) {
|
|
756
|
-
await fs.promises.unlink(path);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
return validLocks.length;
|
|
760
|
-
} catch (error: any) {
|
|
761
|
-
if (error.code === "ENOENT") {
|
|
762
|
-
return 0; // Lock file doesn't exist
|
|
763
|
-
}
|
|
764
|
-
throw error; // Unexpected error, rethrow
|
|
765
|
-
} finally {
|
|
766
|
-
await handle.close();
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
async function execPromise(command: string, args: string[]): Promise<string> {
|
|
771
|
-
return new Promise((resolve, reject) => {
|
|
772
|
-
child_process.execFile(command, args, (error, stdout, stderr) => {
|
|
773
|
-
if (error) {
|
|
774
|
-
reject(error);
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
resolve(stdout);
|
|
778
|
-
});
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
async function isProcessAlive(pid: number): Promise<boolean> {
|
|
784
|
-
try {
|
|
785
|
-
process.kill(pid, 0);
|
|
786
|
-
return true;
|
|
787
|
-
} catch (error) {
|
|
788
|
-
return false;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
async function atomicRead(path: string) {
|
|
794
|
-
while (true) {
|
|
795
|
-
let stat0 = fs.statSync(path);
|
|
796
|
-
let contents = fs.readFileSync(path);
|
|
797
|
-
let stat1 = fs.statSync(path);
|
|
798
|
-
if (stat0.mtimeMs === stat1.mtimeMs) {
|
|
799
|
-
return contents;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
async function lockCacheFile(archives: Archives, key: string): Promise<void> {
|
|
805
|
-
await lockFile(getArchiveCachePath(archives, key));
|
|
806
|
-
}
|
|
807
|
-
async function unlockCacheFile(archives: Archives, key: string): Promise<void> {
|
|
808
|
-
await unlockFile(getArchiveCachePath(archives, key));
|
|
809
|
-
}
|
|
810
|
-
// Returns true if it was deleted
|
|
811
|
-
async function deleteCacheFile(path: string): Promise<boolean> {
|
|
812
|
-
let cannotDelete = await isLocked(path, { type: "delete" });
|
|
813
|
-
return !cannotDelete;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// for i in {1..100}; do yarn typenode src/-b-archives/archiveCache.ts & done
|
|
817
|
-
|
|
818
|
-
async function testLocks() {
|
|
819
|
-
let testFiles = ["a", "b", "c"].map(x => cacheArchives2 + x);
|
|
820
|
-
function getAFile() {
|
|
821
|
-
let file = testFiles[Math.floor(Math.random() * testFiles.length)];
|
|
822
|
-
console.log(file);
|
|
823
|
-
return file;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function assert(condition: unknown, message: string) {
|
|
827
|
-
if (!condition) {
|
|
828
|
-
throw new Error(message);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
async function test1() {
|
|
833
|
-
let file = getAFile();
|
|
834
|
-
await lockFile(file);
|
|
835
|
-
let locked = await isLocked(file);
|
|
836
|
-
assert(locked, "File should be locked");
|
|
837
|
-
await new Promise(r => setTimeout(r, 100));
|
|
838
|
-
await unlockFile(file);
|
|
839
|
-
console.log(await fs.promises.readFile(file + LOCK_SUFFIX, "utf8"));
|
|
840
|
-
}
|
|
841
|
-
async function test2() {
|
|
842
|
-
let file = getAFile();
|
|
843
|
-
let notDeleted = await isLocked(file, { type: "delete" });
|
|
844
|
-
if (notDeleted) {
|
|
845
|
-
console.log("File was not deleted, as it was locked");
|
|
846
|
-
} else {
|
|
847
|
-
console.log("File was deleted");
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
async function test3() {
|
|
851
|
-
let count = 10;
|
|
852
|
-
let file = getAFile();
|
|
853
|
-
console.log(`Lock count before: ${await isLocked(file)}`);
|
|
854
|
-
for (let i = 0; i < count; i++) {
|
|
855
|
-
await lockFile(file);
|
|
856
|
-
}
|
|
857
|
-
for (let i = 0; i < count; i++) {
|
|
858
|
-
await unlockFile(file);
|
|
859
|
-
}
|
|
860
|
-
console.log(`Lock count after: ${await isLocked(file)}`);
|
|
861
|
-
}
|
|
862
|
-
async function test4() {
|
|
863
|
-
let file = getAFile();
|
|
864
|
-
await lockFile(file);
|
|
865
|
-
let notDeleted = await isLocked(file, { type: "delete" });
|
|
866
|
-
assert(notDeleted, "File should not be deleted if it is locked");
|
|
867
|
-
await unlockFile(file);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
async function runRandomTest() {
|
|
872
|
-
let testFn = [test1, test2, test3, test4][Math.floor(Math.random() * 4)];
|
|
873
|
-
console.log(`Running test: ${testFn.name}`);
|
|
874
|
-
await testFn();
|
|
875
|
-
}
|
|
876
|
-
for (let i = 0; i < 1000; i++) {
|
|
877
|
-
await runRandomTest();
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
//testLocks().catch(console.error).finally(() => process.exit(0));
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
async function testLargeFiles() {
|
|
884
|
-
const largeFile = "E:/downloads/Weird.Science.1985.EXTENDED.1080p.BluRay.H264.AAC-RARBG/weirdscience.mp4";
|
|
885
|
-
let test = wrapArchivesWithCache(getArchivesBackblaze("querysub.com-testbucket"));
|
|
886
|
-
|
|
887
|
-
/*
|
|
888
|
-
let cacheHandle: fs.promises.FileHandle = await fs.promises.open(largeFile, "r");
|
|
889
|
-
let data = Buffer.alloc(LARGE_FILE_CHUNK);
|
|
890
|
-
let pos = 0;
|
|
891
|
-
async function getNextData(): Promise<Buffer | undefined> {
|
|
892
|
-
try {
|
|
893
|
-
let read = await cacheHandle.read(data, 0, LARGE_FILE_CHUNK, pos);
|
|
894
|
-
if (read.bytesRead === 0) return undefined;
|
|
895
|
-
if (read.bytesRead < LARGE_FILE_CHUNK) {
|
|
896
|
-
data = data.slice(0, read.bytesRead);
|
|
897
|
-
}
|
|
898
|
-
pos += read.bytesRead;
|
|
899
|
-
return data;
|
|
900
|
-
} catch {
|
|
901
|
-
return undefined;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
await test.setLargeFile({
|
|
906
|
-
path: "test.mp4",
|
|
907
|
-
getNextData,
|
|
908
|
-
});
|
|
909
|
-
*/
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
await test.lockRegion(async fncs => {
|
|
913
|
-
let newPath = await fncs.getPathAndLock("copy.mp4");
|
|
914
|
-
console.log({ newPath });
|
|
915
397
|
});
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
/*
|
|
919
|
-
logErrors((async () => {
|
|
920
|
-
while (true) {
|
|
921
|
-
let first100Bytes = await test.get("copy.mp4", { range: { start: 0, end: 100 } });
|
|
922
|
-
console.log({ first100Bytes });
|
|
923
|
-
await delay(5000);
|
|
924
|
-
}
|
|
925
|
-
})());
|
|
926
|
-
|
|
927
|
-
{
|
|
928
|
-
let pos = 0;
|
|
929
|
-
async function getNextData(): Promise<Buffer | undefined> {
|
|
930
|
-
let data = await test.get("test.mp4", { range: { start: pos, end: pos + LARGE_FILE_CHUNK } });
|
|
931
|
-
if (!data?.length) return undefined;
|
|
932
|
-
pos += data.length;
|
|
933
|
-
return data;
|
|
934
|
-
}
|
|
935
|
-
await test.setLargeFile({
|
|
936
|
-
path: "copy.mp4",
|
|
937
|
-
getNextData,
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
*/
|
|
941
398
|
}
|
|
942
|
-
//testLargeFiles().catch(console.error).finally(() => process.exit(0));
|