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.
Files changed (44) hide show
  1. package/.claude/settings.local.json +5 -1
  2. package/bin/stop-machine.js +4 -0
  3. package/package.json +6 -3
  4. package/spec.txt +12 -0
  5. package/src/-a-archives/archiveCache.ts +28 -7
  6. package/src/-a-archives/archiveCache2.ts +4 -1
  7. package/src/-a-archives/archives.ts +2 -63
  8. package/src/-a-archives/archives2.ts +21 -17
  9. package/src/-a-archives/archivesDisk.ts +2 -13
  10. package/src/-a-archives/archivesMemoryCache.ts +27 -17
  11. package/src/-a-archives/archivesMemoryCache2.ts +23 -0
  12. package/src/-d-trust/NetworkTrust2.ts +3 -3
  13. package/src/-e-certs/certAuthority.ts +2 -2
  14. package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
  15. package/src/0-path-value-core/AuthorityLookup.ts +0 -4
  16. package/src/0-path-value-core/PathRouter.ts +17 -16
  17. package/src/0-path-value-core/ShardPrefixes.ts +2 -4
  18. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +133 -38
  19. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -1
  20. package/src/0-path-value-core/pathValueArchives.ts +9 -5
  21. package/src/2-proxy/archiveMoveHarness.ts +1 -1
  22. package/src/4-deploy/edgeBootstrap.ts +50 -11
  23. package/src/4-deploy/edgeNodes.ts +7 -13
  24. package/src/4-querysub/Querysub.ts +2 -2
  25. package/src/archiveapps/archiveJoinEntry.ts +1 -1
  26. package/src/config2.ts +1 -7
  27. package/src/deployManager/components/ServicesListPage.tsx +27 -8
  28. package/src/deployManager/machineDaemonShared.ts +2 -0
  29. package/src/deployManager/machineSchema.ts +5 -6
  30. package/src/deployManager/setupMachineMain.ts +1 -2
  31. package/src/deployManager/stopMachineMain.ts +45 -0
  32. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +34 -21
  33. package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +3 -3
  34. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +11 -2
  35. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +3 -4
  36. package/src/diagnostics/logs/errorTickets/tickets.ts +3 -4
  37. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +2 -4
  38. package/src/storageSetup.ts +1 -1
  39. package/src/-a-archives/archivesBackBlaze.ts +0 -156
  40. package/src/-a-archives/archivesCborT.ts +0 -52
  41. package/src/-a-archives/archivesLimitedCache.ts +0 -307
  42. package/src/-a-archives/archivesPrivateFileSystem.ts +0 -326
  43. package/src/-a-archives/copyLocalToBackblaze.ts +0 -24
  44. 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
- let files: FileInfo[] = [...filesRawValues, ...filesRawLocks].map(x => ({
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
- readResults.set(file.file, value);
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
- await this.storage.setValue(key, op.value || Buffer.from(""));
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
- for (let file of files) {
498
- if (!file.file.endsWith(".transaction")) continue;
499
- let tFile = file;
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) return false;
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) continue;
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
- if (mustEqualSet.size !== newLockedFiles.size) return false;
624
- if (Array.from(mustEqualSet).some(a => !newLockedFiles.has(a))) return false;
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
- return transaction.ops.every(a => {
629
- const type = a.type;
630
- // Create files might disappear if we hang and they get cleaned up
631
- if (type === "create") return existingRawFiles.has(a.key);
632
- if (type === "delete") return existingFiles.has(a.key);
633
- let unhandled: never = type;
634
- throw new Error(`Unhandled type: ${unhandled}`);
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.info(`Applying transaction with ${createCount} file creates and ${deleteCount} file deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
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 (${getOwnNodeId()})`, opsRemaining.length);
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.info(`Applied transaction with ${createCount} file creates and file ${deleteCount} deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
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.info(`Starting transaction with ${creates} file creates and ${dels} file deletes, ${formatNumber(createBytes)}B`, {
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.info(`Finished transaction with rejection, ${transaction.ops.length} ops`);
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.info(`Finished transaction with ${transaction.ops.length} ops`);
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 (!data) continue;
319
- readCache.set(dataPath, data);
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 (url: string): Promise<EdgeNodesIndex | undefined> => {
21
+ let getCachedConfig = cache(async (urlsKey: string): Promise<EdgeNodesIndex | undefined> => {
22
22
  setTimeout(() => {
23
- getCachedConfig.clear(url);
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
- try {
29
- let response = await fetch(url);
30
- return await response.json();
31
- } catch {
32
- return undefined;
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
- edgeNodeConfigURL: string;
39
+ edgeNodeConfigURLs: string[];
38
40
  }): Promise<string> {
39
41
  return await measureBlock(async function getEdgeBootstrapScript() {
40
- let cachedConfig = await getCachedConfig(config.edgeNodeConfigURL);
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
- edgeNodeConfigURL: string;
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 (await fetch(config.edgeNodeConfigURL)).json() as EdgeNodesIndex;
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 { getArchivesBackblazePublic } from "../-a-archives/archivesBackBlaze";
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() && nestArchives("edgenodes/", getArchivesBackblazePublic(getDomain()));
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
- export const getEdgeNodeConfigURL = lazy(async () => {
269
- let { getURL } = await hostArchives({
270
- archives: edgeNodeStorage,
271
- subdomain: `edge`,
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 getEdgeNodeConfigURL();
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 { getEdgeNodeConfigURL } = await import("../4-deploy/edgeNodes");
828
+ const { getEdgeNodeConfigURLs } = await import("../4-deploy/edgeNodes");
829
829
  let edgeBootstrapFile = await getEdgeBootstrapScript({
830
- edgeNodeConfigURL: await getEdgeNodeConfigURL(),
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 { deepCloneJSON, isNode } from "socket-function/src/misc";
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
- className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 95).alignSelf("flex-start")}
128
- onClick={() => {
129
- this.state.measurementsEnabled = !this.state.measurementsEnabled;
130
- }}
131
- >
132
- <span className={css.boldStyle}>{this.state.measurementsEnabled ? "☑" : "☐"} Measurements</span>
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)}>
@@ -0,0 +1,2 @@
1
+ export const SERVICE_NAME = "machine-alwaysup";
2
+ export const SERVICE_UNIT_NAME = `${SERVICE_NAME}.service`;