querysub 0.590.0 → 0.592.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/bin/stop-machine.js +4 -0
- package/package.json +5 -2
- package/src/-a-archives/archiveCache.ts +28 -7
- package/src/-a-archives/archiveCache2.ts +5 -2
- package/src/-a-archives/archives.ts +2 -63
- package/src/-a-archives/archivesDisk.ts +2 -13
- package/src/-a-archives/archivesMemoryCache.ts +27 -17
- package/src/-a-archives/archivesMemoryCache2.ts +23 -0
- package/src/-d-trust/NetworkTrust2.ts +2 -2
- package/src/-e-certs/certAuthority.ts +2 -2
- package/src/-f-node-discovery/NodeDiscovery.ts +2 -2
- package/src/0-path-value-core/AuthorityLookup.ts +0 -4
- package/src/0-path-value-core/ShardPrefixes.ts +2 -4
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +23 -10
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -1
- package/src/0-path-value-core/pathValueArchives.ts +1 -0
- package/src/4-deploy/edgeBootstrap.ts +50 -11
- package/src/4-deploy/edgeNodes.ts +7 -13
- package/src/4-querysub/Querysub.ts +2 -2
- package/src/config2.ts +1 -7
- package/src/deployManager/components/ServiceMeasureBars.tsx +60 -50
- package/src/deployManager/components/ServicesListPage.tsx +29 -13
- package/src/deployManager/machineDaemonShared.ts +2 -0
- package/src/deployManager/machineSchema.ts +5 -6
- package/src/deployManager/setupMachineMain.ts +1 -2
- package/src/deployManager/stopMachineMain.ts +45 -0
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +34 -21
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +11 -2
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +3 -4
- package/src/diagnostics/logs/errorTickets/tickets.ts +3 -4
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +2 -4
- package/src/diagnostics/watchdog.ts +0 -5
- package/src/-a-archives/archivesBackBlaze.ts +0 -156
- package/src/-a-archives/archivesCborT.ts +0 -52
- package/src/-a-archives/archivesLimitedCache.ts +0 -307
- package/src/-a-archives/archivesPrivateFileSystem.ts +0 -326
- package/src/-a-archives/copyLocalToBackblaze.ts +0 -24
- package/src/-b-authorities/cdnAuthority.ts +0 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.592.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",
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"mc2": "yarn typenode ./src/diagnostics/debugger/mcp-server.ts --cwd D:/repos/qs-cyoa/",
|
|
27
27
|
"autofix": "yarn typenode ./src/diagnostics/logs/errorTickets/autoFixerEntry.ts",
|
|
28
28
|
"parameters-timeline": "yarn typenode ./src/deployManager/parametersTimelineMain.ts",
|
|
29
|
+
"setup-machine": "yarn typenode ./src/deployManager/setupMachineMain.ts",
|
|
30
|
+
"stop-machine": "yarn typenode ./src/deployManager/stopMachineMain.ts",
|
|
29
31
|
"ssh-a-claude": "ssh root@a.querysubtest.com"
|
|
30
32
|
},
|
|
31
33
|
"bin": {
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"machine": "./bin/machine.js",
|
|
39
41
|
"machine-alwaysup": "./bin/machine-alwaysup.js",
|
|
40
42
|
"setup-machine": "./bin/setup-machine.js",
|
|
43
|
+
"stop-machine": "./bin/stop-machine.js",
|
|
41
44
|
"gc": "./bin/gc.js",
|
|
42
45
|
"gc-watch": "./bin/gc-watch.js",
|
|
43
46
|
"gc-watch-public": "./bin/gc-watch-public.js",
|
|
@@ -71,7 +74,7 @@
|
|
|
71
74
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
72
75
|
"pako": "^2.1.0",
|
|
73
76
|
"peggy": "^5.0.6",
|
|
74
|
-
"sliftutils": "^1.7.
|
|
77
|
+
"sliftutils": "^1.7.69",
|
|
75
78
|
"socket-function": "^1.2.27",
|
|
76
79
|
"terser": "^5.31.0",
|
|
77
80
|
"typenode": "^6.6.1",
|
|
@@ -138,11 +138,17 @@ const getDiskMetricsBase = async () => {
|
|
|
138
138
|
// We require our input to be a temp file, so our update can be atomic
|
|
139
139
|
async function addCacheFile(archives: CacheKeySource, key: string, sourceTempFile: string): Promise<void> {
|
|
140
140
|
let path = getArchiveCachePath(archives, key);
|
|
141
|
+
let sourceTempFileStat = await fs.promises.stat(sourceTempFile);
|
|
142
|
+
if (sourceTempFileStat.size === 0) {
|
|
143
|
+
try {
|
|
144
|
+
await fs.promises.unlink(sourceTempFile);
|
|
145
|
+
} catch { }
|
|
146
|
+
throw new Error(`Expected non-empty data when caching ${key}, was 0 bytes. Empty files are treated as deletes by the storage layer, so they must never be written or cached.`);
|
|
147
|
+
}
|
|
141
148
|
let hasFile = fileSizes.some(x => x.path === path);
|
|
142
149
|
if (hasFile) {
|
|
143
150
|
removeFile(path);
|
|
144
151
|
}
|
|
145
|
-
let sourceTempFileStat = await fs.promises.stat(sourceTempFile);
|
|
146
152
|
availableBytes -= sourceTempFileStat.size;
|
|
147
153
|
availableFiles--;
|
|
148
154
|
sort(fileSizes, x => x.time);
|
|
@@ -196,14 +202,19 @@ const getDiskMetricsBase = async () => {
|
|
|
196
202
|
let path = getArchiveCachePath(archives, key);
|
|
197
203
|
let buffer: Buffer | undefined;
|
|
198
204
|
let handle: fs.promises.FileHandle | undefined;
|
|
205
|
+
let isEmptyFile = false;
|
|
199
206
|
try {
|
|
200
207
|
handle = await fs.promises.open(path, "r");
|
|
201
208
|
let stats = await handle.stat();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
if (stats.size === 0) {
|
|
210
|
+
isEmptyFile = true;
|
|
211
|
+
} else {
|
|
212
|
+
let start = config?.range?.start ?? 0;
|
|
213
|
+
let end = config?.range?.end ?? stats.size;
|
|
214
|
+
let readBuffer = Buffer.alloc(end - start);
|
|
215
|
+
let read = await handle.read(readBuffer, 0, end - start, start);
|
|
216
|
+
buffer = readBuffer.slice(0, read.bytesRead);
|
|
217
|
+
}
|
|
207
218
|
}
|
|
208
219
|
catch { }
|
|
209
220
|
finally {
|
|
@@ -211,6 +222,13 @@ const getDiskMetricsBase = async () => {
|
|
|
211
222
|
await handle.close();
|
|
212
223
|
}
|
|
213
224
|
}
|
|
225
|
+
if (isEmptyFile) {
|
|
226
|
+
removeFile(path);
|
|
227
|
+
try {
|
|
228
|
+
await fs.promises.unlink(path);
|
|
229
|
+
} catch { }
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
214
232
|
if (buffer) {
|
|
215
233
|
updateAccessTime(path);
|
|
216
234
|
bumpAtimeIfStale(path);
|
|
@@ -355,7 +373,9 @@ export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
|
|
|
355
373
|
if (rootConfig?.immutable) {
|
|
356
374
|
try {
|
|
357
375
|
let stat = await fs.promises.stat(getArchiveCachePath(archives, fileName));
|
|
358
|
-
|
|
376
|
+
if (stat.size > 0) {
|
|
377
|
+
return { writeTime: stat.mtimeMs, size: stat.size };
|
|
378
|
+
}
|
|
359
379
|
} catch {
|
|
360
380
|
// Not in our cache yet — fall through to the source.
|
|
361
381
|
}
|
|
@@ -363,6 +383,7 @@ export function wrapArchivesWithCache(archives: Archives, rootConfig?: {
|
|
|
363
383
|
return archives.getInfo(fileName);
|
|
364
384
|
},
|
|
365
385
|
set: async (fileName: string, data: Buffer) => {
|
|
386
|
+
if (data.length === 0) throw new Error(`Expected non-empty data when setting ${fileName}, was 0 bytes. Empty writes are treated as deletes by the storage layer.`);
|
|
366
387
|
let metrics = await getDiskMetrics();
|
|
367
388
|
const tempPath = getTempFilePath();
|
|
368
389
|
await fs.promises.writeFile(tempPath, data);
|
|
@@ -116,7 +116,7 @@ export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
|
|
|
116
116
|
get2: async (fileName: string, config?: GetConfig) => {
|
|
117
117
|
let result = await archives.get2(fileName, config);
|
|
118
118
|
// Range reads are partial, so they can't populate the cache.
|
|
119
|
-
if (result && !config?.range) {
|
|
119
|
+
if (result?.data && !config?.range) {
|
|
120
120
|
await cacheBuffer(fileName, result.data);
|
|
121
121
|
}
|
|
122
122
|
return result;
|
|
@@ -127,7 +127,9 @@ export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
|
|
|
127
127
|
if (rootConfig?.immutable) {
|
|
128
128
|
try {
|
|
129
129
|
let stat = await fs.promises.stat(getArchiveCachePath(archives, fileName));
|
|
130
|
-
|
|
130
|
+
if (stat.size > 0) {
|
|
131
|
+
return { writeTime: stat.mtimeMs, size: stat.size };
|
|
132
|
+
}
|
|
131
133
|
} catch {
|
|
132
134
|
// Not in our cache yet — fall through to the source.
|
|
133
135
|
}
|
|
@@ -135,6 +137,7 @@ export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
|
|
|
135
137
|
return archives.getInfo(fileName);
|
|
136
138
|
},
|
|
137
139
|
set: async (fileName: string, data: Buffer, config?: SetConfig) => {
|
|
140
|
+
if (data.length === 0) throw new Error(`Expected non-empty data when setting ${fileName}, was 0 bytes. Empty writes are treated as deletes by the storage layer.`);
|
|
138
141
|
let writtenKey = await archives.set(fileName, data, config);
|
|
139
142
|
// VARIABLE_SHARD keys materialize into a different key on write, in which case
|
|
140
143
|
// fileName isn't the readable key, so caching under it would be wrong.
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
import { isNode } from "socket-function/src/misc";
|
|
2
|
-
import { getDomain, isNoNetwork } from "../config";
|
|
3
|
-
import { getStorageDir, getSubFolder } from "../fs";
|
|
4
|
-
import { getArchivesBackblaze, hasBackblazePermissions } from "./archivesBackBlaze";
|
|
5
|
-
import { getArchivesLocal } from "./archivesDisk";
|
|
6
|
-
import { magenta } from "socket-function/src/formatting/logColors";
|
|
7
|
-
import debugbreak from "debugbreak";
|
|
8
|
-
import { isClient } from "../config2";
|
|
9
|
-
import { wrapArchivesWithCache } from "./archiveCache";
|
|
10
1
|
import { Args } from "socket-function/src/types";
|
|
11
|
-
import { IArchives } from "sliftutils/storage/IArchives";
|
|
12
2
|
|
|
13
3
|
export interface Archives {
|
|
14
4
|
getDebugName(): string;
|
|
@@ -105,8 +95,8 @@ export function createArchivesOverride<T extends Partial<Archives>>(
|
|
|
105
95
|
};
|
|
106
96
|
}
|
|
107
97
|
|
|
108
|
-
export function nestArchives(path: string, archives: Archives): Archives
|
|
109
|
-
if (!path) return archives
|
|
98
|
+
export function nestArchives(path: string, archives: Archives): Archives {
|
|
99
|
+
if (!path) return archives;
|
|
110
100
|
if (!path.endsWith("/")) {
|
|
111
101
|
path = path + "/";
|
|
112
102
|
}
|
|
@@ -116,7 +106,6 @@ export function nestArchives(path: string, archives: Archives): Archives & IArch
|
|
|
116
106
|
}
|
|
117
107
|
return file;
|
|
118
108
|
}
|
|
119
|
-
// NOTE: Just ignore the errors in here. We're going to be getting rid of this soon enough.
|
|
120
109
|
return {
|
|
121
110
|
getDebugName: () => archives.getDebugName() + "/" + path,
|
|
122
111
|
get: (fileName: string, config) => archives.get(path + stripFilePrefix(fileName), config),
|
|
@@ -153,53 +142,3 @@ export function nestArchives(path: string, archives: Archives): Archives & IArch
|
|
|
153
142
|
};
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
let archivesLogging = false;
|
|
157
|
-
export function enableArchivesLogging() {
|
|
158
|
-
archivesLogging = true;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let cached = new Map<string, Archives>();
|
|
162
|
-
export function getArchives(rootPath = ""): Archives {
|
|
163
|
-
let value = cached.get(rootPath);
|
|
164
|
-
if (!value) {
|
|
165
|
-
value = getArchivesBase(rootPath);
|
|
166
|
-
cached.set(rootPath, value);
|
|
167
|
-
}
|
|
168
|
-
return value;
|
|
169
|
-
}
|
|
170
|
-
export function getArchivesBase(rootPath = ""): Archives {
|
|
171
|
-
let archives: Archives;
|
|
172
|
-
let domain = getDomain();
|
|
173
|
-
if (isNoNetwork()) {
|
|
174
|
-
archives = getArchivesLocal(domain);
|
|
175
|
-
archives = nestArchives(rootPath, archives);
|
|
176
|
-
} else {
|
|
177
|
-
archives = getArchivesBackblaze(domain);
|
|
178
|
-
archives = nestArchives(rootPath, archives);
|
|
179
|
-
// Why did we think we should always cache? We absolutely should not. Even if we were checking
|
|
180
|
-
// file hashes on every read (which we do not), it would still potentially fill the cache up
|
|
181
|
-
// with large temporary files we have no intention of using (which might evict entries we
|
|
182
|
-
// intend to use).
|
|
183
|
-
//archives = wrapArchivesWithCache(archives);
|
|
184
|
-
}
|
|
185
|
-
if (archivesLogging) {
|
|
186
|
-
archives.enableLogging();
|
|
187
|
-
}
|
|
188
|
-
return archives;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function hasArchivesPermissions() {
|
|
192
|
-
if (isNoNetwork()) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
return hasBackblazePermissions();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (isNode()) {
|
|
199
|
-
if (isNoNetwork()) {
|
|
200
|
-
console.log("Primary data storage: " + magenta(getStorageDir()));
|
|
201
|
-
} else {
|
|
202
|
-
console.log("Config storage path: " + magenta(getStorageDir()));
|
|
203
|
-
console.log("Primary data storage: " + magenta(`backblaze:${getDomain()}`));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { isNode } from "socket-function/src/misc";
|
|
2
2
|
import os from "os";
|
|
3
|
-
import { fsExistsAsync
|
|
3
|
+
import { fsExistsAsync } from "../fs";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import { blue, red, yellow } from "socket-function/src/formatting/logColors";
|
|
6
6
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
7
7
|
import { delay } from "socket-function/src/batching";
|
|
8
8
|
import debugbreak from "debugbreak";
|
|
9
|
-
import { Archives
|
|
9
|
+
import { Archives } from "./archives";
|
|
10
10
|
import { cache, lazy } from "socket-function/src/caching";
|
|
11
11
|
import { isDefined } from "../misc";
|
|
12
12
|
|
|
@@ -433,17 +433,6 @@ function unescapeFileName(fileName: string): string {
|
|
|
433
433
|
return fileName;
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
export const getArchivesLocal = cache((domain: string): Archives => {
|
|
437
|
-
|
|
438
|
-
const archivesLocal = new ArchivesDisk();
|
|
439
|
-
|
|
440
|
-
if (isNode()) {
|
|
441
|
-
archivesLocal.LOCAL_ARCHIVE_FOLDER = getSubFolder(domain).replaceAll("\\", "/");
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return archivesLocal;
|
|
445
|
-
});
|
|
446
|
-
|
|
447
436
|
export const getArchivesHome = cache((domain: string): Archives => {
|
|
448
437
|
const archivesLocal = new ArchivesDisk();
|
|
449
438
|
|
|
@@ -24,21 +24,29 @@ export function testDisableCache() {
|
|
|
24
24
|
cacheDisabled = true;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export type MemoryCacheConfig = {
|
|
28
|
+
extraReadSize?: number;
|
|
29
|
+
maxSize?: number;
|
|
30
|
+
maxCount?: number;
|
|
31
|
+
// If the files are guaranteed to be immutable, then all reads, including reading entire file and reading any range, can be cached.
|
|
32
|
+
// - Otherwise, we can't cache when the entire file is read, as there might be further appends after it.
|
|
33
|
+
// - Also, it means if there's a range read that happens, if that range read doesn't return enough bytes, for example, we try to read from 0 to 100, but we only get 50 bytes back, we can only cache the 0 to 50, and then if they read from 0 to 100 again, we can use the cache, but we have to also read 50 to 100 from the source, because the file might have gotten larger.
|
|
34
|
+
fullyImmutable?: boolean;
|
|
35
|
+
stats?: ArchivesMemoryCacheStats;
|
|
36
|
+
sizeCache?: Map<string, number>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The operations the cache actually needs from its source, so any archives flavor (old Archives or IArchives) can be cached. */
|
|
40
|
+
export type MemoryCacheSource = {
|
|
41
|
+
get(path: string, config?: { range?: { start: number; end: number; } }): Promise<Buffer | undefined>;
|
|
42
|
+
getInfo(path: string): Promise<{ size: number } | undefined>;
|
|
43
|
+
};
|
|
44
|
+
|
|
27
45
|
/** Creates an in-memory cache for the archives. We expect that the values will only have values appended to them or be deleted. Also, if a value is deleted, we won't erase our in-memory cache, and so we will still allow reading from deleted values. */
|
|
28
|
-
export function
|
|
29
|
-
archives:
|
|
30
|
-
config?:
|
|
31
|
-
|
|
32
|
-
maxSize?: number;
|
|
33
|
-
maxCount?: number;
|
|
34
|
-
// If the files are guaranteed to be immutable, then all reads, including reading entire file and reading any range, can be cached.
|
|
35
|
-
// - Otherwise, we can't cache when the entire file is read, as there might be further appends after it.
|
|
36
|
-
// - Also, it means if there's a range read that happens, if that range read doesn't return enough bytes, for example, we try to read from 0 to 100, but we only get 50 bytes back, we can only cache the 0 to 50, and then if they read from 0 to 100 again, we can use the cache, but we have to also read 50 to 100 from the source, because the file might have gotten larger.
|
|
37
|
-
fullyImmutable?: boolean;
|
|
38
|
-
stats?: ArchivesMemoryCacheStats;
|
|
39
|
-
sizeCache?: Map<string, number>;
|
|
40
|
-
}
|
|
41
|
-
): Archives {
|
|
46
|
+
export function createMemoryCachedGet(
|
|
47
|
+
archives: MemoryCacheSource,
|
|
48
|
+
config?: MemoryCacheConfig
|
|
49
|
+
) {
|
|
42
50
|
let {
|
|
43
51
|
maxSize = 1024 * 1024 * 1024 * 4,
|
|
44
52
|
maxCount = 1000 * 1000,
|
|
@@ -221,8 +229,6 @@ export function createArchivesMemoryCache(
|
|
|
221
229
|
|
|
222
230
|
async function cachedGet(path: string, getConfig?: {
|
|
223
231
|
range?: { start: number; end: number; };
|
|
224
|
-
retryCount?: number;
|
|
225
|
-
fastRead?: boolean;
|
|
226
232
|
}): Promise<Buffer | undefined> {
|
|
227
233
|
if (cacheDisabled) {
|
|
228
234
|
let result = await archives.get(path, getConfig);
|
|
@@ -374,7 +380,11 @@ export function createArchivesMemoryCache(
|
|
|
374
380
|
return result;
|
|
375
381
|
}
|
|
376
382
|
|
|
383
|
+
return cachedGet;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function createArchivesMemoryCache(archives: Archives, config?: MemoryCacheConfig): Archives {
|
|
377
387
|
return createArchivesOverride(archives, {
|
|
378
|
-
get:
|
|
388
|
+
get: createMemoryCachedGet(archives, config),
|
|
379
389
|
});
|
|
380
390
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { IArchives } from "sliftutils/storage/IArchives";
|
|
2
|
+
import { createMemoryCachedGet, MemoryCacheConfig } from "./archivesMemoryCache";
|
|
3
|
+
|
|
4
|
+
/** See createMemoryCachedGet — the same in-memory range cache, wrapping an IArchives (archives2). */
|
|
5
|
+
export function createArchivesMemoryCache2(archives: IArchives, config?: MemoryCacheConfig): IArchives {
|
|
6
|
+
let cachedGet = createMemoryCachedGet(archives, config);
|
|
7
|
+
return {
|
|
8
|
+
get: cachedGet,
|
|
9
|
+
getDebugName: () => archives.getDebugName(),
|
|
10
|
+
hasWriteAccess: () => archives.hasWriteAccess(),
|
|
11
|
+
get2: (fileName, getConfig) => archives.get2(fileName, getConfig),
|
|
12
|
+
set: (fileName, data, setConfig) => archives.set(fileName, data, setConfig),
|
|
13
|
+
del: (fileName) => archives.del(fileName),
|
|
14
|
+
setLargeFile: (largeConfig) => archives.setLargeFile(largeConfig),
|
|
15
|
+
getInfo: (fileName) => archives.getInfo(fileName),
|
|
16
|
+
find: (prefix, findConfig) => archives.find(prefix, findConfig),
|
|
17
|
+
findInfo: (prefix, findConfig) => archives.findInfo(prefix, findConfig),
|
|
18
|
+
getURL: (path) => archives.getURL(path),
|
|
19
|
+
getConfig: () => archives.getConfig(),
|
|
20
|
+
getChangesAfter2: (changesConfig) => archives.getChangesAfter2(changesConfig),
|
|
21
|
+
getSyncStatus: archives.getSyncStatus && (() => archives.getSyncStatus!()),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { measureWrap } from "socket-function/src/profiling/measure";
|
|
2
2
|
import { getIdentityCA, getMachineId, getOwnMachineId } from "sliftutils/misc/https/certs";
|
|
3
|
-
import {
|
|
3
|
+
import { getArchives2 } from "../-a-archives/archives2";
|
|
4
4
|
import { isNode, throttleFunction, timeInHour, timeInSecond } from "socket-function/src/misc";
|
|
5
5
|
import { SocketFunctionHook } from "socket-function/SocketFunctionTypes";
|
|
6
6
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
@@ -21,7 +21,7 @@ import { magenta } from "socket-function/src/formatting/logColors";
|
|
|
21
21
|
const UNTRUST_CACHE_TIME = 30 * timeInSecond;
|
|
22
22
|
const TRUSTED_CACHE_RESET_INTERVAL = timeInHour;
|
|
23
23
|
|
|
24
|
-
const archives = lazy(() =>
|
|
24
|
+
const archives = lazy(() => getArchives2("trust2/"));
|
|
25
25
|
|
|
26
26
|
export const requiresNetworkTrustHook: SocketFunctionHook = async config => {
|
|
27
27
|
// HACK: On the clientside we strip the domain process and machine id, so we can no longer determine
|
|
@@ -5,13 +5,13 @@ import { cache, lazy } from "socket-function/src/caching";
|
|
|
5
5
|
import { hasDNSWritePermissions, setRecord } from "../-b-authorities/dnsAuthority";
|
|
6
6
|
import { magenta } from "socket-function/src/formatting/logColors";
|
|
7
7
|
import { createKeyStore } from "../persistentLocalStore";
|
|
8
|
-
import {
|
|
8
|
+
import { getArchives2 } from "../-a-archives/archives2";
|
|
9
9
|
import { isNoNetwork } from "../config";
|
|
10
10
|
import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
|
|
11
11
|
import { delay } from "socket-function/src/batching";
|
|
12
12
|
import { timeInMinute } from "socket-function/src/misc";
|
|
13
13
|
|
|
14
|
-
const archives = lazy(() =>
|
|
14
|
+
const archives = lazy(() => getArchives2(`https_certs_3/`));
|
|
15
15
|
// Expire EXPIRATION_THRESHOLD% of the way through the certificate's lifetime
|
|
16
16
|
const EXPIRATION_THRESHOLD = 0.4;
|
|
17
17
|
// Let's Encrypt only accepts RSA or ECDSA account keys (not Ed25519, which is all sliftutils/certs generates).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
-
import {
|
|
2
|
+
import { getArchives2 } from "../-a-archives/archives2";
|
|
3
3
|
import { getDomain, isDevDebugbreak, isNoNetwork, isPublic } from "../config";
|
|
4
4
|
import { measureBlock } from "socket-function/src/profiling/measure";
|
|
5
5
|
import { isNode, keyByArray, sha256Hash, throttleFunction, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
@@ -56,7 +56,7 @@ let DEAD_NODE_POLL_COOLDOWN = timeInMinute * 5;
|
|
|
56
56
|
|
|
57
57
|
let shutdown = false;
|
|
58
58
|
|
|
59
|
-
const archives = lazy(() =>
|
|
59
|
+
const archives = lazy(() => getArchives2("nodes/"));
|
|
60
60
|
|
|
61
61
|
let logging = true;
|
|
62
62
|
export function enableNodeDiscoveryLogging() {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
2
|
-
import { nestArchives } from "../-a-archives/archives";
|
|
3
|
-
import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
|
|
4
|
-
import { archiveJSONT } from "../-a-archives/archivesJSONT";
|
|
5
|
-
import { getDomain, isPublic } from "../config";
|
|
6
2
|
import { cache, lazy } from "socket-function/src/caching";
|
|
7
3
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
8
4
|
import { delay, runInSerial, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { isNode } from "typesafecss";
|
|
13
|
-
import {
|
|
14
|
-
import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
|
|
13
|
+
import { getArchives2 } from "../-a-archives/archives2";
|
|
15
14
|
import { archiveJSONT } from "../-a-archives/archivesJSONT";
|
|
16
|
-
import { getDomain } from "../config";
|
|
17
15
|
import { sort } from "socket-function/src/misc";
|
|
18
16
|
import { getPathFromStr, getPathStr } from "../path";
|
|
19
17
|
import { green, red } from "socket-function/src/formatting/logColors";
|
|
@@ -22,7 +20,7 @@ type PrefixObj = {
|
|
|
22
20
|
prefixes: string[];
|
|
23
21
|
};
|
|
24
22
|
|
|
25
|
-
let prefixes = archiveJSONT<PrefixObj>(() =>
|
|
23
|
+
let prefixes = archiveJSONT<PrefixObj>(() => getArchives2("shard-prefixes/"));
|
|
26
24
|
// We don't actually need a collection, we only need a single value. However, the collection interface is a lot more convenient than manually dealing with backplays files.
|
|
27
25
|
const key = "all";
|
|
28
26
|
|
|
@@ -32,6 +32,7 @@ const ARCHIVE_PROPAGATION_TIME = timeInSecond * 5;
|
|
|
32
32
|
const MAX_APPLY_TRIES = 10;
|
|
33
33
|
const CONCURRENT_READ_COUNT = 32;
|
|
34
34
|
const CONCURRENT_WRITE_COUNT = 16;
|
|
35
|
+
const CONCURRENT_TRANSACTION_READS = 8;
|
|
35
36
|
|
|
36
37
|
/** 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
38
|
async function moveArchiveFile(from: IArchives, to: IArchives, path: string): Promise<void> {
|
|
@@ -391,7 +392,7 @@ class TransactionLocker {
|
|
|
391
392
|
let path = this.getConfirmKey(key);
|
|
392
393
|
console.info(`Creating confirmation for ${key}, ${countLeft} left`);
|
|
393
394
|
if (!await this.storage.getValue(path)) {
|
|
394
|
-
await this.storage.setValue(path, Buffer.from(
|
|
395
|
+
await this.storage.setValue(path, Buffer.from(JSON.stringify({ time: Date.now(), node: getOwnNodeId() })));
|
|
395
396
|
}
|
|
396
397
|
return path;
|
|
397
398
|
}
|
|
@@ -410,7 +411,11 @@ class TransactionLocker {
|
|
|
410
411
|
if (!op) return;
|
|
411
412
|
if (op.type === "create") {
|
|
412
413
|
let key = op.key;
|
|
413
|
-
|
|
414
|
+
const value = op.value;
|
|
415
|
+
if (!value || value.length === 0) {
|
|
416
|
+
throw new Error(`Create op for ${key} has an empty value. Empty files are treated as deletes by the storage layer, so this create would vanish and the transaction could never be confirmed.`);
|
|
417
|
+
}
|
|
418
|
+
await this.storage.setValue(key, value);
|
|
414
419
|
}
|
|
415
420
|
}
|
|
416
421
|
};
|
|
@@ -494,11 +499,16 @@ class TransactionLocker {
|
|
|
494
499
|
createTime: number;
|
|
495
500
|
source: FileInfo;
|
|
496
501
|
})[] = [];
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
502
|
+
let transactionFiles = files.filter(x => x.file.endsWith(".transaction"));
|
|
503
|
+
// Set when any reader finds a missing value, so the other parallel readers stop early.
|
|
504
|
+
let missingValue = false;
|
|
505
|
+
const readTransaction = async (tFile: FileInfo) => {
|
|
506
|
+
if (missingValue) return;
|
|
500
507
|
let buffer = await this.storage.getValue(tFile.file);
|
|
501
|
-
if (!buffer)
|
|
508
|
+
if (!buffer) {
|
|
509
|
+
missingValue = true;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
502
512
|
bufferCache.set(tFile.file, buffer);
|
|
503
513
|
let transaction: Transaction | undefined;
|
|
504
514
|
try {
|
|
@@ -507,7 +517,7 @@ class TransactionLocker {
|
|
|
507
517
|
// Maybe it is actively being written to? Or it is just broken. Skip it.
|
|
508
518
|
console.error(`Failed to parse transaction. Ignoring, ${tFile.file}, ${e.stack}`);
|
|
509
519
|
}
|
|
510
|
-
if (!transaction)
|
|
520
|
+
if (!transaction) return;
|
|
511
521
|
|
|
512
522
|
let { name } = parsePath(tFile.file);
|
|
513
523
|
let kvp = parseFileNameKVP(name);
|
|
@@ -518,8 +528,11 @@ class TransactionLocker {
|
|
|
518
528
|
createTime: tFile.createTime,
|
|
519
529
|
source: tFile,
|
|
520
530
|
});
|
|
521
|
-
}
|
|
531
|
+
};
|
|
532
|
+
let readTransactionParallel = runInParallel({ parallelCount: CONCURRENT_TRANSACTION_READS }, readTransaction);
|
|
533
|
+
await Promise.all(transactionFiles.map(readTransactionParallel));
|
|
522
534
|
this.perf.checkpoint("read transaction files");
|
|
535
|
+
if (missingValue) return false;
|
|
523
536
|
|
|
524
537
|
// Check all of our files to see if any have changed (or if there a new files).
|
|
525
538
|
// Because we don't reuse file names, or change them, it means if a file exists
|
|
@@ -664,7 +677,7 @@ class TransactionLocker {
|
|
|
664
677
|
}
|
|
665
678
|
return true;
|
|
666
679
|
}
|
|
667
|
-
private async applyTransaction(transaction: Transaction): Promise<void> {
|
|
680
|
+
private async applyTransaction(transaction: Transaction & { source: FileInfo }): Promise<void> {
|
|
668
681
|
let createCount = transaction.ops.filter(a => a.type === "create").length;
|
|
669
682
|
let deleteCount = transaction.ops.filter(a => a.type === "delete").length;
|
|
670
683
|
let lockedFiles = transaction.lockedFilesMustEqual?.length;
|
|
@@ -685,7 +698,7 @@ class TransactionLocker {
|
|
|
685
698
|
if (op.type === "create") {
|
|
686
699
|
await this.createConfirm(op.key, opsRemaining.length);
|
|
687
700
|
} else if (op.type === "delete") {
|
|
688
|
-
await this.deleteDataFile(op.key, `transaction
|
|
701
|
+
await this.deleteDataFile(op.key, `transaction for ${transaction.source.file}`, opsRemaining.length);
|
|
689
702
|
} else {
|
|
690
703
|
let unhandled: never = op;
|
|
691
704
|
throw new Error(`Unhandled type: ${unhandled}`);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { sha256 } from "js-sha256";
|
|
2
|
-
import { getArchives } from "../../-a-archives/archives";
|
|
3
2
|
import { getAllNodeIds, getOwnThreadId } from "../../-f-node-discovery/NodeDiscovery";
|
|
4
3
|
import { archives, pathValueArchives } from "../pathValueArchives";
|
|
5
4
|
import { ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../../errors";
|
|
@@ -139,6 +139,7 @@ export class PathValueArchives {
|
|
|
139
139
|
if (!config?.noFilter) {
|
|
140
140
|
values = await this.filterForDiskStorage(values);
|
|
141
141
|
}
|
|
142
|
+
if (values.length === 0) return undefined;
|
|
142
143
|
|
|
143
144
|
let buffers = await pathValueSerializer.serialize(values, {
|
|
144
145
|
noLocks: true,
|
|
@@ -18,26 +18,28 @@ declare global {
|
|
|
18
18
|
var getEdgeNodeConfig: undefined | (() => Promise<EdgeNodeConfig>);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
let getCachedConfig = cache(async (
|
|
21
|
+
let getCachedConfig = cache(async (urlsKey: string): Promise<EdgeNodesIndex | undefined> => {
|
|
22
22
|
setTimeout(() => {
|
|
23
|
-
getCachedConfig.clear(
|
|
23
|
+
getCachedConfig.clear(urlsKey);
|
|
24
24
|
// NOTE: We want deploying to be fast, so we don't cache for long. We also don't
|
|
25
25
|
// get the new value every 30 seconds, as we don't want to create too many requests.
|
|
26
26
|
}, timeInSecond * 30);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
for (let url of urlsKey.split("\n")) {
|
|
29
|
+
try {
|
|
30
|
+
let response = await fetch(url);
|
|
31
|
+
if (!response.ok) continue;
|
|
32
|
+
return await response.json();
|
|
33
|
+
} catch { }
|
|
33
34
|
}
|
|
35
|
+
return undefined;
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
export async function getEdgeBootstrapScript(config: {
|
|
37
|
-
|
|
39
|
+
edgeNodeConfigURLs: string[];
|
|
38
40
|
}): Promise<string> {
|
|
39
41
|
return await measureBlock(async function getEdgeBootstrapScript() {
|
|
40
|
-
let cachedConfig = await getCachedConfig(config.
|
|
42
|
+
let cachedConfig = await getCachedConfig(config.edgeNodeConfigURLs.join("\n"));
|
|
41
43
|
let allowedArgs = ["--local"];
|
|
42
44
|
// TODO: Be smarter about this (getting values and not just flags)
|
|
43
45
|
let argv = process.argv.filter(x => allowedArgs.includes(x));
|
|
@@ -72,7 +74,7 @@ declare global {
|
|
|
72
74
|
*/
|
|
73
75
|
|
|
74
76
|
async function edgeNodeFunction(config: {
|
|
75
|
-
|
|
77
|
+
edgeNodeConfigURLs: string[];
|
|
76
78
|
cachedConfig: EdgeNodesIndex | undefined;
|
|
77
79
|
argv: string[];
|
|
78
80
|
}) {
|
|
@@ -302,8 +304,45 @@ async function edgeNodeFunction(config: {
|
|
|
302
304
|
return false;
|
|
303
305
|
}
|
|
304
306
|
}
|
|
307
|
+
// Fetches all the URLs at the same time and uses whichever responds first, aborting the rest (so slow sources don't hold sockets — chrome limits us to 6 per domain). Racing redundant sources is normally wasteful, but this is the very first request of the page load, so minimizing its latency is a huge win.
|
|
308
|
+
async function fetchFirstEdgeIndex(urls: string[]): Promise<EdgeNodesIndex> {
|
|
309
|
+
let controllers = urls.map(() => new AbortController());
|
|
310
|
+
return await new Promise<EdgeNodesIndex>((resolve, reject) => {
|
|
311
|
+
let resolved = false;
|
|
312
|
+
let pendingCount = urls.length;
|
|
313
|
+
let errors: string[] = [];
|
|
314
|
+
if (!pendingCount) {
|
|
315
|
+
reject(new Error(`No edge node config URLs to fetch`));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
urls.forEach((url, index) => {
|
|
319
|
+
void (async () => {
|
|
320
|
+
try {
|
|
321
|
+
let response = await fetch(url, { signal: controllers[index].signal });
|
|
322
|
+
if (!response.ok) throw new Error(`Bad status ${response.status} from ${url}`);
|
|
323
|
+
let edgeIndex = await response.json() as EdgeNodesIndex;
|
|
324
|
+
if (resolved) return;
|
|
325
|
+
resolved = true;
|
|
326
|
+
for (let otherIndex = 0; otherIndex < controllers.length; otherIndex++) {
|
|
327
|
+
if (otherIndex !== index) {
|
|
328
|
+
controllers[otherIndex].abort();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
resolve(edgeIndex);
|
|
332
|
+
} catch (e: any) {
|
|
333
|
+
errors.push(`${url}: ${e?.stack ?? e}`);
|
|
334
|
+
pendingCount--;
|
|
335
|
+
if (!resolved && pendingCount === 0) {
|
|
336
|
+
reject(new Error(`All edge node config URLs failed: ${errors.join(" | ")}`));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
})();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
305
344
|
async function getEdgeNodeConfig(): Promise<EdgeNodeConfig> {
|
|
306
|
-
let edgeIndex = cachedConfig || await (
|
|
345
|
+
let edgeIndex = cachedConfig || await fetchFirstEdgeIndex(config.edgeNodeConfigURLs);
|
|
307
346
|
cachedConfig = undefined;
|
|
308
347
|
|
|
309
348
|
edgeIndex.edgeNodes.sort((a, b) => -(a.bootTime - b.bootTime));
|