querysub 0.591.0 → 0.593.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 +5 -1
- package/bin/stop-machine.js +4 -0
- package/package.json +6 -3
- package/spec.txt +12 -0
- package/src/-a-archives/archiveCache.ts +28 -7
- package/src/-a-archives/archiveCache2.ts +4 -1
- package/src/-a-archives/archives.ts +2 -63
- package/src/-a-archives/archives2.ts +21 -17
- 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 +3 -3
- package/src/-e-certs/certAuthority.ts +2 -2
- package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
- package/src/0-path-value-core/AuthorityLookup.ts +0 -4
- package/src/0-path-value-core/PathRouter.ts +17 -16
- package/src/0-path-value-core/ShardPrefixes.ts +2 -4
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +133 -38
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -1
- package/src/0-path-value-core/pathValueArchives.ts +9 -5
- package/src/2-proxy/archiveMoveHarness.ts +1 -1
- 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/archiveapps/archiveJoinEntry.ts +1 -1
- package/src/config2.ts +1 -7
- package/src/deployManager/components/ServicesListPage.tsx +27 -8
- 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/storageSetup.ts +1 -1
- 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
|
@@ -5,7 +5,6 @@ import { getOwnNodeId, getOwnNodeIdAssert } from "../../-f-node-discovery/NodeDi
|
|
|
5
5
|
import { pathValueArchives } from "../pathValueArchives";
|
|
6
6
|
import { ArchiveLocker, ArchiveTransaction } from "./ArchiveLocks";
|
|
7
7
|
import { copyArchiveFile, IArchives } from "sliftutils/storage/IArchives";
|
|
8
|
-
import debugbreak from "debugbreak";
|
|
9
8
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
10
9
|
import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
|
|
11
10
|
import { devDebugbreak, getDomain } from "../../config";
|
|
@@ -15,7 +14,7 @@ import { getNodeId } from "socket-function/src/nodeCache";
|
|
|
15
14
|
import { logNodeStateStats, logNodeStats } from "../../-0-hooks/hooks";
|
|
16
15
|
import { parsePath, toFileNameKVP, parseFileNameKVP } from "../../misc";
|
|
17
16
|
import { logDisk } from "../../diagnostics/logs/diskLogger";
|
|
18
|
-
import { retryFunctional, runInParallel } from "socket-function/src/batching";
|
|
17
|
+
import { delay, retryFunctional, runInParallel } from "socket-function/src/batching";
|
|
19
18
|
|
|
20
19
|
/*
|
|
21
20
|
Data starts a .data files, from servers creating files.
|
|
@@ -30,8 +29,11 @@ const ARCHIVE_PROPAGATION_TIME = timeInSecond * 5;
|
|
|
30
29
|
// a few times though, just in case the storage system was having some issues. After enough
|
|
31
30
|
// tries though... we HAVE to just skip it, otherwise we will be stuck forever.
|
|
32
31
|
const MAX_APPLY_TRIES = 10;
|
|
32
|
+
// Confirms used to be zero-byte files, which the storage layer now treats as deletes, wiping every historical confirmation. Locked files created before this date are assumed confirmed, as anything invalid that old would have already been cleaned up.
|
|
33
|
+
const ASSUME_CONFIRMED_BEFORE = Date.parse("2026-07-23T00:00:00.000Z");
|
|
33
34
|
const CONCURRENT_READ_COUNT = 32;
|
|
34
35
|
const CONCURRENT_WRITE_COUNT = 16;
|
|
36
|
+
const CONCURRENT_TRANSACTION_READS = 8;
|
|
35
37
|
|
|
36
38
|
/** 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
39
|
async function moveArchiveFile(from: IArchives, to: IArchives, path: string): Promise<void> {
|
|
@@ -60,7 +62,8 @@ export function createArchiveLocker2(config: {
|
|
|
60
62
|
async getKeys() {
|
|
61
63
|
let filesRawValues = await archiveValues.findInfo("", { type: "files" });
|
|
62
64
|
let filesRawLocks = await archiveLocks.findInfo("", { type: "files" });
|
|
63
|
-
|
|
65
|
+
// 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.
|
|
66
|
+
let files: FileInfo[] = [...filesRawValues, ...filesRawLocks].filter(x => x.size > 0).map(x => ({
|
|
64
67
|
file: x.path,
|
|
65
68
|
createTime: x.createTime,
|
|
66
69
|
size: x.size,
|
|
@@ -77,12 +80,6 @@ export function createArchiveLocker2(config: {
|
|
|
77
80
|
},
|
|
78
81
|
async deleteKey(key) {
|
|
79
82
|
let archives = await getArchives(key);
|
|
80
|
-
if (
|
|
81
|
-
!await archives.getInfo(key)
|
|
82
|
-
&& !await archives.get(key)
|
|
83
|
-
) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
83
|
// ACTUALLY delete confirms, as they don't contain that much information, and just clutter up the recycle bin.
|
|
87
84
|
// - DO archive transactions, as it might be useful to view the old transaction history.
|
|
88
85
|
if (archives === archiveLocks && key.endsWith(".confirm")) {
|
|
@@ -216,7 +213,8 @@ export function createArchiveLocker2(config: {
|
|
|
216
213
|
while (pendingFiles.length > 0) {
|
|
217
214
|
let file = pendingFiles.pop()!;
|
|
218
215
|
let value = await storage.getValue(file.file);
|
|
219
|
-
|
|
216
|
+
// NOTE: The way we've updated our archives is it's going to return a result unless it throws. And it shouldn't throw very often. The only reason it would return undefined is if the file was empty. After I do the deploy, get keys won't return empty files, but I need to fix this to do the deploy. So we're just going to default any empty returns to have a value. So we stop saying that files are missing and rereading infinitely.
|
|
217
|
+
readResults.set(file.file, value ?? Buffer.alloc(0));
|
|
220
218
|
}
|
|
221
219
|
}
|
|
222
220
|
await Promise.all(list(CONCURRENT_READ_COUNT).map(runReadThread));
|
|
@@ -391,7 +389,7 @@ class TransactionLocker {
|
|
|
391
389
|
let path = this.getConfirmKey(key);
|
|
392
390
|
console.info(`Creating confirmation for ${key}, ${countLeft} left`);
|
|
393
391
|
if (!await this.storage.getValue(path)) {
|
|
394
|
-
await this.storage.setValue(path, Buffer.from(
|
|
392
|
+
await this.storage.setValue(path, Buffer.from(JSON.stringify({ time: Date.now(), node: getOwnNodeId() })));
|
|
395
393
|
}
|
|
396
394
|
return path;
|
|
397
395
|
}
|
|
@@ -410,11 +408,28 @@ class TransactionLocker {
|
|
|
410
408
|
if (!op) return;
|
|
411
409
|
if (op.type === "create") {
|
|
412
410
|
let key = op.key;
|
|
413
|
-
|
|
411
|
+
const value = op.value;
|
|
412
|
+
if (!value || value.length === 0) {
|
|
413
|
+
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.`);
|
|
414
|
+
}
|
|
415
|
+
await this.storage.setValue(key, value);
|
|
414
416
|
}
|
|
415
417
|
}
|
|
416
418
|
};
|
|
417
419
|
await Promise.all(list(CONCURRENT_WRITE_COUNT).map(runThread));
|
|
420
|
+
|
|
421
|
+
// Verify the files still exist. There shouldn't be any valid case where they disappear. If they do disappear, it's our storage's fault.
|
|
422
|
+
let keys = await this.storage.getKeys();
|
|
423
|
+
let keysSet = new Set(keys.map(a => a.file));
|
|
424
|
+
let creates = transaction.ops.filter(a => a.type === "create") as { type: "create"; key: string; value: Buffer | undefined }[];
|
|
425
|
+
let missingCreates = creates.filter(a => !keysSet.has(a.key));
|
|
426
|
+
if (missingCreates.length > 0) {
|
|
427
|
+
console.error(`We are missing files that we just wrote to storage. This shouldn't be possible because the files can't be cleaned up until they get old enough, so this means our storage is broken`, {
|
|
428
|
+
missingCount: missingCreates.length,
|
|
429
|
+
firstMissing: missingCreates[0]?.key,
|
|
430
|
+
firstMissingSize: missingCreates[0]?.value?.length,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
418
433
|
}
|
|
419
434
|
// Just writes the transaction (in a format readDataState can read)
|
|
420
435
|
private async writeTransaction(transaction: Transaction & { seqNum: number; }): Promise<void> {
|
|
@@ -494,11 +509,16 @@ class TransactionLocker {
|
|
|
494
509
|
createTime: number;
|
|
495
510
|
source: FileInfo;
|
|
496
511
|
})[] = [];
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
512
|
+
let transactionFiles = files.filter(x => x.file.endsWith(".transaction"));
|
|
513
|
+
// Set when any reader finds a missing value, so the other parallel readers stop early.
|
|
514
|
+
let missingValue = false;
|
|
515
|
+
const readTransaction = async (tFile: FileInfo) => {
|
|
516
|
+
if (missingValue) return;
|
|
500
517
|
let buffer = await this.storage.getValue(tFile.file);
|
|
501
|
-
if (!buffer)
|
|
518
|
+
if (!buffer) {
|
|
519
|
+
missingValue = true;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
502
522
|
bufferCache.set(tFile.file, buffer);
|
|
503
523
|
let transaction: Transaction | undefined;
|
|
504
524
|
try {
|
|
@@ -507,7 +527,7 @@ class TransactionLocker {
|
|
|
507
527
|
// Maybe it is actively being written to? Or it is just broken. Skip it.
|
|
508
528
|
console.error(`Failed to parse transaction. Ignoring, ${tFile.file}, ${e.stack}`);
|
|
509
529
|
}
|
|
510
|
-
if (!transaction)
|
|
530
|
+
if (!transaction) return;
|
|
511
531
|
|
|
512
532
|
let { name } = parsePath(tFile.file);
|
|
513
533
|
let kvp = parseFileNameKVP(name);
|
|
@@ -518,8 +538,11 @@ class TransactionLocker {
|
|
|
518
538
|
createTime: tFile.createTime,
|
|
519
539
|
source: tFile,
|
|
520
540
|
});
|
|
521
|
-
}
|
|
541
|
+
};
|
|
542
|
+
let readTransactionParallel = runInParallel({ parallelCount: CONCURRENT_TRANSACTION_READS }, readTransaction);
|
|
543
|
+
await Promise.all(transactionFiles.map(readTransactionParallel));
|
|
522
544
|
this.perf.checkpoint("read transaction files");
|
|
545
|
+
if (missingValue) return false;
|
|
523
546
|
|
|
524
547
|
// Check all of our files to see if any have changed (or if there a new files).
|
|
525
548
|
// Because we don't reuse file names, or change them, it means if a file exists
|
|
@@ -579,7 +602,7 @@ class TransactionLocker {
|
|
|
579
602
|
if (!file.file.endsWith(".locked")) continue;
|
|
580
603
|
let confirmKey = this.getConfirmKey(file.file);
|
|
581
604
|
let confirmFile = existingFiles.get(confirmKey);
|
|
582
|
-
if (confirmFile) {
|
|
605
|
+
if (confirmFile || file.createTime < ASSUME_CONFIRMED_BEFORE) {
|
|
583
606
|
currentDataFiles.set(file.file, file);
|
|
584
607
|
} else {
|
|
585
608
|
uncomfirmedCount++;
|
|
@@ -614,25 +637,67 @@ class TransactionLocker {
|
|
|
614
637
|
}
|
|
615
638
|
// #endregion
|
|
616
639
|
|
|
617
|
-
private isTransactionValid(transaction: Transaction, dataFiles: FileInfo[], rawDataFiles: FileInfo[]): boolean {
|
|
640
|
+
private async isTransactionValid(transaction: Transaction, dataFiles: FileInfo[], rawDataFiles: FileInfo[]): Promise<boolean> {
|
|
618
641
|
let override = this.isTransactionValidOverride;
|
|
619
642
|
if (override) return override(transaction, dataFiles, rawDataFiles);
|
|
620
643
|
if (transaction.lockedFilesMustEqual) {
|
|
621
644
|
let newLockedFiles = new Set(dataFiles.map(a => a.file).filter(a => a.endsWith(".locked")));
|
|
622
645
|
let mustEqualSet = new Set(transaction.lockedFilesMustEqual);
|
|
623
|
-
|
|
624
|
-
|
|
646
|
+
let missingFiles = Array.from(mustEqualSet).filter(a => !newLockedFiles.has(a));
|
|
647
|
+
let extraFiles = Array.from(newLockedFiles).filter(a => !mustEqualSet.has(a));
|
|
648
|
+
let missingFile = missingFiles[0];
|
|
649
|
+
if (missingFile) {
|
|
650
|
+
console.error(`Missing file, cannot apply transaction`, {
|
|
651
|
+
missing: missingFile,
|
|
652
|
+
totalMissing: missingFiles.length,
|
|
653
|
+
totalExtra: extraFiles.length,
|
|
654
|
+
});
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
let extraFile = extraFiles[0];
|
|
658
|
+
if (extraFile) {
|
|
659
|
+
let extra = await this.storage.getValue(extraFile);
|
|
660
|
+
console.error(`Extra file, cannot apply transaction`, {
|
|
661
|
+
extra: extraFile,
|
|
662
|
+
extraLength: extra?.length,
|
|
663
|
+
extraFirst1024: extra?.slice(0, 1024).toString("hex"),
|
|
664
|
+
totalMissing: missingFiles.length,
|
|
665
|
+
totalExtra: extraFiles.length,
|
|
666
|
+
});
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
if (mustEqualSet.size !== newLockedFiles.size) {
|
|
670
|
+
console.error(`Transaction not applied (locked files must equal but size mismatch)`, {
|
|
671
|
+
mustEqualSetSize: mustEqualSet.size,
|
|
672
|
+
newLockedFilesSize: newLockedFiles.size,
|
|
673
|
+
});
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
625
676
|
}
|
|
626
677
|
let existingFiles = new Set(dataFiles.map(a => a.file));
|
|
627
678
|
let existingRawFiles = new Set(rawDataFiles.map(a => a.file));
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
679
|
+
let missingCreates = transaction.ops.filter(a => a.type === "create" && !existingRawFiles.has(a.key)) as { type: "create"; key: string; value: Buffer | undefined }[];
|
|
680
|
+
let missingDeletes = transaction.ops.filter(a => a.type === "delete" && !existingFiles.has(a.key)) as { type: "delete"; key: string }[];
|
|
681
|
+
if (missingCreates.length > 0 || missingDeletes.length > 0) {
|
|
682
|
+
console.error(`Transaction not applied (missing creates or deletes)`, {
|
|
683
|
+
missingCreateFirst: missingCreates[0]?.key,
|
|
684
|
+
missingDeleteFirst: missingDeletes[0]?.key,
|
|
685
|
+
missingCreateFirstSize: missingCreates[0]?.value?.length,
|
|
686
|
+
totalMissingCreates: missingCreates.length,
|
|
687
|
+
totalMissingDeletes: missingDeletes.length,
|
|
688
|
+
});
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
for (let op of transaction.ops) {
|
|
692
|
+
const type = op.type;
|
|
693
|
+
if (type === "create") {
|
|
694
|
+
} else if (type === "delete") {
|
|
695
|
+
} else {
|
|
696
|
+
let unhandled: never = type;
|
|
697
|
+
throw new Error(`Unhandled op type: ${unhandled}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
636
701
|
}
|
|
637
702
|
|
|
638
703
|
// NOTE: If the transaction was applied long in the past, this will return false. HOWEVER, this is fine,
|
|
@@ -664,11 +729,11 @@ class TransactionLocker {
|
|
|
664
729
|
}
|
|
665
730
|
return true;
|
|
666
731
|
}
|
|
667
|
-
private async applyTransaction(transaction: Transaction): Promise<void> {
|
|
732
|
+
private async applyTransaction(transaction: Transaction & { source: FileInfo }): Promise<void> {
|
|
668
733
|
let createCount = transaction.ops.filter(a => a.type === "create").length;
|
|
669
734
|
let deleteCount = transaction.ops.filter(a => a.type === "delete").length;
|
|
670
735
|
let lockedFiles = transaction.lockedFilesMustEqual?.length;
|
|
671
|
-
console.
|
|
736
|
+
console.log(`Applying transaction with ${createCount} file creates and ${deleteCount} file deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
|
|
672
737
|
transactions: transaction.ops.map(x => JSON.stringify(x)),
|
|
673
738
|
});
|
|
674
739
|
logNodeStats(`archives|TΔ Apply`, formatNumber, 1);
|
|
@@ -685,7 +750,7 @@ class TransactionLocker {
|
|
|
685
750
|
if (op.type === "create") {
|
|
686
751
|
await this.createConfirm(op.key, opsRemaining.length);
|
|
687
752
|
} else if (op.type === "delete") {
|
|
688
|
-
await this.deleteDataFile(op.key, `transaction
|
|
753
|
+
await this.deleteDataFile(op.key, `transaction for ${transaction.source.file}`, opsRemaining.length);
|
|
689
754
|
} else {
|
|
690
755
|
let unhandled: never = op;
|
|
691
756
|
throw new Error(`Unhandled type: ${unhandled}`);
|
|
@@ -694,9 +759,29 @@ class TransactionLocker {
|
|
|
694
759
|
};
|
|
695
760
|
await Promise.all(list(CONCURRENT_WRITE_COUNT).map(runThread));
|
|
696
761
|
|
|
697
|
-
console.
|
|
762
|
+
console.log(`Applied transaction with ${createCount} file creates and file ${deleteCount} deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
|
|
698
763
|
transactions: transaction.ops.map(x => JSON.stringify(x)),
|
|
699
764
|
});
|
|
765
|
+
|
|
766
|
+
let newKeys = await this.storage.getKeys();
|
|
767
|
+
let newKeysSet = new Set(newKeys.map(a => a.file));
|
|
768
|
+
let createFiles = transaction.ops.filter(a => a.type === "create") as { type: "create"; key: string; value: Buffer | undefined }[];
|
|
769
|
+
let deleteFiles = transaction.ops.filter(a => a.type === "delete") as { type: "delete"; key: string }[];
|
|
770
|
+
let deleteSet = new Set(deleteFiles.map(a => a.key));
|
|
771
|
+
|
|
772
|
+
let missingCreates = createFiles.filter(a => !newKeysSet.has(a.key));
|
|
773
|
+
let extraFiles = newKeys.filter(a => deleteSet.has(a.file));
|
|
774
|
+
|
|
775
|
+
if (missingCreates.length > 0 || extraFiles.length > 0) {
|
|
776
|
+
console.error(`The storage system is broken and did not actually apply our transaction changes (transaction ${transaction.source.file})`, {
|
|
777
|
+
missingCreateFirst: missingCreates[0]?.key,
|
|
778
|
+
missingDeleteFirst: extraFiles[0]?.file,
|
|
779
|
+
missingCreateFirstSize: missingCreates[0]?.value?.length,
|
|
780
|
+
missingDeleteFirstSize: extraFiles[0]?.size,
|
|
781
|
+
totalMissingCreates: missingCreates.length,
|
|
782
|
+
totalExtraFiles: extraFiles.length,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
700
785
|
}
|
|
701
786
|
|
|
702
787
|
/** Only returns data files (no transaction files, or confirmations).
|
|
@@ -858,6 +943,13 @@ class TransactionLocker {
|
|
|
858
943
|
this.perf.checkpoint("apply transaction");
|
|
859
944
|
this.transactionAppliedCount.set(activeT.seqNum, applyCount + 1);
|
|
860
945
|
|
|
946
|
+
{
|
|
947
|
+
let dataState = await this.readDataState();
|
|
948
|
+
if (!this.wasTransactionApplied(activeT, dataState.confirmedDataFiles, dataState.rawDataFiles)) {
|
|
949
|
+
console.error(`Transaction ${activeT.seqNum} was not applied, even though we just applied it.`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
861
953
|
// Run again, until we can be reasonable sure activeT isn't changing. We can be wrong,
|
|
862
954
|
// though, which will just result in any transaction depending on activeT being
|
|
863
955
|
// rejecting, and having to be inserted again.
|
|
@@ -869,7 +961,7 @@ class TransactionLocker {
|
|
|
869
961
|
let dels = transaction.ops.filter(a => a.type === "delete").length;
|
|
870
962
|
let creates = transaction.ops.filter(a => a.type === "create").length;
|
|
871
963
|
let createBytes = transaction.ops.map(a => a.type === "create" && a.value?.length || 0).reduce((a, b) => a + b, 0);
|
|
872
|
-
console.
|
|
964
|
+
console.log(`Starting transaction with ${creates} file creates and ${dels} file deletes, ${formatNumber(createBytes)}B`, {
|
|
873
965
|
createFilesNames: transaction.ops.filter(a => a.type === "create").map(a => a.key),
|
|
874
966
|
deleteFilesNames: transaction.ops.filter(a => a.type === "delete").map(a => a.key),
|
|
875
967
|
});
|
|
@@ -896,9 +988,12 @@ class TransactionLocker {
|
|
|
896
988
|
|
|
897
989
|
while (true) {
|
|
898
990
|
let beforeData = await this.getFilesBase();
|
|
899
|
-
if (!this.isTransactionValid(transaction, beforeData.dataFiles, beforeData.rawDataFiles)) {
|
|
991
|
+
if (!await this.isTransactionValid(transaction, beforeData.dataFiles, beforeData.rawDataFiles)) {
|
|
900
992
|
logNodeStats(`archives|TΔ Rejected`, formatNumber, 1);
|
|
901
|
-
console.
|
|
993
|
+
console.log(`Finished transaction with rejection, ${transaction.ops.length} ops`);
|
|
994
|
+
await delay(30 * 1000);
|
|
995
|
+
let valid2 = await this.isTransactionValid(transaction, beforeData.dataFiles, beforeData.rawDataFiles);
|
|
996
|
+
console.log(`Valid2: ${valid2}`);
|
|
902
997
|
return "rejected";
|
|
903
998
|
}
|
|
904
999
|
|
|
@@ -907,7 +1002,7 @@ class TransactionLocker {
|
|
|
907
1002
|
let afterData = await this.getFilesBase();
|
|
908
1003
|
if (this.wasTransactionApplied(transaction, afterData.dataFiles, afterData.rawDataFiles)) {
|
|
909
1004
|
logNodeStats(`archives|TΔ Accepted`, formatNumber, 1);
|
|
910
|
-
console.
|
|
1005
|
+
console.log(`Finished transaction with ${transaction.ops.length} ops`);
|
|
911
1006
|
return "accepted";
|
|
912
1007
|
}
|
|
913
1008
|
}
|
|
@@ -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,
|
|
@@ -180,7 +181,7 @@ export class PathValueArchives {
|
|
|
180
181
|
|
|
181
182
|
@measureFnc
|
|
182
183
|
public async archiveValues(values: PathValue[]) {
|
|
183
|
-
let parts = PathRouter.getPathIdentifierTargets(values, authorityLookup.getOurSpec());
|
|
184
|
+
let parts = await PathRouter.getPathIdentifierTargets(values, authorityLookup.getOurSpec());
|
|
184
185
|
|
|
185
186
|
for (let [pathIdentifier, values] of parts) {
|
|
186
187
|
let encodedObj = await this.encodeValuePaths(values);
|
|
@@ -315,8 +316,9 @@ export class PathValueArchives {
|
|
|
315
316
|
let dataPath = pendingDataPaths.pop()!;
|
|
316
317
|
if (readCache.has(dataPath)) continue;
|
|
317
318
|
let data = await archives().get(dataPath);
|
|
318
|
-
if
|
|
319
|
-
|
|
319
|
+
// NOTE: The way we've updated our archives is it's going to return a result unless it throws. And it shouldn't throw very often. The only reason it would return undefined is if the file was empty. After I do the deploy, get keys won't return empty files, but I need to fix this to do the deploy. So we're just going to default any empty returns to have a value. So we stop saying that files are missing and rereading infinitely.
|
|
320
|
+
//if (!data) continue;
|
|
321
|
+
readCache.set(dataPath, data || Buffer.alloc(0));
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
324
|
await Promise.all(list(32).map(runReadThread));
|
|
@@ -358,6 +360,7 @@ export class PathValueArchives {
|
|
|
358
360
|
|
|
359
361
|
await safeLoop({ data: dataPaths, name: blue("Load PathValues From Buffer") }, async dataFile => {
|
|
360
362
|
let data = readCache.get(dataFile);
|
|
363
|
+
if (!data?.length) return;
|
|
361
364
|
if (!data) throw new Error(`Data file ${dataFile} not in lookup which we just checked?`);
|
|
362
365
|
totalSize += data.byteLength;
|
|
363
366
|
fileCount++;
|
|
@@ -409,6 +412,7 @@ export class PathValueArchives {
|
|
|
409
412
|
tooOldValues: number;
|
|
410
413
|
}> {
|
|
411
414
|
const { path, data } = config;
|
|
415
|
+
if (data.length === 0) return { values: [], rawCount: 0, tooOldValues: 0 };
|
|
412
416
|
let tooOldValues = 0;
|
|
413
417
|
|
|
414
418
|
let dataValues: PathValue[] = [];
|
|
@@ -422,10 +426,10 @@ export class PathValueArchives {
|
|
|
422
426
|
skipValues: config.skipValues,
|
|
423
427
|
});
|
|
424
428
|
if (dataValues.length !== decodedObj.valueCount) {
|
|
425
|
-
console.warn(`Bad archive data file at ${config.path}, Decoded count ${formatNumber(decodedObj.valueCount)} !== count in file name ${formatNumber(dataValues.length)} (${decodedObj.valueCount} !== ${dataValues.length})`);
|
|
429
|
+
console.warn(`Bad archive data file (size ${data.length}) at ${config.path}, Decoded count ${formatNumber(decodedObj.valueCount)} !== count in file name ${formatNumber(dataValues.length)} (${decodedObj.valueCount} !== ${dataValues.length})`);
|
|
426
430
|
}
|
|
427
431
|
} catch (e: any) {
|
|
428
|
-
console.warn(`Bad archive data file at ${config.path}, error: ${e.stack}`);
|
|
432
|
+
console.warn(`Bad archive data file (size ${data.length}) at ${config.path}, error: ${e.stack}`);
|
|
429
433
|
}
|
|
430
434
|
let rawCount = dataValues.length;
|
|
431
435
|
|
|
@@ -250,7 +250,7 @@ export async function runArchiveMover(config: {
|
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
for (let [key, values] of Object.entries(result.newValues)) {
|
|
253
|
-
let targets = PathRouter.getPathIdentifierTargets(values, authority);
|
|
253
|
+
let targets = await PathRouter.getPathIdentifierTargets(values, authority);
|
|
254
254
|
for (let [target, values] of targets) {
|
|
255
255
|
await addValues(target + "/", values, key);
|
|
256
256
|
}
|
|
@@ -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));
|
|
@@ -3,14 +3,12 @@ import { getOwnMachineId } from "sliftutils/misc/https/certs";
|
|
|
3
3
|
import { devDebugbreak, getDomain, isLocal, isPublic, noSyncing } from "../config";
|
|
4
4
|
import child_process from "child_process";
|
|
5
5
|
import { getAllNodeIds, getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
6
|
-
import {
|
|
7
|
-
import { nestArchives } from "../-a-archives/archives";
|
|
6
|
+
import { getArchives2Public } from "../-a-archives/archives2";
|
|
8
7
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
8
|
import { delay, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
10
9
|
import { compare, compareArray, isNodeTrue, sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
11
10
|
import { cacheLimited, lazy } from "socket-function/src/caching";
|
|
12
11
|
import { canHaveChildren } from "socket-function/src/types";
|
|
13
|
-
import { hostArchives } from "../-b-authorities/cdnAuthority";
|
|
14
12
|
import { getModuleFromConfig } from "../3-path-functions/pathFunctionLoader";
|
|
15
13
|
import path from "path";
|
|
16
14
|
import debugbreak from "debugbreak";
|
|
@@ -31,7 +29,7 @@ import type { FunctionRunnerIndex } from "../4-querysub/FunctionRunnerTracking";
|
|
|
31
29
|
const UPDATE_POLL_INTERVAL = timeInMinute * 15;
|
|
32
30
|
const DEAD_NODE_COUNT_THRESHOLD = 15;
|
|
33
31
|
|
|
34
|
-
const edgeNodeStorage = isNodeTrue() &&
|
|
32
|
+
const edgeNodeStorage = isNodeTrue() && getArchives2Public("edgenodes/");
|
|
35
33
|
const edgeNodeIndexFile = "edge-nodes-index.json";
|
|
36
34
|
|
|
37
35
|
const getEdgeNodeConfig = cacheLimited(10000, async (fileName: string): Promise<EdgeNodeConfig | undefined> => {
|
|
@@ -265,19 +263,15 @@ async function publishOwnScheduledShutdown(time: number) {
|
|
|
265
263
|
await updateEdgeNodesFile();
|
|
266
264
|
}
|
|
267
265
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
domain: getDomain(),
|
|
273
|
-
});
|
|
274
|
-
return await getURL(edgeNodeIndexFile);
|
|
275
|
-
});
|
|
266
|
+
/** Every public URL that can serve the edge node index (write node first, then fastest-first). Not cached: the underlying source config can change (scheduled window switchovers), and after the one initialization await this is synchronous anyway. */
|
|
267
|
+
export async function getEdgeNodeConfigURLs(): Promise<string[]> {
|
|
268
|
+
return await edgeNodeStorage.getURLs(edgeNodeIndexFile);
|
|
269
|
+
}
|
|
276
270
|
|
|
277
271
|
const startUpdateLoop = lazy(async () => {
|
|
278
272
|
startEdgeNotifier();
|
|
279
273
|
SocketFunction.expose(EdgeNodeController);
|
|
280
|
-
await
|
|
274
|
+
await getEdgeNodeConfigURLs();
|
|
281
275
|
await runInfinitePollCallAtStart(UPDATE_POLL_INTERVAL * (1 + Math.random() * 0.1), updateLoop);
|
|
282
276
|
});
|
|
283
277
|
|
|
@@ -825,9 +825,9 @@ export class Querysub {
|
|
|
825
825
|
|
|
826
826
|
RequireController.injectHTMLBeforeStartup(async () => {
|
|
827
827
|
const { getEdgeBootstrapScript } = await import("../4-deploy/edgeBootstrap");
|
|
828
|
-
const {
|
|
828
|
+
const { getEdgeNodeConfigURLs } = await import("../4-deploy/edgeNodes");
|
|
829
829
|
let edgeBootstrapFile = await getEdgeBootstrapScript({
|
|
830
|
-
|
|
830
|
+
edgeNodeConfigURLs: await getEdgeNodeConfigURLs(),
|
|
831
831
|
});
|
|
832
832
|
return `<script>${edgeBootstrapFile}</script>`;
|
|
833
833
|
});
|
|
@@ -99,7 +99,7 @@ async function runGenesisJoinIteration(config?: { force?: boolean }) {
|
|
|
99
99
|
transaction.deleteFiles.push(file);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
let targets = PathRouter.getPathIdentifierTargets(allCombinedValues, authoritySpec);
|
|
102
|
+
let targets = await PathRouter.getPathIdentifierTargets(allCombinedValues, authoritySpec);
|
|
103
103
|
for (let [target, values] of targets) {
|
|
104
104
|
|
|
105
105
|
let dataObj = await pathValueArchives.encodeValuePaths(values, {
|
package/src/config2.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { hasArchivesPermissions } from "./-a-archives/archives";
|
|
3
|
-
import { baseIsClient, getDomain } from "./config";
|
|
4
|
-
import { JSONLACKS } from "socket-function/src/JSONLACKS/JSONLACKS";
|
|
5
|
-
import { rootPathStr, prependToPathStr, getPathDepth } from "./path";
|
|
6
|
-
import fs from "fs";
|
|
7
|
-
import { AuthoritySpec } from "./0-path-value-core/PathRouter";
|
|
1
|
+
import { baseIsClient } from "./config";
|
|
8
2
|
|
|
9
3
|
export function isClient() {
|
|
10
4
|
return baseIsClient();
|
|
@@ -17,10 +17,12 @@ import { PendingDeployInfo, UpdateButtons, UpdateServiceButtons } from "./deploy
|
|
|
17
17
|
import { isDefined } from "../../misc";
|
|
18
18
|
import { formatDateJSX } from "../../misc/formatJSX";
|
|
19
19
|
import { Tools } from "./Tools";
|
|
20
|
-
import { ServiceMeasureBars, ServiceMeasureBreakdown } from "./ServiceMeasureBars";
|
|
20
|
+
import { ServiceMeasureBars, ServiceMeasureBreakdown, ServiceMeasureController } from "./ServiceMeasureBars";
|
|
21
21
|
|
|
22
22
|
module.hotreload = true;
|
|
23
23
|
|
|
24
|
+
const INVALIDATE_FLASH_TIME = timeInSecond;
|
|
25
|
+
|
|
24
26
|
/** What kind of service this is, when we can tell from its command, linking to wherever that kind is administered. Its own box beside the service's, so clicking the service still opens the service. */
|
|
25
27
|
class ServiceCategoryBadge extends qreact.Component<{ config: ServiceConfig }> {
|
|
26
28
|
render() {
|
|
@@ -45,6 +47,7 @@ export class ServicesListPage extends qreact.Component {
|
|
|
45
47
|
state = t.state({
|
|
46
48
|
measurementsEnabled: t.atomic<boolean>(true),
|
|
47
49
|
expandedMeasureNodes: t.lookup(t.string),
|
|
50
|
+
invalidateClickTime: t.atomic<number>(0),
|
|
48
51
|
});
|
|
49
52
|
|
|
50
53
|
render() {
|
|
@@ -123,13 +126,29 @@ export class ServicesListPage extends qreact.Component {
|
|
|
123
126
|
</button>
|
|
124
127
|
<UpdateButtons services={services.map(x => x[1]).filter(isDefined)} />
|
|
125
128
|
</div>
|
|
126
|
-
<div
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
<div className={css.hbox(8)}>
|
|
130
|
+
<div
|
|
131
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 95)}
|
|
132
|
+
onClick={() => {
|
|
133
|
+
this.state.measurementsEnabled = !this.state.measurementsEnabled;
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<span className={css.boldStyle}>{this.state.measurementsEnabled ? "☑" : "☐"} Measurements</span>
|
|
137
|
+
</div>
|
|
138
|
+
{this.state.measurementsEnabled && (() => {
|
|
139
|
+
let justInvalidated = now < this.state.invalidateClickTime + INVALIDATE_FLASH_TIME;
|
|
140
|
+
return <div
|
|
141
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20)
|
|
142
|
+
+ (justInvalidated && css.hsl(120, 50, 80) || css.hsl(0, 0, 95))
|
|
143
|
+
}
|
|
144
|
+
onClick={() => {
|
|
145
|
+
this.state.invalidateClickTime = Date.now();
|
|
146
|
+
ServiceMeasureController.resetAll();
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{justInvalidated ? "Invalidated ✓" : "Invalidate"}
|
|
150
|
+
</div>;
|
|
151
|
+
})()}
|
|
133
152
|
</div>
|
|
134
153
|
<Tools />
|
|
135
154
|
<div className={css.vbox(8)}>
|