querysub 0.604.0 → 0.606.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/archives2.ts +1 -0
- package/src/-a-archives/archivesJSONT.ts +8 -7
- package/src/-d-trust/NetworkTrust2.ts +1 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +6 -6
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +40 -19
- package/src/deployManager/machineSchema.ts +10 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.606.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",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"pako": "^2.1.0",
|
|
77
77
|
"peggy": "^5.0.6",
|
|
78
78
|
"sliftutils": "^1.7.83",
|
|
79
|
-
"socket-function": "^1.2.
|
|
79
|
+
"socket-function": "^1.2.30",
|
|
80
80
|
"terser": "^5.31.0",
|
|
81
81
|
"typenode": "^6.6.1",
|
|
82
82
|
"typesafecss": "^0.32.0",
|
|
@@ -121,6 +121,7 @@ function nestBucket(folder: string, archives: ReturnType<typeof createArchives>)
|
|
|
121
121
|
let changes = await archives.getChangesAfter2(config);
|
|
122
122
|
return changes.filter(x => x.path.startsWith(folder)).map(x => ({ ...x, path: x.path.slice(folder.length) }));
|
|
123
123
|
},
|
|
124
|
+
getSyncStatus: async () => await archives.getSyncStatus(),
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
+
import { FindConfig, IArchives } from "sliftutils/storage/IArchives";
|
|
5
|
+
import { SetConfig } from "sliftutils/storage/IArchives";
|
|
5
6
|
|
|
6
7
|
export type ArchiveT<T> = {
|
|
7
8
|
get(key: string): Promise<T | undefined>;
|
|
@@ -13,7 +14,7 @@ export type ArchiveT<T> = {
|
|
|
13
14
|
[Symbol.asyncIterator](): AsyncIterator<[string, T]>;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
|
|
17
|
+
export function archiveJSONT<T>(archives: () => IArchives, config?: { fallbacks?: boolean }): ArchiveT<T> {
|
|
17
18
|
archives = lazy(archives);
|
|
18
19
|
|
|
19
20
|
let valuesCache = new Map<string, {
|
|
@@ -30,7 +31,7 @@ export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
|
|
|
30
31
|
async function set(key: string, value: T) {
|
|
31
32
|
let a = archives();
|
|
32
33
|
console.log(`In archiveJSONT ${a.getDebugName()}, setting ${key} to ${JSON.stringify(value)}`);
|
|
33
|
-
await a.set(key, Buffer.from(JSON.stringify(value)));
|
|
34
|
+
await a.set(key, Buffer.from(JSON.stringify(value)), config);
|
|
34
35
|
}
|
|
35
36
|
async function deleteFnc(key: string) {
|
|
36
37
|
let a = archives();
|
|
@@ -38,10 +39,10 @@ export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
|
|
|
38
39
|
await a.del(key);
|
|
39
40
|
}
|
|
40
41
|
async function keys() {
|
|
41
|
-
return (await archives().find("")).map(value => value.toString());
|
|
42
|
+
return (await archives().find("", config)).map(value => value.toString());
|
|
42
43
|
}
|
|
43
44
|
async function values() {
|
|
44
|
-
let infos = await archives().findInfo("");
|
|
45
|
+
let infos = await archives().findInfo("", config);
|
|
45
46
|
|
|
46
47
|
let needsUpdate = false;
|
|
47
48
|
let currentKeys = new Set(infos.map(info => info.path));
|
|
@@ -63,7 +64,7 @@ export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
|
|
|
63
64
|
let maxRetries = 10;
|
|
64
65
|
let updated = false;
|
|
65
66
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
66
|
-
infos = await archives().findInfo("");
|
|
67
|
+
infos = await archives().findInfo("", config);
|
|
67
68
|
|
|
68
69
|
let newCache = new Map<string, {
|
|
69
70
|
createTime: number;
|
|
@@ -92,7 +93,7 @@ export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
|
|
|
92
93
|
|
|
93
94
|
if (!allFound) continue;
|
|
94
95
|
|
|
95
|
-
let newInfos = await archives().findInfo("");
|
|
96
|
+
let newInfos = await archives().findInfo("", config);
|
|
96
97
|
if (newInfos.length !== infos.length) continue;
|
|
97
98
|
function anyChanged() {
|
|
98
99
|
for (let i = 0; i < newInfos.length; i++) {
|
|
@@ -132,7 +132,7 @@ export const ensureWeAreTrusted = lazy(measureWrap(async () => {
|
|
|
132
132
|
let machineKeyCert = getIdentityCA(getDomain());
|
|
133
133
|
let machineId = getOwnMachineId(getDomain());
|
|
134
134
|
if (!await archives().get(machineId)) {
|
|
135
|
-
await archives().set(machineId, machineKeyCert.cert);
|
|
135
|
+
await archives().set(machineId, machineKeyCert.cert, { fallbacks: true });
|
|
136
136
|
}
|
|
137
137
|
}));
|
|
138
138
|
|
|
@@ -308,7 +308,7 @@ export async function triggerNodeChange() {
|
|
|
308
308
|
|
|
309
309
|
|
|
310
310
|
async function clearDeadThreadsFromArchives() {
|
|
311
|
-
let nodes = await archives().find("");
|
|
311
|
+
let nodes = await archives().find("", { fallbacks: true });
|
|
312
312
|
|
|
313
313
|
function getPortHash(nodeId: string) {
|
|
314
314
|
let obj = decodeNodeId(nodeId, getDomain());
|
|
@@ -329,7 +329,7 @@ async function clearDeadThreadsFromArchives() {
|
|
|
329
329
|
let deadThreads = nodeIds.filter(nodeId => nodeId !== aliveNodeId);
|
|
330
330
|
await Promise.all(deadThreads.map(async deadNodeId => {
|
|
331
331
|
console.log(`Removing dead thread. We contacted a node on the same port and same machine (${aliveNodeId}), which means the port has been reused by another thread, which proves that the old thread has died, as otherwise the new thread would not be able to use it.`);
|
|
332
|
-
await archives().del(deadNodeId);
|
|
332
|
+
await archives().del(deadNodeId, { fallbacks: true });
|
|
333
333
|
}));
|
|
334
334
|
}
|
|
335
335
|
}
|
|
@@ -393,7 +393,7 @@ async function runHeartbeatAuditLoop() {
|
|
|
393
393
|
if (count >= DEAD_CHECK_COUNT) {
|
|
394
394
|
removedNodeIds.push(nodeId);
|
|
395
395
|
console.info(yellow(`Node ${nodeId} was found to be dead, removing from node list. Last heartbeat at ${formatDateTime(lastTime)}, dead threshold at ${formatDateTime(deadTime)}`));
|
|
396
|
-
await archives().del(nodeId);
|
|
396
|
+
await archives().del(nodeId, { fallbacks: true });
|
|
397
397
|
deadCount.delete(nodeId);
|
|
398
398
|
} else {
|
|
399
399
|
console.info(yellow(`Node ${nodeId} was found to be dead, last heartbeat at ${formatDateTime(lastTime)} < dead threshold at ${formatDateTime(deadTime)}, dead count ${count}/${DEAD_CHECK_COUNT}. Total nodes seen ${nodeIds.length}`));
|
|
@@ -497,7 +497,7 @@ async function writeHeartbeat() {
|
|
|
497
497
|
let nodeId = getMountNodeId();
|
|
498
498
|
console.log(green(`Writing heartbeat ${formatDateTime(now)} for self (${nodeId})`));
|
|
499
499
|
if (!nodeId) return;
|
|
500
|
-
await archives().set(nodeId, Buffer.from(now + ""));
|
|
500
|
+
await archives().set(nodeId, Buffer.from(now + ""), { fallbacks: true });
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
async function runServerSyncLoops() {
|
|
@@ -636,7 +636,7 @@ if (isServer()) {
|
|
|
636
636
|
|
|
637
637
|
|
|
638
638
|
export async function forceRemoveNode(nodeId: string) {
|
|
639
|
-
await archives().del(nodeId);
|
|
639
|
+
await archives().del(nodeId, { fallbacks: true });
|
|
640
640
|
void tellEveryoneNodesChanges(`forceRemoveNode ${nodeId}`);
|
|
641
641
|
}
|
|
642
642
|
|
|
@@ -646,7 +646,7 @@ export async function nodeDiscoveryShutdown() {
|
|
|
646
646
|
console.log(red(`Shutting down node discovery`));
|
|
647
647
|
shutdown = true;
|
|
648
648
|
if (isServer()) {
|
|
649
|
-
await archives().del(getOwnNodeId());
|
|
649
|
+
await archives().del(getOwnNodeId(), { fallbacks: true });
|
|
650
650
|
}
|
|
651
651
|
void tellEveryoneNodesChanges("nodeDiscoveryShutdown");
|
|
652
652
|
}
|
|
@@ -4,11 +4,11 @@ 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 { copyArchiveFile, IArchives } from "sliftutils/storage/IArchives";
|
|
7
|
+
import { ArchivesSyncStatus, copyArchiveFile, IArchives } from "sliftutils/storage/IArchives";
|
|
8
8
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
9
9
|
import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
|
|
10
10
|
import { devDebugbreak, getDomain } from "../../config";
|
|
11
|
-
import { logErrors } from "../../errors";
|
|
11
|
+
import { errorToUndefined, errorToUndefinedSilent, logErrors, timeoutToUndefined } from "../../errors";
|
|
12
12
|
import { saveSnapshot } from "./archiveSnapshots";
|
|
13
13
|
import { getNodeId } from "socket-function/src/nodeCache";
|
|
14
14
|
import { logNodeStateStats, logNodeStats } from "../../-0-hooks/hooks";
|
|
@@ -35,6 +35,8 @@ const ASSUME_CONFIRMED_BEFORE = Date.parse("2026-07-23T00:00:00.000Z");
|
|
|
35
35
|
const CONCURRENT_READ_COUNT = 128;
|
|
36
36
|
const CONCURRENT_WRITE_COUNT = 128;
|
|
37
37
|
const CONCURRENT_TRANSACTION_READS = 8;
|
|
38
|
+
// When getFiles fails, how long we wait on the lock store's sync status before assuming the store is unreachable and falling back to non-authoritative (backblaze) reads.
|
|
39
|
+
const FALLBACK_SYNC_STATUS_TIMEOUT = timeInSecond * 20;
|
|
38
40
|
|
|
39
41
|
/** 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. */
|
|
40
42
|
async function moveArchiveFile(from: IArchives, to: IArchives, path: string): Promise<void> {
|
|
@@ -60,9 +62,9 @@ export function createArchiveLocker2(config: {
|
|
|
60
62
|
debugKey,
|
|
61
63
|
propagationTime: ARCHIVE_PROPAGATION_TIME,
|
|
62
64
|
|
|
63
|
-
async getKeys() {
|
|
64
|
-
let filesRawValues = await archiveValues.findInfo("", { type: "files" });
|
|
65
|
-
let filesRawLocks = await archiveLocks.findInfo("", { type: "files" });
|
|
65
|
+
async getKeys(config) {
|
|
66
|
+
let filesRawValues = await archiveValues.findInfo("", { type: "files", fallbacks: config?.fallbacks });
|
|
67
|
+
let filesRawLocks = await archiveLocks.findInfo("", { type: "files", fallbacks: config?.fallbacks });
|
|
66
68
|
// Zero-byte files are deletes on new storage servers. Old storage servers still list them, so we filter here to make them behave the same.
|
|
67
69
|
let files: FileInfo[] = [...filesRawValues, ...filesRawLocks].filter(x => x.size > 0).map(x => ({
|
|
68
70
|
file: x.path,
|
|
@@ -71,14 +73,18 @@ export function createArchiveLocker2(config: {
|
|
|
71
73
|
}));
|
|
72
74
|
return files;
|
|
73
75
|
},
|
|
76
|
+
async getSyncStatus() {
|
|
77
|
+
if (!archiveLocks.getSyncStatus) return undefined;
|
|
78
|
+
return archiveLocks.getSyncStatus();
|
|
79
|
+
},
|
|
74
80
|
|
|
75
81
|
async setValue(key, value) {
|
|
76
82
|
await getArchives(key).set(key, value);
|
|
77
83
|
logNodeStats(`archives|Created TΔ`, formatNumber, 1);
|
|
78
84
|
},
|
|
79
|
-
async getValue(key) {
|
|
80
|
-
//
|
|
81
|
-
return getArchives(key).get(key, { noFallbacks:
|
|
85
|
+
async getValue(key, config) {
|
|
86
|
+
// Default to no fallbacks as we usually want this to be very safe. We don't want to apply transactions if the system isn't in a good state.
|
|
87
|
+
return await getArchives(key).get(key, { noFallbacks: !config?.fallbacks });
|
|
82
88
|
},
|
|
83
89
|
async deleteKey(key) {
|
|
84
90
|
let archives = await getArchives(key);
|
|
@@ -294,10 +300,12 @@ export type FileInfo = {
|
|
|
294
300
|
type StorageType = {
|
|
295
301
|
debugKey: string;
|
|
296
302
|
propagationTime: number;
|
|
297
|
-
getKeys(): Promise<FileInfo[]>;
|
|
303
|
+
getKeys(config?: { fallbacks?: boolean }): Promise<FileInfo[]>;
|
|
298
304
|
setValue(key: string, value: Buffer): Promise<void>;
|
|
299
|
-
getValue(key: string): Promise<Buffer | undefined>;
|
|
305
|
+
getValue(key: string, config?: { fallbacks?: boolean }): Promise<Buffer | undefined>;
|
|
300
306
|
deleteKey(key: string): Promise<void>;
|
|
307
|
+
/** Sync introspection of the lock store, if it supports it - undefined when unsupported. getFiles uses this to distinguish "store is up, this was a real error" from "store is unreachable, fall back to non-authoritative reads". */
|
|
308
|
+
getSyncStatus(): Promise<ArchivesSyncStatus | undefined>;
|
|
301
309
|
/** The amount of time before we have to wait until it is assumed that all reads
|
|
302
310
|
* this old will have been received.
|
|
303
311
|
* - On the local disk it could be 0, but we use a higher value so development is closer to production
|
|
@@ -473,7 +481,7 @@ class TransactionLocker {
|
|
|
473
481
|
|
|
474
482
|
private lastFilesRead: FileInfo[] | undefined;
|
|
475
483
|
private lastFilesReadTime: number | undefined;
|
|
476
|
-
private async readDataState(): Promise<{
|
|
484
|
+
private async readDataState(config?: { fallbacks?: boolean }): Promise<{
|
|
477
485
|
rawDataFiles: FileInfo[];
|
|
478
486
|
/** Confirmed FileInfos are === the FileInfos in rawDataFiles */
|
|
479
487
|
confirmedDataFiles: FileInfo[];
|
|
@@ -487,7 +495,7 @@ class TransactionLocker {
|
|
|
487
495
|
let bufferCache = new Map<string, Buffer>();
|
|
488
496
|
const tryToRead = async () => {
|
|
489
497
|
let time = Date.now();
|
|
490
|
-
let files = await this.storage.getKeys();
|
|
498
|
+
let files = await this.storage.getKeys(config);
|
|
491
499
|
this.perf.checkpoint("getKeys");
|
|
492
500
|
if (this.lastFilesRead && this.lastFilesReadTime) {
|
|
493
501
|
let prevFiles = new Set(this.lastFilesRead.map(a => a.file));
|
|
@@ -516,7 +524,7 @@ class TransactionLocker {
|
|
|
516
524
|
let missingValue = false;
|
|
517
525
|
const readTransaction = async (tFile: FileInfo) => {
|
|
518
526
|
if (missingValue) return;
|
|
519
|
-
let buffer = await this.storage.getValue(tFile.file);
|
|
527
|
+
let buffer = await this.storage.getValue(tFile.file, config);
|
|
520
528
|
if (!buffer) {
|
|
521
529
|
missingValue = true;
|
|
522
530
|
return;
|
|
@@ -554,7 +562,7 @@ class TransactionLocker {
|
|
|
554
562
|
// but validity should/can only depend on the current state, not past event transitions
|
|
555
563
|
// (it pretty much only depends on files existing).
|
|
556
564
|
{
|
|
557
|
-
let filesVerify = await this.storage.getKeys();
|
|
565
|
+
let filesVerify = await this.storage.getKeys(config);
|
|
558
566
|
this.perf.checkpoint("verify getKeys");
|
|
559
567
|
let filesVerifySet = new Set(filesVerify.map(a => a.file));
|
|
560
568
|
// If it changes while reading, read again. Otherwise, if there were no changes while reading,
|
|
@@ -790,11 +798,24 @@ class TransactionLocker {
|
|
|
790
798
|
* - Might run a transaction
|
|
791
799
|
*/
|
|
792
800
|
public async getFiles(): Promise<FileInfo[]> {
|
|
793
|
-
|
|
794
|
-
let
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
801
|
+
let time = Date.now();
|
|
802
|
+
let storageAlivePromise = errorToUndefinedSilent(this.storage.getSyncStatus());
|
|
803
|
+
try {
|
|
804
|
+
this.perf.start();
|
|
805
|
+
let obj = await this.getFilesBase();
|
|
806
|
+
logNodeStateStats(`ArchiveLock Data File`, formatNumber, obj.dataFiles.length);
|
|
807
|
+
this.perf.logAndFlush(`getFiles (${this.storage.debugKey})`);
|
|
808
|
+
return obj.dataFiles;
|
|
809
|
+
} catch (e) {
|
|
810
|
+
let duration0 = Date.now() - time;
|
|
811
|
+
if (await storageAlivePromise) {
|
|
812
|
+
throw e;
|
|
813
|
+
}
|
|
814
|
+
let duration = Date.now() - time;
|
|
815
|
+
console.error(`Falling back to non-authoritative path value files after ${formatTime(duration)} (${formatTime(duration0)}). NO data will be written to the database until our storage servers are back online!`, { error: e });
|
|
816
|
+
let dataState = await this.readDataState({ fallbacks: true });
|
|
817
|
+
return dataState.confirmedDataFiles;
|
|
818
|
+
}
|
|
798
819
|
}
|
|
799
820
|
private transactionAppliedCount = new Map<number, number>();
|
|
800
821
|
private async getFilesBase(): Promise<{
|
|
@@ -29,6 +29,16 @@ export const SERVICE_FOLDER = `${SERVICE_FOLDER_NAME}/`;
|
|
|
29
29
|
export const MACHINE_RESYNC_INTERVAL = timeInMinute * 15;
|
|
30
30
|
export const SERVICE_NODE_FILE_NAME = "serviceNodeId.txt";
|
|
31
31
|
|
|
32
|
+
|
|
33
|
+
export type MachineConfig = {
|
|
34
|
+
machineId: string;
|
|
35
|
+
disabled: boolean;
|
|
36
|
+
};
|
|
37
|
+
// We want all of these to have very high availability. Even if they're wrong, it's better to have an old service deployed or even view information on an old service, or even set information only in back plays rather than have the server be completely down.
|
|
38
|
+
export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"), { fallbacks: true });
|
|
39
|
+
export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"), { fallbacks: true });
|
|
40
|
+
export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"), { fallbacks: true });
|
|
41
|
+
|
|
32
42
|
export type MachineInfo = {
|
|
33
43
|
machineId: string;
|
|
34
44
|
|
|
@@ -182,13 +192,6 @@ function normalizeServiceConfig(config: ServiceConfig): ServiceConfig {
|
|
|
182
192
|
return config;
|
|
183
193
|
}
|
|
184
194
|
|
|
185
|
-
export type MachineConfig = {
|
|
186
|
-
machineId: string;
|
|
187
|
-
disabled: boolean;
|
|
188
|
-
};
|
|
189
|
-
export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"));
|
|
190
|
-
export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"));
|
|
191
|
-
export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"));
|
|
192
195
|
|
|
193
196
|
export type LaunchRecord = {
|
|
194
197
|
serviceId: string;
|