querysub 0.589.0 → 0.591.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/package.json +2 -2
- package/src/-a-archives/archiveCache.ts +9 -7
- package/src/-a-archives/archiveCache2.ts +153 -0
- package/src/-a-archives/archives.ts +4 -2
- package/src/-a-archives/archives2.ts +25 -9
- package/src/-a-archives/archivesJSONT.ts +20 -5
- package/src/-g-core-values/NodeCapabilities.ts +13 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +57 -19
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +17 -13
- package/src/0-path-value-core/pathValueArchives.ts +9 -9
- package/src/2-proxy/PathValueProxyWatcher.ts +6 -6
- package/src/4-dom/qreact.tsx +4 -4
- package/src/4-querysub/Querysub.ts +8 -1
- package/src/deployManager/components/RouteConfigView.tsx +10 -1
- package/src/deployManager/components/ServiceMeasureBars.tsx +179 -0
- package/src/deployManager/components/ServicesListPage.tsx +49 -2
- package/src/deployManager/components/StoragePage.tsx +43 -22
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +16 -47
- package/src/diagnostics/managementPages.tsx +3 -0
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +32 -9
- package/src/diagnostics/watchdog.ts +67 -1
- package/src/library-components/ATag.tsx +1 -0
- package/src/library-components/SyncedController.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.591.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
72
72
|
"pako": "^2.1.0",
|
|
73
73
|
"peggy": "^5.0.6",
|
|
74
|
-
"sliftutils": "^1.7.
|
|
74
|
+
"sliftutils": "^1.7.66",
|
|
75
75
|
"socket-function": "^1.2.27",
|
|
76
76
|
"terser": "^5.31.0",
|
|
77
77
|
"typenode": "^6.6.1",
|
|
@@ -48,13 +48,15 @@ const getCacheDir = lazy(() => {
|
|
|
48
48
|
const TEMP_SUFFIX = ".tmp";
|
|
49
49
|
const TEMP_THRESHOLD = timeInHour * 3;
|
|
50
50
|
|
|
51
|
-
function getTempFilePath() {
|
|
51
|
+
export function getTempFilePath() {
|
|
52
52
|
return os.tmpdir() + "/" + nextId() + TEMP_SUFFIX;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const LARGE_FILE_CHUNK = 1024 * 1024 * 32;
|
|
55
|
+
export const LARGE_FILE_CHUNK = 1024 * 1024 * 32;
|
|
56
56
|
const CACHE_SUFFIX = ".cache";
|
|
57
|
-
|
|
57
|
+
/** The cache only needs the debug name (for cache-path hashing), so any archives flavor (old Archives or IArchives) works. */
|
|
58
|
+
export type CacheKeySource = { getDebugName(): string };
|
|
59
|
+
export function getArchiveCachePath(archives: CacheKeySource, key: string): string {
|
|
58
60
|
// Remove all "/backblaze/" values from the path, as they make equivalent paths
|
|
59
61
|
// hash differently, which we don't want.
|
|
60
62
|
let fullFileName = (archives.getDebugName() + "/" + key).replaceAll("//", "/").replaceAll("backblaze/", "/");
|
|
@@ -134,7 +136,7 @@ const getDiskMetricsBase = async () => {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
// We require our input to be a temp file, so our update can be atomic
|
|
137
|
-
async function addCacheFile(archives:
|
|
139
|
+
async function addCacheFile(archives: CacheKeySource, key: string, sourceTempFile: string): Promise<void> {
|
|
138
140
|
let path = getArchiveCachePath(archives, key);
|
|
139
141
|
let hasFile = fileSizes.some(x => x.path === path);
|
|
140
142
|
if (hasFile) {
|
|
@@ -187,7 +189,7 @@ const getDiskMetricsBase = async () => {
|
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
async function getCacheFile(
|
|
190
|
-
archives:
|
|
192
|
+
archives: CacheKeySource,
|
|
191
193
|
key: string,
|
|
192
194
|
config?: { range?: { start: number; end: number } }
|
|
193
195
|
): Promise<Buffer | undefined> {
|
|
@@ -215,7 +217,7 @@ const getDiskMetricsBase = async () => {
|
|
|
215
217
|
}
|
|
216
218
|
return buffer;
|
|
217
219
|
}
|
|
218
|
-
async function delCacheFile(archives:
|
|
220
|
+
async function delCacheFile(archives: CacheKeySource, key: string): Promise<void> {
|
|
219
221
|
let path = getArchiveCachePath(archives, key);
|
|
220
222
|
if (removeFile(path)) {
|
|
221
223
|
try {
|
|
@@ -235,7 +237,7 @@ const getDiskMetricsBase = async () => {
|
|
|
235
237
|
};
|
|
236
238
|
let curSeqNum = 1;
|
|
237
239
|
const getDiskMetricSeqNum = cache((seqNum: number) => getDiskMetricsBase());
|
|
238
|
-
async function getDiskMetrics() {
|
|
240
|
+
export async function getDiskMetrics() {
|
|
239
241
|
return getDiskMetricSeqNum(curSeqNum);
|
|
240
242
|
}
|
|
241
243
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { GetConfig, IArchives, SetConfig } from "sliftutils/storage/IArchives";
|
|
3
|
+
import { getArchiveCachePath, getDiskMetrics, getTempFilePath, LARGE_FILE_CHUNK } from "./archiveCache";
|
|
4
|
+
|
|
5
|
+
let cacheArchives2Symbol = Symbol("cacheArchives2");
|
|
6
|
+
|
|
7
|
+
/** IMPORTANT! The cache assumes file contents are immutable: files are only created and deleted, never mutated in place.
|
|
8
|
+
* - Shares the disk cache pool (directory, size limits, eviction) with the old wrapArchivesWithCache.
|
|
9
|
+
*/
|
|
10
|
+
export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
|
|
11
|
+
/** Files are never deleted either, so we can serve cache hits (and even getInfo) without checking the source. */
|
|
12
|
+
immutable?: boolean;
|
|
13
|
+
}): IArchives & {
|
|
14
|
+
debugGetPath(key: string): string;
|
|
15
|
+
} {
|
|
16
|
+
if (cacheArchives2Symbol in archives) {
|
|
17
|
+
return archives as any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function cacheBuffer(fileName: string, data: Buffer) {
|
|
21
|
+
let metrics = await getDiskMetrics();
|
|
22
|
+
const tempPath = getTempFilePath();
|
|
23
|
+
await fs.promises.writeFile(tempPath, data);
|
|
24
|
+
await metrics.addCacheFile(archives, fileName, tempPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function setLargeFile(config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined>; }) {
|
|
28
|
+
const tempPath = getTempFilePath();
|
|
29
|
+
let handle: fs.promises.FileHandle | undefined;
|
|
30
|
+
try {
|
|
31
|
+
handle = await fs.promises.open(tempPath, "w");
|
|
32
|
+
let pos = 0;
|
|
33
|
+
while (true) {
|
|
34
|
+
let data = await config.getNextData();
|
|
35
|
+
if (!data?.length) break;
|
|
36
|
+
await handle.write(data, 0, data.length, pos);
|
|
37
|
+
pos += data.length;
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
if (handle) {
|
|
41
|
+
try {
|
|
42
|
+
await handle.close();
|
|
43
|
+
} catch { }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let metrics = await getDiskMetrics();
|
|
48
|
+
await metrics.addCacheFile(archives, config.path, tempPath);
|
|
49
|
+
let cachePath = getArchiveCachePath(archives, config.path);
|
|
50
|
+
|
|
51
|
+
let pos = 0;
|
|
52
|
+
let cacheHandle: fs.promises.FileHandle = await fs.promises.open(cachePath, "r");
|
|
53
|
+
let data = Buffer.alloc(LARGE_FILE_CHUNK);
|
|
54
|
+
async function getNextData(): Promise<Buffer | undefined> {
|
|
55
|
+
try {
|
|
56
|
+
let read = await cacheHandle.read(data, 0, LARGE_FILE_CHUNK, pos);
|
|
57
|
+
if (read.bytesRead === 0) return undefined;
|
|
58
|
+
let curData = data;
|
|
59
|
+
if (read.bytesRead < LARGE_FILE_CHUNK) {
|
|
60
|
+
curData = curData.slice(0, read.bytesRead);
|
|
61
|
+
}
|
|
62
|
+
pos += read.bytesRead;
|
|
63
|
+
return curData;
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
await archives.setLargeFile({
|
|
70
|
+
...config,
|
|
71
|
+
getNextData,
|
|
72
|
+
});
|
|
73
|
+
} finally {
|
|
74
|
+
await cacheHandle.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
[cacheArchives2Symbol]: true,
|
|
80
|
+
debugGetPath: (key: string) => getArchiveCachePath(archives, key),
|
|
81
|
+
|
|
82
|
+
getDebugName: () => archives.getDebugName(),
|
|
83
|
+
hasWriteAccess: () => archives.hasWriteAccess(),
|
|
84
|
+
getConfig: () => archives.getConfig(),
|
|
85
|
+
getURL: (path: string) => archives.getURL(path),
|
|
86
|
+
find: (prefix, config) => archives.find(prefix, config),
|
|
87
|
+
findInfo: (prefix, config) => archives.findInfo(prefix, config),
|
|
88
|
+
getChangesAfter2: (config) => archives.getChangesAfter2(config),
|
|
89
|
+
getSyncStatus: archives.getSyncStatus && (() => archives.getSyncStatus!()),
|
|
90
|
+
|
|
91
|
+
get: async (fileName: string, config?: GetConfig) => {
|
|
92
|
+
let metrics = await getDiskMetrics();
|
|
93
|
+
// Get info to at least see if it is removed or not. This makes sure we don't get in
|
|
94
|
+
// a bad state, at least for immutable files (for mutable files we don't have an easy
|
|
95
|
+
// way to check)
|
|
96
|
+
if (!rootConfig?.immutable) {
|
|
97
|
+
let info = await archives.getInfo(fileName);
|
|
98
|
+
if (!info) {
|
|
99
|
+
// If it is gone remotely, remove it from the cache, to save space.
|
|
100
|
+
await metrics.delCacheFile(archives, fileName);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let buffer = await metrics.getCacheFile(archives, fileName, config);
|
|
105
|
+
if (buffer) return buffer;
|
|
106
|
+
let result = await archives.get(fileName);
|
|
107
|
+
if (result) {
|
|
108
|
+
await cacheBuffer(fileName, result);
|
|
109
|
+
let range = config?.range;
|
|
110
|
+
if (range) {
|
|
111
|
+
result = result.slice(range.start, range.end);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
},
|
|
116
|
+
get2: async (fileName: string, config?: GetConfig) => {
|
|
117
|
+
let result = await archives.get2(fileName, config);
|
|
118
|
+
// Range reads are partial, so they can't populate the cache.
|
|
119
|
+
if (result?.data && !config?.range) {
|
|
120
|
+
await cacheBuffer(fileName, result.data);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
},
|
|
124
|
+
getInfo: async (fileName: string) => {
|
|
125
|
+
// When the archives are immutable, the local cache file is a byte-for-byte copy, so we can
|
|
126
|
+
// answer getInfo from a local stat and skip the (slow, network) getInfo on the underlying archives.
|
|
127
|
+
if (rootConfig?.immutable) {
|
|
128
|
+
try {
|
|
129
|
+
let stat = await fs.promises.stat(getArchiveCachePath(archives, fileName));
|
|
130
|
+
return { writeTime: stat.mtimeMs, size: stat.size };
|
|
131
|
+
} catch {
|
|
132
|
+
// Not in our cache yet — fall through to the source.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return archives.getInfo(fileName);
|
|
136
|
+
},
|
|
137
|
+
set: async (fileName: string, data: Buffer, config?: SetConfig) => {
|
|
138
|
+
let writtenKey = await archives.set(fileName, data, config);
|
|
139
|
+
// VARIABLE_SHARD keys materialize into a different key on write, in which case
|
|
140
|
+
// fileName isn't the readable key, so caching under it would be wrong.
|
|
141
|
+
if (writtenKey === fileName) {
|
|
142
|
+
await cacheBuffer(fileName, data);
|
|
143
|
+
}
|
|
144
|
+
return writtenKey;
|
|
145
|
+
},
|
|
146
|
+
setLargeFile,
|
|
147
|
+
del: async (fileName: string) => {
|
|
148
|
+
let metrics = await getDiskMetrics();
|
|
149
|
+
await metrics.delCacheFile(archives, fileName);
|
|
150
|
+
await archives.del(fileName);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -8,6 +8,7 @@ import debugbreak from "debugbreak";
|
|
|
8
8
|
import { isClient } from "../config2";
|
|
9
9
|
import { wrapArchivesWithCache } from "./archiveCache";
|
|
10
10
|
import { Args } from "socket-function/src/types";
|
|
11
|
+
import { IArchives } from "sliftutils/storage/IArchives";
|
|
11
12
|
|
|
12
13
|
export interface Archives {
|
|
13
14
|
getDebugName(): string;
|
|
@@ -104,8 +105,8 @@ export function createArchivesOverride<T extends Partial<Archives>>(
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
export function nestArchives(path: string, archives: Archives): Archives {
|
|
108
|
-
if (!path) return archives;
|
|
108
|
+
export function nestArchives(path: string, archives: Archives): Archives & IArchives {
|
|
109
|
+
if (!path) return archives as any;
|
|
109
110
|
if (!path.endsWith("/")) {
|
|
110
111
|
path = path + "/";
|
|
111
112
|
}
|
|
@@ -115,6 +116,7 @@ export function nestArchives(path: string, archives: Archives): Archives {
|
|
|
115
116
|
}
|
|
116
117
|
return file;
|
|
117
118
|
}
|
|
119
|
+
// NOTE: Just ignore the errors in here. We're going to be getting rid of this soon enough.
|
|
118
120
|
return {
|
|
119
121
|
getDebugName: () => archives.getDebugName() + "/" + path,
|
|
120
122
|
get: (fileName: string, config) => archives.get(path + stripFilePrefix(fileName), config),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IArchives, RemoteConfigBase } from "sliftutils/storage/IArchives";
|
|
1
|
+
import { ChangesAfterConfig, GetConfig, IArchives, RemoteConfigBase, SetConfig } from "sliftutils/storage/IArchives";
|
|
2
2
|
import { createArchives } from "sliftutils/storage/remoteStorage/createArchives";
|
|
3
3
|
import { cache, cacheJSONArgsEqual } from "socket-function/src/caching";
|
|
4
4
|
import { formatDateTime } from "socket-function/src/formatting/format";
|
|
@@ -39,8 +39,10 @@ function createSourceWindows(
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
|
|
42
|
+
// REMEMBER TO INCREMENT VERSION WHEN YOU MAKE CHANGES!
|
|
43
|
+
// REMEMBER TO RERUN initArchives2 if you change this (It's probably something you should call in your entry point.
|
|
42
44
|
const ONTARIO = `https://99-250-124-91.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
|
|
43
|
-
const HETZNER = `https://65-109-93-113.querysubtest.com:
|
|
45
|
+
const HETZNER = `https://65-109-93-113.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
|
|
44
46
|
const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
|
|
45
47
|
let sources = createSourceWindows(overrides, [
|
|
46
48
|
{
|
|
@@ -66,7 +68,7 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
|
|
|
66
68
|
]);
|
|
67
69
|
|
|
68
70
|
return createArchives({
|
|
69
|
-
version:
|
|
71
|
+
version: 16,
|
|
70
72
|
sources,
|
|
71
73
|
});
|
|
72
74
|
}
|
|
@@ -79,17 +81,18 @@ const archivesBuilderCache = cacheJSONArgsEqual((domain: string, overrides: Part
|
|
|
79
81
|
});
|
|
80
82
|
function nestBucket(folder: string, archives: ReturnType<typeof createArchives>) {
|
|
81
83
|
if (!folder) return archives;
|
|
84
|
+
folder = folder.replaceAll("\\", "/");
|
|
82
85
|
if (!folder.endsWith("/")) {
|
|
83
86
|
folder = folder + "/";
|
|
84
87
|
}
|
|
85
88
|
return {
|
|
86
|
-
get: (path: string) => archives.get(folder + path),
|
|
87
|
-
set: (path: string, data: Buffer) => archives.set(folder + path, data),
|
|
89
|
+
get: (path: string, config?: GetConfig) => archives.get(folder + path, config),
|
|
90
|
+
set: (path: string, data: Buffer, config?: SetConfig) => archives.set(folder + path, data, config),
|
|
88
91
|
del: (path: string) => archives.del(folder + path),
|
|
89
|
-
find: (prefix: string) => archives.find(folder + prefix),
|
|
90
|
-
findInfo: (prefix: string) => archives.findInfo(folder + prefix),
|
|
91
|
-
get2: (path: string) => archives.get2(folder + path),
|
|
92
|
-
setLargeFile: (config: { path: string; getNextData(): Promise<Buffer | undefined>; }) => archives.setLargeFile({ path: folder + config.path
|
|
92
|
+
find: async (prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }) => (await archives.find(folder + prefix, config)).map(x => x.slice(folder.length)),
|
|
93
|
+
findInfo: async (prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }) => (await archives.findInfo(folder + prefix, config)).map(x => ({ ...x, path: x.path.slice(folder.length) })),
|
|
94
|
+
get2: (path: string, config?: GetConfig) => archives.get2(folder + path, config),
|
|
95
|
+
setLargeFile: (config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined>; }) => archives.setLargeFile({ ...config, path: folder + config.path }),
|
|
93
96
|
getInfo: (path: string) => archives.getInfo(folder + path),
|
|
94
97
|
getDebugName: () => archives.getDebugName() + "/" + folder,
|
|
95
98
|
hasWriteAccess: () => archives.hasWriteAccess(),
|
|
@@ -101,6 +104,19 @@ function nestBucket(folder: string, archives: ReturnType<typeof createArchives>)
|
|
|
101
104
|
return (path: string) => getUrls(folder + path);
|
|
102
105
|
},
|
|
103
106
|
getShardKey: async (path: string) => (await archives.getShardKey(folder + path)).slice(folder.length),
|
|
107
|
+
// NOTE: It's recommended that you only store the keys and don't store the URLs. If you absolutely need a URL, it's best to call it lazily.
|
|
108
|
+
getGetFastURLs: async () => {
|
|
109
|
+
let getFastUrls = await archives.getGetFastURLs();
|
|
110
|
+
return (path: string) => getFastUrls(folder + path);
|
|
111
|
+
},
|
|
112
|
+
// getFast has less latency than get. Writes won't be available immediately after sets however. If it's a buffered archives the delay will be bufferDelay, ex 5 minutes, instead of a few seconds.
|
|
113
|
+
getFast: async (path: string, config?: GetConfig) => {
|
|
114
|
+
return await archives.getFast(folder + path, config);
|
|
115
|
+
},
|
|
116
|
+
getChangesAfter2: async (config: ChangesAfterConfig) => {
|
|
117
|
+
let changes = await archives.getChangesAfter2(config);
|
|
118
|
+
return changes.filter(x => x.path.startsWith(folder)).map(x => ({ ...x, path: x.path.slice(folder.length) }));
|
|
119
|
+
},
|
|
104
120
|
};
|
|
105
121
|
}
|
|
106
122
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lazy } from "socket-function/src/caching";
|
|
2
2
|
import { Archives } from "./archives";
|
|
3
3
|
import { green } from "socket-function/src/formatting/logColors";
|
|
4
|
+
import { IArchives } from "sliftutils/storage/IArchives";
|
|
4
5
|
|
|
5
6
|
export type ArchiveT<T> = {
|
|
6
7
|
get(key: string): Promise<T | undefined>;
|
|
@@ -12,7 +13,7 @@ export type ArchiveT<T> = {
|
|
|
12
13
|
[Symbol.asyncIterator](): AsyncIterator<[string, T]>;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
export function archiveJSONT<T>(archives: () =>
|
|
16
|
+
export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
|
|
16
17
|
archives = lazy(archives);
|
|
17
18
|
|
|
18
19
|
let valuesCache = new Map<string, {
|
|
@@ -60,18 +61,28 @@ export function archiveJSONT<T>(archives: () => Archives): ArchiveT<T> {
|
|
|
60
61
|
|
|
61
62
|
if (needsUpdate) {
|
|
62
63
|
let maxRetries = 10;
|
|
64
|
+
let updated = false;
|
|
63
65
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
64
66
|
infos = await archives().findInfo("");
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
let newCache = new Map<string, {
|
|
69
|
+
createTime: number;
|
|
70
|
+
size: number;
|
|
71
|
+
value: Buffer;
|
|
72
|
+
}>();
|
|
67
73
|
let allFound = true;
|
|
68
74
|
|
|
69
75
|
await Promise.all(infos.map(async info => {
|
|
76
|
+
let cached = valuesCache.get(info.path);
|
|
77
|
+
if (cached && cached.createTime === info.createTime && cached.size === info.size) {
|
|
78
|
+
newCache.set(info.path, cached);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
70
81
|
let buffer = await archives().get(info.path);
|
|
71
82
|
if (!buffer) {
|
|
72
83
|
allFound = false;
|
|
73
84
|
} else {
|
|
74
|
-
|
|
85
|
+
newCache.set(info.path, {
|
|
75
86
|
createTime: info.createTime,
|
|
76
87
|
size: info.size,
|
|
77
88
|
value: buffer
|
|
@@ -94,10 +105,14 @@ export function archiveJSONT<T>(archives: () => Archives): ArchiveT<T> {
|
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
|
-
if (!anyChanged())
|
|
108
|
+
if (!anyChanged()) {
|
|
109
|
+
valuesCache = newCache;
|
|
110
|
+
updated = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
if (
|
|
115
|
+
if (!updated) {
|
|
101
116
|
throw new Error(`Failed to get consistent snapshot of values after ${maxRetries} attempts`);
|
|
102
117
|
}
|
|
103
118
|
}
|
|
@@ -25,6 +25,7 @@ import { PromiseObj } from "../promise";
|
|
|
25
25
|
import { getTrueTimeOffset } from "socket-function/time/trueTimeShim";
|
|
26
26
|
import { getGitRefLive } from "../4-deploy/git";
|
|
27
27
|
import { getScheduledShutdownTime } from "./scheduledShutdown";
|
|
28
|
+
import type { MeasureChunk, MeasureSummary } from "../diagnostics/watchdog";
|
|
28
29
|
setImmediate(() => {
|
|
29
30
|
import("../diagnostics/MachineThreadInfo");
|
|
30
31
|
});
|
|
@@ -164,6 +165,16 @@ class NodeCapabilitiesControllerBase {
|
|
|
164
165
|
return process.memoryUsage();
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
public async getMeasureSummary(): Promise<MeasureSummary | undefined> {
|
|
169
|
+
let { getLastMeasureSummary } = await import("../diagnostics/watchdog");
|
|
170
|
+
return getLastMeasureSummary();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public async getMeasureBreakdown(range: { startTime: number; endTime: number }): Promise<MeasureChunk | undefined> {
|
|
174
|
+
let { getMeasureBreakdown } = await import("../diagnostics/watchdog");
|
|
175
|
+
return getMeasureBreakdown(range);
|
|
176
|
+
}
|
|
177
|
+
|
|
167
178
|
public async getInspectURL() {
|
|
168
179
|
return await getDebuggerUrl();
|
|
169
180
|
}
|
|
@@ -204,6 +215,8 @@ export const NodeCapabilitiesController = SocketFunction.register(
|
|
|
204
215
|
() => ({
|
|
205
216
|
getMetadata: {},
|
|
206
217
|
getMemoryUsage: {},
|
|
218
|
+
getMeasureSummary: {},
|
|
219
|
+
getMeasureBreakdown: {},
|
|
207
220
|
getInspectURL: { hooks: [requiresNetworkTrustHook] },
|
|
208
221
|
exposeExternalDebugPortOnce: { hooks: [requiresNetworkTrustHook] },
|
|
209
222
|
}),
|
|
@@ -4,7 +4,7 @@ import { decodeNodeId } from "sliftutils/misc/https/certs";
|
|
|
4
4
|
import { getOwnNodeId, getOwnNodeIdAssert } from "../../-f-node-discovery/NodeDiscovery";
|
|
5
5
|
import { pathValueArchives } from "../pathValueArchives";
|
|
6
6
|
import { ArchiveLocker, ArchiveTransaction } from "./ArchiveLocks";
|
|
7
|
-
import {
|
|
7
|
+
import { copyArchiveFile, IArchives } from "sliftutils/storage/IArchives";
|
|
8
8
|
import debugbreak from "debugbreak";
|
|
9
9
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
10
10
|
import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
|
|
@@ -33,13 +33,20 @@ const MAX_APPLY_TRIES = 10;
|
|
|
33
33
|
const CONCURRENT_READ_COUNT = 32;
|
|
34
34
|
const CONCURRENT_WRITE_COUNT = 16;
|
|
35
35
|
|
|
36
|
+
/** Moves by copy-then-delete (IArchives has no move). copyArchiveFile stamps the target with the source's write time, so ordering-by-writeTime survives the move. A missing source means another process already moved it, which is fine. */
|
|
37
|
+
async function moveArchiveFile(from: IArchives, to: IArchives, path: string): Promise<void> {
|
|
38
|
+
let copied = await copyArchiveFile({ from, to, path });
|
|
39
|
+
if (!copied) return;
|
|
40
|
+
await from.del(path);
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
export function createArchiveLocker2(config: {
|
|
37
|
-
archiveValues:
|
|
44
|
+
archiveValues: IArchives;
|
|
38
45
|
/** IMPORTANT! This must be the same independent of archiveValues, otherwise we won't
|
|
39
46
|
* lock moves between directories.
|
|
40
47
|
*/
|
|
41
|
-
archiveLocks:
|
|
42
|
-
archiveRecycleBin:
|
|
48
|
+
archiveLocks: IArchives;
|
|
49
|
+
archiveRecycleBin: IArchives;
|
|
43
50
|
}): ArchiveLocker {
|
|
44
51
|
let { archiveValues, archiveLocks, archiveRecycleBin } = config;
|
|
45
52
|
function getArchives(key: string) {
|
|
@@ -85,11 +92,7 @@ export function createArchiveLocker2(config: {
|
|
|
85
92
|
return;
|
|
86
93
|
}
|
|
87
94
|
try {
|
|
88
|
-
await archives
|
|
89
|
-
path: key,
|
|
90
|
-
targetPath: key,
|
|
91
|
-
target: archiveRecycleBin,
|
|
92
|
-
});
|
|
95
|
+
await moveArchiveFile(archives, archiveRecycleBin, key);
|
|
93
96
|
logNodeStats(`archives|Deleted TΔ`, formatNumber, 1);
|
|
94
97
|
} catch {
|
|
95
98
|
// It was probably just moved by another process
|
|
@@ -126,11 +129,7 @@ export function createArchiveLocker2(config: {
|
|
|
126
129
|
if (correctFiles.has(file)) return;
|
|
127
130
|
let info = await archiveValues.getInfo(file);
|
|
128
131
|
console.log(`Moving file ${file}, ${moveCount}/${valuePaths.size}, ${formatNumber(info?.size || 0)} bytes`);
|
|
129
|
-
await archiveValues
|
|
130
|
-
path: file,
|
|
131
|
-
targetPath: file,
|
|
132
|
-
target: archiveRecycleBin,
|
|
133
|
-
});
|
|
132
|
+
await moveArchiveFile(archiveValues, archiveRecycleBin, file);
|
|
134
133
|
console.log(`Moved file ${file}, ${moveCount}/${valuePaths.size}`);
|
|
135
134
|
}
|
|
136
135
|
let moveFileParallel = runInParallel({ parallelCount: 8 }, moveFile);
|
|
@@ -151,11 +150,7 @@ export function createArchiveLocker2(config: {
|
|
|
151
150
|
let isInRecycleBin = !!(await archiveRecycleBin.getInfo(file));
|
|
152
151
|
if (isInRecycleBin) {
|
|
153
152
|
console.log(`Restoring ${file}, ${restoreCount++}/${files.length}`);
|
|
154
|
-
await archiveRecycleBin
|
|
155
|
-
path: file,
|
|
156
|
-
targetPath: file,
|
|
157
|
-
target: archiveValues,
|
|
158
|
-
});
|
|
153
|
+
await moveArchiveFile(archiveRecycleBin, archiveValues, file);
|
|
159
154
|
} else {
|
|
160
155
|
console.log(`File ${file} is not in the recycle bin, skipping, ${restoreCount++}/${files.length}`);
|
|
161
156
|
}
|
|
@@ -312,6 +307,36 @@ type StorageType = {
|
|
|
312
307
|
* all writes should now be readable.
|
|
313
308
|
*/
|
|
314
309
|
};
|
|
310
|
+
function createCheckpointTimer() {
|
|
311
|
+
let last = Date.now();
|
|
312
|
+
let spans = new Map<string, { time: number; count: number }>();
|
|
313
|
+
return {
|
|
314
|
+
start() {
|
|
315
|
+
last = Date.now();
|
|
316
|
+
spans.clear();
|
|
317
|
+
},
|
|
318
|
+
/** Attributes the time since the previous checkpoint (or start) to name, accumulating across repeat visits. */
|
|
319
|
+
checkpoint(name: string) {
|
|
320
|
+
let now = Date.now();
|
|
321
|
+
let span = spans.get(name);
|
|
322
|
+
if (!span) {
|
|
323
|
+
span = { time: 0, count: 0 };
|
|
324
|
+
spans.set(name, span);
|
|
325
|
+
}
|
|
326
|
+
span.time += now - last;
|
|
327
|
+
span.count++;
|
|
328
|
+
last = now;
|
|
329
|
+
},
|
|
330
|
+
logAndFlush(prefix: string) {
|
|
331
|
+
let total = Array.from(spans.values()).reduce((sum, span) => sum + span.time, 0);
|
|
332
|
+
let parts = Array.from(spans.entries()).map(([name, span]) => `${name}=${formatTime(span.time)}${span.count > 1 ? ` (x${span.count})` : ""}`);
|
|
333
|
+
console.log(blue(`${prefix} time breakdown, total ${formatTime(total)}: ${parts.join(" | ")}`));
|
|
334
|
+
spans.clear();
|
|
335
|
+
last = Date.now();
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
315
340
|
export type Transaction = {
|
|
316
341
|
ops: ({
|
|
317
342
|
type: "create";
|
|
@@ -355,6 +380,8 @@ class TransactionLocker {
|
|
|
355
380
|
private isTransactionValidOverride?: (transaction: Transaction, dataFiles: FileInfo[], rawDataFiles: FileInfo[]) => boolean
|
|
356
381
|
) { }
|
|
357
382
|
|
|
383
|
+
private perf = createCheckpointTimer();
|
|
384
|
+
|
|
358
385
|
// #region Base File Ops
|
|
359
386
|
public getConfirmKey(key: string): string {
|
|
360
387
|
let { dir, name } = parsePath(key);
|
|
@@ -444,6 +471,7 @@ class TransactionLocker {
|
|
|
444
471
|
const tryToRead = async () => {
|
|
445
472
|
let time = Date.now();
|
|
446
473
|
let files = await this.storage.getKeys();
|
|
474
|
+
this.perf.checkpoint("getKeys");
|
|
447
475
|
if (this.lastFilesRead && this.lastFilesReadTime) {
|
|
448
476
|
let prevFiles = new Set(this.lastFilesRead.map(a => a.file));
|
|
449
477
|
let suspiciousThreshold = this.lastFilesReadTime - ARCHIVE_PROPAGATION_TIME * 10;
|
|
@@ -491,6 +519,7 @@ class TransactionLocker {
|
|
|
491
519
|
source: tFile,
|
|
492
520
|
});
|
|
493
521
|
}
|
|
522
|
+
this.perf.checkpoint("read transaction files");
|
|
494
523
|
|
|
495
524
|
// Check all of our files to see if any have changed (or if there a new files).
|
|
496
525
|
// Because we don't reuse file names, or change them, it means if a file exists
|
|
@@ -501,6 +530,7 @@ class TransactionLocker {
|
|
|
501
530
|
// (it pretty much only depends on files existing).
|
|
502
531
|
{
|
|
503
532
|
let filesVerify = await this.storage.getKeys();
|
|
533
|
+
this.perf.checkpoint("verify getKeys");
|
|
504
534
|
let filesVerifySet = new Set(filesVerify.map(a => a.file));
|
|
505
535
|
// If it changes while reading, read again. Otherwise, if there were no changes while reading,
|
|
506
536
|
// we know the state was all the state we read, at one time (maybe not now, but at an instant
|
|
@@ -563,6 +593,7 @@ class TransactionLocker {
|
|
|
563
593
|
//rawFiles: files.map(a => a.file),
|
|
564
594
|
//confirmedFiles: Array.from(currentDataFiles.values()).map(a => a.file),
|
|
565
595
|
});
|
|
596
|
+
this.perf.checkpoint("categorize files");
|
|
566
597
|
|
|
567
598
|
return {
|
|
568
599
|
rawDataFiles: files,
|
|
@@ -672,8 +703,10 @@ class TransactionLocker {
|
|
|
672
703
|
* - Might run a transaction
|
|
673
704
|
*/
|
|
674
705
|
public async getFiles(): Promise<FileInfo[]> {
|
|
706
|
+
this.perf.start();
|
|
675
707
|
let obj = await this.getFilesBase();
|
|
676
708
|
logNodeStateStats(`ArchiveLock Data File`, formatNumber, obj.dataFiles.length);
|
|
709
|
+
this.perf.logAndFlush(`getFiles (${this.storage.debugKey})`);
|
|
677
710
|
return obj.dataFiles;
|
|
678
711
|
}
|
|
679
712
|
private transactionAppliedCount = new Map<number, number>();
|
|
@@ -731,6 +764,7 @@ class TransactionLocker {
|
|
|
731
764
|
let waitTime = threshold - Date.now();
|
|
732
765
|
console.info(`Waiting ${formatTime(waitTime)} for transaction ${activeT.seqNum} to settle.`);
|
|
733
766
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
767
|
+
this.perf.checkpoint("settle wait");
|
|
734
768
|
return this.getFilesBase();
|
|
735
769
|
}
|
|
736
770
|
|
|
@@ -752,6 +786,7 @@ class TransactionLocker {
|
|
|
752
786
|
await this.storage.deleteKey(t.source.file);
|
|
753
787
|
}
|
|
754
788
|
}
|
|
789
|
+
this.perf.checkpoint("delete old transactions");
|
|
755
790
|
|
|
756
791
|
// Delete any create files that are WAY too old, and not confirmed
|
|
757
792
|
{
|
|
@@ -780,6 +815,7 @@ class TransactionLocker {
|
|
|
780
815
|
console.warn(`Almost deleted ${unconfirmedOldFiles.length} very old unconfirmed files. This is bad, did we miss their confirmations that first time? If we missed them twice in a row, we might literally delete the database, and need to enter recovery mode to fix it...`, { files: unconfirmedOldFiles });
|
|
781
816
|
}
|
|
782
817
|
}
|
|
818
|
+
this.perf.checkpoint("cleanup old unconfirmed files");
|
|
783
819
|
}
|
|
784
820
|
|
|
785
821
|
|
|
@@ -805,6 +841,7 @@ class TransactionLocker {
|
|
|
805
841
|
}
|
|
806
842
|
}
|
|
807
843
|
}
|
|
844
|
+
this.perf.checkpoint("cleanup old confirms");
|
|
808
845
|
}
|
|
809
846
|
|
|
810
847
|
return {
|
|
@@ -818,6 +855,7 @@ class TransactionLocker {
|
|
|
818
855
|
console.log(`Applying transaction ${activeT.seqNum}, cur try ${applyCount} / ${MAX_APPLY_TRIES}`);
|
|
819
856
|
}
|
|
820
857
|
await this.applyTransaction(activeT);
|
|
858
|
+
this.perf.checkpoint("apply transaction");
|
|
821
859
|
this.transactionAppliedCount.set(activeT.seqNum, applyCount + 1);
|
|
822
860
|
|
|
823
861
|
// Run again, until we can be reasonable sure activeT isn't changing. We can be wrong,
|
|
@@ -9,11 +9,12 @@ import { sort } from "socket-function/src/misc";
|
|
|
9
9
|
import { lazy } from "socket-function/src/caching";
|
|
10
10
|
import { decodeCborx, encodeCborx } from "../../misc/cloneHelpers";
|
|
11
11
|
import { runInParallel } from "socket-function/src/batching";
|
|
12
|
-
import { formatNumber } from "socket-function/src/formatting/format";
|
|
12
|
+
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
13
|
+
import { getArchives2 } from "../../-a-archives/archives2";
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
// Snapshot is just a newline delimitted list of files. The name contains the metadata and time of it.
|
|
16
|
-
const snapshots = lazy(() =>
|
|
17
|
+
const snapshots = lazy(() => getArchives2("snapshots"));
|
|
17
18
|
|
|
18
19
|
export type ArchiveSnapshotOverview = {
|
|
19
20
|
// If live, it isn't a real file that can be loaded
|
|
@@ -89,23 +90,26 @@ function fileNameToOverview(fileName: string): ArchiveSnapshotOverview {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
export type ArchiveSnapshotList = {
|
|
94
|
+
loadTime: number;
|
|
95
|
+
snapshots: ArchiveSnapshotOverview[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export async function getSavedSnapshotList(): Promise<ArchiveSnapshotList> {
|
|
99
|
+
let startTime = Date.now();
|
|
94
100
|
let snapshotFiles = await snapshots().find("");
|
|
101
|
+
let overview = snapshotFiles.map(file => fileNameToOverview(file));
|
|
102
|
+
sort(overview, x => -x.time);
|
|
103
|
+
return { loadTime: Date.now() - startTime, snapshots: overview };
|
|
104
|
+
}
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
export async function getLiveSnapshotList(): Promise<ArchiveSnapshotList> {
|
|
107
|
+
let startTime = Date.now();
|
|
97
108
|
let locker = await pathValueArchives.getArchiveLocker();
|
|
98
109
|
let allFiles = await locker.getAllValidFiles();
|
|
99
|
-
|
|
100
110
|
let liveOverview = getSnapshotOverview(allFiles.map(x => x.file));
|
|
101
111
|
liveOverview.file = "live";
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
overview.push(...snapshotFiles.map(file => fileNameToOverview(file)));
|
|
105
|
-
|
|
106
|
-
sort(overview, x => -x.time);
|
|
107
|
-
|
|
108
|
-
return overview;
|
|
112
|
+
return { loadTime: Date.now() - startTime, snapshots: [liveOverview] };
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
export async function getSnapshot(snapshotFile: string | "live"): Promise<ArchiveSnapshotRead> {
|