querysub 0.604.0 → 0.605.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.604.0",
3
+ "version": "0.605.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.29",
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,14 +1,15 @@
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>;
8
- set(key: string, value: T): Promise<void>;
9
+ set(key: string, value: T, setConfig?: SetConfig): Promise<void>;
9
10
  delete(key: string): Promise<void>;
10
- keys(): Promise<string[]>;
11
- values(): Promise<T[]>;
11
+ keys(findConfig?: FindConfig): Promise<string[]>;
12
+ values(findConfig?: FindConfig): Promise<T[]>;
12
13
  entries(): Promise<[string, T][]>;
13
14
  [Symbol.asyncIterator](): AsyncIterator<[string, T]>;
14
15
  };
@@ -27,21 +28,21 @@ export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
27
28
  if (!buffer) return undefined;
28
29
  return JSON.parse(buffer.toString()) as T;
29
30
  }
30
- async function set(key: string, value: T) {
31
+ async function set(key: string, value: T, setConfig?: SetConfig) {
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)), setConfig);
34
35
  }
35
36
  async function deleteFnc(key: string) {
36
37
  let a = archives();
37
38
  console.log(`In archiveJSONT ${a.getDebugName()}, deleting ${key}`);
38
39
  await a.del(key);
39
40
  }
40
- async function keys() {
41
- return (await archives().find("")).map(value => value.toString());
41
+ async function keys(findConfig?: FindConfig) {
42
+ return (await archives().find("", findConfig)).map(value => value.toString());
42
43
  }
43
- async function values() {
44
- let infos = await archives().findInfo("");
44
+ async function values(findConfig?: FindConfig) {
45
+ let infos = await archives().findInfo("", findConfig);
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("", findConfig);
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("", findConfig);
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
- // NOTE: I think we wanna not use fallbacks, Because we really want this to be atomic and safe.
81
- return getArchives(key).get(key, { noFallbacks: true });
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
- this.perf.start();
794
- let obj = await this.getFilesBase();
795
- logNodeStateStats(`ArchiveLock Data File`, formatNumber, obj.dataFiles.length);
796
- this.perf.logAndFlush(`getFiles (${this.storage.debugKey})`);
797
- return obj.dataFiles;
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<{
@@ -276,7 +276,7 @@ export class MachineServiceControllerBase {
276
276
 
277
277
  public async getMachineList() {
278
278
  return [...new Set([
279
- ...(await machineInfos.keys()),
279
+ ...(await machineInfos.keys({ fallbacks: true })),
280
280
  ])];
281
281
  }
282
282
  public async deleteMachineIds(machineIds: string[]) {
@@ -303,7 +303,7 @@ export class MachineServiceControllerBase {
303
303
  }
304
304
  public async getServiceList(): Promise<string[]> {
305
305
  return [...new Set([
306
- ...(await serviceConfigs.keys()),
306
+ ...(await serviceConfigs.keys({ fallbacks: true })),
307
307
  ])];
308
308
  }
309
309
  public async getServiceConfig(serviceId: string): Promise<ServiceConfig | undefined> {
@@ -312,7 +312,7 @@ export class MachineServiceControllerBase {
312
312
  }
313
313
 
314
314
  public async getMachineConfigList() {
315
- return await machineConfigs.values();
315
+ return await machineConfigs.values({ fallbacks: true });
316
316
  }
317
317
  public async getMachineConfig(machineId: string): Promise<MachineConfig | undefined> {
318
318
  return await machineConfigs.get(machineId);
@@ -378,7 +378,7 @@ export class MachineServiceControllerBase {
378
378
  // NOTE: This is correct. This is obviously correct. For some reason, the AI wrote some really big, complicated thing here. I don't know why. This is the only way it should be done, obviously.
379
379
  config.oldParameters = getLiveServiceParameters(serviceConfig);
380
380
 
381
- await serviceConfigs.set(serviceId, config);
381
+ await serviceConfigs.set(serviceId, config, { fallbacks: true });
382
382
  // Only notify we were or are deployed. If it's not deployed, this will be ignored anyways.
383
383
  if (config.parameters.deploy || serviceConfig.parameters.deploy) {
384
384
  for (let machineId of getConfigMachineIds(config)) {