querysub 0.406.0 → 0.408.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 (55) hide show
  1. package/bin/audit-disk-values.js +7 -0
  2. package/bin/deploy-prefixes.js +7 -0
  3. package/package.json +5 -3
  4. package/src/-a-archives/archiveCache.ts +12 -9
  5. package/src/-a-auth/certs.ts +1 -1
  6. package/src/-c-identity/IdentityController.ts +9 -1
  7. package/src/-f-node-discovery/NodeDiscovery.ts +63 -10
  8. package/src/0-path-value-core/AuthorityLookup.ts +14 -4
  9. package/src/0-path-value-core/PathRouter.ts +247 -117
  10. package/src/0-path-value-core/PathRouterRouteOverride.ts +1 -1
  11. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +4 -2
  12. package/src/0-path-value-core/PathValueCommitter.ts +68 -31
  13. package/src/0-path-value-core/PathValueController.ts +77 -8
  14. package/src/0-path-value-core/PathWatcher.ts +46 -4
  15. package/src/0-path-value-core/ShardPrefixes.ts +6 -0
  16. package/src/0-path-value-core/ValidStateComputer.ts +20 -8
  17. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +18 -55
  18. package/src/0-path-value-core/pathValueArchives.ts +19 -8
  19. package/src/0-path-value-core/pathValueCore.ts +75 -27
  20. package/src/0-path-value-core/startupAuthority.ts +9 -9
  21. package/src/1-path-client/RemoteWatcher.ts +217 -178
  22. package/src/1-path-client/pathValueClientWatcher.ts +6 -11
  23. package/src/2-proxy/pathValueProxy.ts +2 -3
  24. package/src/3-path-functions/PathFunctionRunner.ts +3 -1
  25. package/src/3-path-functions/syncSchema.ts +6 -2
  26. package/src/4-deploy/deployGetFunctionsInner.ts +1 -1
  27. package/src/4-deploy/deployPrefixes.ts +14 -0
  28. package/src/4-deploy/edgeNodes.ts +1 -1
  29. package/src/4-querysub/Querysub.ts +17 -5
  30. package/src/4-querysub/QuerysubController.ts +21 -10
  31. package/src/4-querysub/predictionQueue.tsx +3 -0
  32. package/src/4-querysub/querysubPrediction.ts +27 -20
  33. package/src/5-diagnostics/nodeMetadata.ts +17 -0
  34. package/src/diagnostics/NodeConnectionsPage.tsx +167 -0
  35. package/src/diagnostics/NodeViewer.tsx +11 -15
  36. package/src/diagnostics/PathDistributionInfo.tsx +102 -0
  37. package/src/diagnostics/SyncTestPage.tsx +19 -8
  38. package/src/diagnostics/auditDiskValues.ts +221 -0
  39. package/src/diagnostics/auditDiskValuesEntry.ts +43 -0
  40. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
  41. package/src/diagnostics/logs/TimeRangeSelector.tsx +3 -3
  42. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +2 -0
  43. package/src/diagnostics/managementPages.tsx +10 -1
  44. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +3 -2
  45. package/src/diagnostics/pathAuditer.ts +21 -0
  46. package/src/path.ts +9 -2
  47. package/src/rangeMath.ts +41 -0
  48. package/tempnotes.txt +5 -58
  49. package/test.ts +13 -295
  50. package/src/diagnostics/benchmark.ts +0 -139
  51. package/src/diagnostics/runSaturationTest.ts +0 -416
  52. package/src/diagnostics/satSchema.ts +0 -64
  53. package/src/test/mongoSatTest.tsx +0 -55
  54. package/src/test/satTest.ts +0 -193
  55. package/src/test/test.tsx +0 -552
@@ -1,12 +1,12 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { delay, batchFunction } from "socket-function/src/batching";
3
- import { isNode, timeInSecond } from "socket-function/src/misc";
3
+ import { deepCloneJSON, isNode, timeInSecond } from "socket-function/src/misc";
4
4
  import { measureBlock, measureFnc } from "socket-function/src/profiling/measure";
5
5
  import { isTrustedByNode } from "../-d-trust/NetworkTrust2";
6
6
  import { areNodeIdsEqual, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
7
7
  import { ActionsHistory } from "../diagnostics/ActionsHistory";
8
8
  import { errorToUndefined, logErrors, timeoutToUndefined } from "../errors";
9
- import { getPathFromStr } from "../path";
9
+ import { getPathFromStr, hack_stripPackedPath } from "../path";
10
10
  import { PathValueControllerBase } from "./PathValueController";
11
11
  import { PathRouter } from "./PathRouter";
12
12
  import { PathValue, MAX_ACCEPTED_CHANGE_AGE, WriteState, debugPathValuePath, compareTime, epochTime } from "./pathValueCore";
@@ -16,6 +16,11 @@ import { red } from "socket-function/src/formatting/logColors";
16
16
  import { isClient } from "../config2";
17
17
  import { auditLog, isDebugLogEnabled } from "./auditLogs";
18
18
  import { authorityLookup } from "./AuthorityLookup";
19
+ import { debugNodeId } from "../-c-identity/IdentityController";
20
+ import { decodeNodeId } from "../-a-auth/certs";
21
+ import { decodeParentFilter, encodeParentFilter } from "./hackedPackedPathParentFiltering";
22
+ import { deepCloneCborx } from "../misc/cloneHelpers";
23
+ import { removeRange } from "../rangeMath";
19
24
  setImmediate(() => import("../1-path-client/RemoteWatcher"));
20
25
  setImmediate(() => import("../4-querysub/Querysub"));
21
26
 
@@ -32,10 +37,7 @@ export type BatchValues = {
32
37
  export type RemoteValueAndValidState = {
33
38
  sourceNodeId: string;
34
39
  pathValues: PathValue[];
35
- validStates: WriteState[];
36
40
  initialTriggers: { values: Set<string>; parentPaths: Set<string> };
37
- // Means it's from an authority path sync
38
- authoritySyncPaths?: Set<string>;
39
41
  };
40
42
 
41
43
  class PathValueCommitter {
@@ -175,6 +177,7 @@ class PathValueCommitter {
175
177
  auditLog("CREATE VALUE", {
176
178
  path: pathValue.path,
177
179
  timeId: pathValue.time.time,
180
+ timeIdFull: pathValue.time,
178
181
  source: pathValue.source,
179
182
  transparent: pathValue.isTransparent,
180
183
  // value: valueStr,
@@ -194,7 +197,6 @@ class PathValueCommitter {
194
197
  parentSyncs: [],
195
198
  initialTriggers: { values: new Set(), parentPaths: new Set() },
196
199
  });
197
- PathRouter.getAllAuthorities(pathValue.path);
198
200
  console.error(`There are no authorities for path ${pathValue.path}. The write will be lost.`, {
199
201
  path: pathValue.path,
200
202
  timeId: pathValue.time.time,
@@ -215,6 +217,7 @@ class PathValueCommitter {
215
217
  // Don't send to bad nodes for 60 seconds
216
218
  const nodeIgnoreTime = Date.now() - 1000 * 60;
217
219
  let promises = Array.from(valuesPerOtherAuthority.entries()).map(async ([otherAuthority, values]) => {
220
+
218
221
  let disconnected = SocketFunction.getLastDisconnectTime(otherAuthority);
219
222
  if (disconnected && disconnected > nodeIgnoreTime) {
220
223
  // If it disconnected recently... don't send to it for a little bit, so we don't spend
@@ -240,7 +243,7 @@ class PathValueCommitter {
240
243
  void forwardPromise.then(x => {
241
244
  if (x === "refused") {
242
245
  values = values.map(value => {
243
- console.info(`Rejecting past value due to initial sync: ${debugPathValuePath(value)}`, {
246
+ console.info(`Rejecting value that was refused: ${debugPathValuePath(value)}`, {
244
247
  path: value.path,
245
248
  timeId: value.time.time,
246
249
  });
@@ -299,16 +302,35 @@ class PathValueCommitter {
299
302
  if (!isClient()) {
300
303
  measureBlock(function ignoreUnrequestedValues() {
301
304
  for (let batch of batched) {
302
- function isWrongAuthority(path: string) {
305
+ function isWrongAuthority(path: string, value?: PathValue, type?: string) {
303
306
  let watchingAuthorityId = remoteWatcher.getExistingWatchRemoteNodeId(path);
304
307
  // If we AREN'T watching it... it's actually fine, we can receive any values.
305
308
  // When we start watching, those values will get clobbered.
306
309
  if (watchingAuthorityId === undefined) return false;
307
- return !areNodeIdsEqual(watchingAuthorityId, batch.sourceNodeId);
310
+ if (!areNodeIdsEqual(watchingAuthorityId, batch.sourceNodeId)) {
311
+ let valueWatchNode = remoteWatcher.getValueWatchRemoteNodeId(path);
312
+ if (!valueWatchNode || !areNodeIdsEqual(valueWatchNode, batch.sourceNodeId)) {
313
+ let candidates = PathRouter.getAllAuthorities(path);
314
+ require("debugbreak")(2);
315
+ debugger;
316
+ remoteWatcher.getExistingWatchRemoteNodeId(path);
317
+ console.warn(`Ignoring value from wrong authority. Should have been ${debugNodeId(watchingAuthorityId)}, but was received from ${debugNodeId(batch.sourceNodeId)}.`, {
318
+ path,
319
+ type,
320
+ timeId: value?.time.time,
321
+ source: value?.source,
322
+ sourceNodeId: debugNodeId(batch.sourceNodeId),
323
+ sourceNodeThreadId: decodeNodeId(batch.sourceNodeId)?.threadId,
324
+ watchingAuthorityId: debugNodeId(watchingAuthorityId),
325
+ watchingAuthorityNodeThreadId: decodeNodeId(watchingAuthorityId)?.threadId,
326
+ isTransparent: value?.isTransparent,
327
+ });
328
+ }
329
+ return true;
330
+ }
331
+ return false;
308
332
  }
309
333
  batch.pathValues = batch.pathValues.filter(value => {
310
- // Authorities watch other authorities using path watches, And so one path can come from many authorities, so we just have to accept it no matter what, if it's from that type of source.
311
- if (batch.authoritySyncPaths?.has(value.path)) return true;
312
334
  // NOTE: See the definition for lock count for why this check isn't checking all the possible cases. Essentially, locks is often empty, and that's intentional. However, the reverse should never be true, locks should never have values when lockCount is 0.
313
335
  if (value.lockCount === 0 && value.locks.length > 0) {
314
336
  console.error(red(`Ignoring value with invalid lockCount. Was ${value.lockCount}, but we have ${value.locks.length} locks. locks are optional, but lockCount isn't. We should never have locks without having lockCount set. ${debugPathValuePath(value)}`));
@@ -316,30 +338,33 @@ class PathValueCommitter {
316
338
  }
317
339
 
318
340
  if (PathRouter.isSelfAuthority(value.path)) return true;
319
- if (isWrongAuthority(value.path)) return false;
320
- // epochTimes are just indicators that the value has no value, and so are safe to sync
321
- // from any source. They won't cause future conflicts, because any other value overrides them.
322
- // - This might not be required?
323
- if (compareTime(value.time, epochTime) === 0) {
324
- return true;
341
+ if (isWrongAuthority(value.path, value, "value")) {
342
+ return false;
325
343
  }
326
-
327
- // Also warn, because... if we get a lot of these, there might be a bug.
328
- // A few when we change watches is possible, but it should be rare.
329
- let watchingAuthorityId = remoteWatcher.getExistingWatchRemoteNodeId(value.path);
330
- auditLog("IGNORING VALUE FROM DIFFERENT AUTHORITY", { path: value.path, watchingAuthorityId, receivedFromAuthority: batch.sourceNodeId });
331
- return false;
344
+ return true;
332
345
  });
333
346
 
334
- for (let value of batch.initialTriggers.values) {
335
- if (isWrongAuthority(value)) {
347
+ for (let value of Array.from(batch.initialTriggers.values)) {
348
+ if (isWrongAuthority(value, undefined, "initialTrigger")) {
336
349
  batch.initialTriggers.values.delete(value);
337
350
  }
338
351
  }
339
- for (let parentPath of batch.initialTriggers.parentPaths) {
340
- if (isWrongAuthority(parentPath)) {
341
- batch.initialTriggers.parentPaths.delete(parentPath);
352
+ for (let parentPath of Array.from(batch.initialTriggers.parentPaths)) {
353
+ if (remoteWatcher.isFinalRemoteWatchPath({ parentPath, nodeId: batch.sourceNodeId })) {
354
+ continue;
342
355
  }
356
+
357
+ // TODO: Remove this breakpoint eventually. This can happen naturally when servers go down?
358
+ require("debugbreak")(2);
359
+ debugger;
360
+ remoteWatcher.isFinalRemoteWatchPath({ parentPath, nodeId: batch.sourceNodeId });
361
+
362
+ console.warn(`Ignoring parent path which we aren't watching. From ${debugNodeId(batch.sourceNodeId)}.`, {
363
+ parentPath,
364
+ sourceNodeId: debugNodeId(batch.sourceNodeId),
365
+ sourceNodeThreadId: decodeNodeId(batch.sourceNodeId)?.threadId,
366
+ });
367
+ batch.initialTriggers.parentPaths.delete(parentPath);
343
368
  }
344
369
  }
345
370
  });
@@ -347,7 +372,7 @@ class PathValueCommitter {
347
372
 
348
373
 
349
374
  // path => sourceNodeId
350
- let parentSyncs = new Map<string, string>();
375
+ let parentSyncs = new Map<string, Set<string>>();
351
376
 
352
377
  // We need to do a bit of work to properly clear path values that are from old initial triggers. As if we receive two initial triggers, they need to clobber each other. And if we collapse it, we lose that information. So we have to do that here.
353
378
  let finalResults = new Map<string, {
@@ -380,16 +405,28 @@ class PathValueCommitter {
380
405
  results.pathValues.push(pathValue);
381
406
  }
382
407
  for (let parentPath of batch.initialTriggers.parentPaths) {
383
- parentSyncs.set(parentPath, batch.sourceNodeId);
408
+ let sourceNodeIds = parentSyncs.get(parentPath);
409
+ if (!sourceNodeIds) {
410
+ sourceNodeIds = new Set();
411
+ parentSyncs.set(parentPath, sourceNodeIds);
412
+ }
413
+ sourceNodeIds.add(batch.sourceNodeId);
384
414
  }
385
415
  }
386
416
 
387
417
  let parentPaths = new Set(parentSyncs.keys());
388
418
  let initialValues = new Set(Array.from(finalResults.values()).filter(x => x.initialTrigger).map(x => x.path));
389
419
 
420
+ let parentSyncsList: { parentPath: string; sourceNodeId: string }[] = [];
421
+ for (let [parentPath, sourceNodeIds] of parentSyncs.entries()) {
422
+ for (let sourceNodeId of sourceNodeIds) {
423
+ parentSyncsList.push({ parentPath, sourceNodeId });
424
+ }
425
+ }
426
+
390
427
  validStateComputer.ingestValuesAndValidStates({
391
428
  pathValues: Array.from(finalResults.values()).map(x => x.pathValues).flat(),
392
- parentSyncs: Array.from(parentSyncs.entries()).map(([parentPath, sourceNodeId]) => ({ parentPath, sourceNodeId })),
429
+ parentSyncs: parentSyncsList,
393
430
  initialTriggers: { values: initialValues, parentPaths: parentPaths },
394
431
  });
395
432
  },
@@ -7,7 +7,6 @@ import { ActionsHistory } from "../diagnostics/ActionsHistory";
7
7
  import { isNode, nextId, timeInSecond, timeoutToUndefined } from "socket-function/src/misc";
8
8
  import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
9
9
  import { pathValueCommitter } from "./PathValueCommitter";
10
- import { Benchmark } from "../diagnostics/benchmark";
11
10
  import { auditLog, isDebugLogEnabled } from "./auditLogs";
12
11
  import { debugNodeId, debugNodeThread } from "../-c-identity/IdentityController";
13
12
  import { isDiskAudit } from "../config";
@@ -17,8 +16,18 @@ import { getNodeIdIP } from "socket-function/src/nodeCache";
17
16
  import { authorityLookup } from "./AuthorityLookup";
18
17
  import { timeoutToError } from "../errors";
19
18
  import { AuthoritySpec, PathRouter } from "./PathRouter";
19
+ import { LZ4 } from "../storage/LZ4";
20
+ import { Time } from "./pathValueCore";
21
+ import { encodeCborx, decodeCborx } from "../misc/cloneHelpers";
22
+ import { debugGetAllCallFactories } from "socket-function/src/nodeCache";
20
23
  export { pathValueCommitter };
21
24
 
25
+ // ONLY returns non-canGCValues that are valid
26
+ export type AuditSnapshotEntry = {
27
+ path: string;
28
+ time: Time;
29
+ };
30
+
22
31
  // The sync test page has some functions which related to PathValueController significantly, and should always be exposed if PathValueController is exposed.
23
32
  if (isNode()) {
24
33
  setImmediate(() => {
@@ -43,6 +52,7 @@ export class PathValueControllerBase {
43
52
  auditLog("SEND CREATED VALUE", {
44
53
  path: value.path,
45
54
  timeId: value.time.time,
55
+ timeIdVersion: value.time.version,
46
56
  targetNodeThreadId: debugNodeThread(nodeId),
47
57
  transparent: value.isTransparent,
48
58
  source: value.source,
@@ -66,6 +76,7 @@ export class PathValueControllerBase {
66
76
  auditLog("SEND VALUE", {
67
77
  path: pathValue.path,
68
78
  timeId: pathValue.time.time,
79
+ timeIdVersion: pathValue.time.version,
69
80
  watcher: nodeId,
70
81
  isValid: pathValue.valid,
71
82
  nodeId: debugNodeId(nodeId),
@@ -119,7 +130,6 @@ export class PathValueControllerBase {
119
130
  let values: PathValue[] = [];
120
131
  if (valueBuffers) {
121
132
  values = await pathValueSerializer.deserialize(valueBuffers);
122
- Benchmark.onReceivedValues(values);
123
133
  ActionsHistory.OnRead(values);
124
134
  }
125
135
 
@@ -152,6 +162,7 @@ export class PathValueControllerBase {
152
162
  path: value.path,
153
163
  // NOTE: Yes, this might conflict as we're throwing away a lot of information, however in practice no, it's not going to conflict when we're debugging, and this + path will be globally unique for every value we investigate.
154
164
  timeId: value.time.time,
165
+ timeIdVersion: value.time.version,
155
166
  sourceNodeId,
156
167
  sourceNodeThreadId: decodeNodeId(sourceNodeId)?.threadId,
157
168
  });
@@ -159,13 +170,11 @@ export class PathValueControllerBase {
159
170
  }
160
171
 
161
172
  try {
162
- let parentSyncs = Array.from(config.initialTriggers?.parentPaths || [])
163
- .map(x => ({ parentPath: x, sourceNodeId: callerId }));
164
173
  let initialTriggers = config.initialTriggers || { values: new Set(), parentPaths: new Set() };
165
- validStateComputer.ingestValuesAndValidStates({
174
+ await pathValueCommitter.ingestRemoteValuesAndValidStates({
166
175
  pathValues: values,
167
- parentSyncs,
168
176
  initialTriggers,
177
+ sourceNodeId: callerId,
169
178
  });
170
179
  } catch (error) {
171
180
  console.error("Error ingesting values and valid states", error);
@@ -254,6 +263,65 @@ export class PathValueControllerBase {
254
263
  });
255
264
  return buffers;
256
265
  }
266
+
267
+ public static async getAuditSnapshot(config: {
268
+ nodeId: string;
269
+ spec: AuthoritySpec;
270
+ }): Promise<AuditSnapshotEntry[]> {
271
+ let compressed = await PathValueController.nodes[config.nodeId].getAuditSnapshot(config);
272
+ let decompressed = LZ4.decompress(compressed);
273
+ return decodeCborx(decompressed);
274
+ }
275
+
276
+ public async getAuditSnapshot(config: {
277
+ spec: AuthoritySpec;
278
+ }): Promise<Buffer> {
279
+ let { spec } = config;
280
+ let { compareTime } = await import("./pathValueCore");
281
+
282
+ let allValues = authorityStorage.getAllValues({ spec, startTime: 0, endTime: Number.MAX_SAFE_INTEGER });
283
+
284
+ let pathToLatest = new Map<string, Time>();
285
+ for (let value of allValues) {
286
+ if (!value.valid) continue;
287
+ if (value.canGCValue) continue;
288
+
289
+ let existing = pathToLatest.get(value.path);
290
+ if (!existing || compareTime(value.time, existing) > 0) {
291
+ pathToLatest.set(value.path, value.time);
292
+ }
293
+ }
294
+
295
+ let entries: AuditSnapshotEntry[] = Array.from(pathToLatest.entries()).map(([path, time]) => ({ path, time }));
296
+
297
+ let encoded = encodeCborx(entries);
298
+ return LZ4.compress(encoded);
299
+ }
300
+
301
+
302
+ public static async getValuesByPathAndTime(config: {
303
+ nodeId: string;
304
+ entries: AuditSnapshotEntry[];
305
+ }): Promise<PathValue[]> {
306
+ let buffers = await PathValueController.nodes[config.nodeId].getValuesByPathAndTime(config.entries);
307
+ return await pathValueSerializer.deserialize(buffers);
308
+ }
309
+
310
+ public async getValuesByPathAndTime(entries: AuditSnapshotEntry[]): Promise<Buffer[]> {
311
+ let values: PathValue[] = [];
312
+ for (let entry of entries) {
313
+ let value = authorityStorage.getValueExactIgnoreInvalid(entry.path, entry.time);
314
+ if (!value) continue;
315
+ if (value.isTransparent) continue;
316
+ if (value.canGCValue) continue;
317
+ values.push(value);
318
+ }
319
+ let buffers = await pathValueSerializer.serialize(values, {
320
+ noLocks: true,
321
+ compress: getCompressNetwork(),
322
+ });
323
+ return buffers;
324
+ }
257
325
  }
258
326
 
259
327
  export const PathValueController = SocketFunction.register(
@@ -263,6 +331,8 @@ export const PathValueController = SocketFunction.register(
263
331
  sendData: {},
264
332
 
265
333
  getInitialValues: {},
334
+ getAuditSnapshot: {},
335
+ getValuesByPathAndTime: {},
266
336
 
267
337
  watchLatest: {},
268
338
  unwatchLatest: {},
@@ -273,5 +343,4 @@ export const PathValueController = SocketFunction.register(
273
343
  {
274
344
  noFunctionMeasure: !isNode(),
275
345
  }
276
- );
277
-
346
+ );
@@ -11,14 +11,44 @@ import { PathValueControllerBase } from "./PathValueController";
11
11
  import { PathRouter } from "./PathRouter";
12
12
  import { auditLog } from "./auditLogs";
13
13
  import { WatchConfig, authorityStorage, PathValue, NodeId, compareTime, isCoreQuiet, MAX_CHANGE_AGE, createMissingEpochValue } from "./pathValueCore";
14
- import { decodeParentFilter, matchesParentRangeFilter } from "./hackedPackedPathParentFiltering";
14
+ import { matchesParentRangeFilter } from "./hackedPackedPathParentFiltering";
15
+ import { isClient } from "../config2";
16
+ import { delay } from "socket-function/src/batching";
15
17
 
18
+ setImmediate(() => import("../4-querysub/Querysub"));
16
19
 
17
20
  // These are used so we can have our internal trigger lookup without watching them on remote values.
18
21
  export function getInternalValueWatchPrefix() {
19
22
  return "INTERNAL_VALUE_WATCH_PREFIX_";
20
23
  }
21
24
 
25
+ let pathWatcherState: (() => { watcherSequence: number }) | undefined;
26
+
27
+ setImmediate(async () => {
28
+ const { createLocalSchema } = await import("../4-querysub/schemaHelpers");
29
+ const { t } = await import("../2-proxy/schema2");
30
+ pathWatcherState = createLocalSchema("pathWatcherState", {
31
+ watcherSequence: t.number
32
+ });
33
+ });
34
+
35
+ let triggering = false;
36
+ function incrementWatcherSequence() {
37
+ if (!isClient()) return;
38
+ if (!pathWatcherState) return;
39
+ if (triggering) return;
40
+ triggering = true;
41
+ void delay(10).finally(async () => {
42
+ let { Querysub } = await import("../4-querysub/Querysub");
43
+ triggering = false;
44
+ Querysub.commit(() => {
45
+ if (!pathWatcherState) return;
46
+ let state = pathWatcherState();
47
+ state.watcherSequence = (state.watcherSequence || 0) + 1;
48
+ });
49
+ });
50
+ }
51
+
22
52
  // WATCH CASES
23
53
  // 1) getOwnNodeId() is used to trigger pathValueClientWatcher (no one else should be using it)
24
54
  // 2) Use other nodes to immediately proxy to the other nodes
@@ -133,6 +163,7 @@ class PathWatcher {
133
163
  }
134
164
 
135
165
  if (newPathsWatched.size > 0 || newParentsWatched.size > 0) {
166
+ incrementWatcherSequence();
136
167
  if (isOwnNodeId(config.nodeId)) {
137
168
  for (let path of newPathsWatched) {
138
169
  auditLog("new local WATCH VALUE", { path });
@@ -177,6 +208,7 @@ class PathWatcher {
177
208
 
178
209
  if (!config.noInitialTrigger) {
179
210
  this.triggerValuesChanged({
211
+ onlyTriggerNodeId: config.nodeId,
180
212
  valuesChanged: new Set(),
181
213
  initialTriggers: { values: newPathsWatched, parentPaths: newParentsWatched },
182
214
  });
@@ -215,7 +247,6 @@ class PathWatcher {
215
247
 
216
248
  if (obj.watchers.size === 0) {
217
249
  watchersObj.delete(path);
218
- this.parentWatchers.delete(path);
219
250
  fullyUnwatched.parentPaths.push(path);
220
251
  authorityStorage.markParentPathAsUnwatched(path);
221
252
 
@@ -269,6 +300,7 @@ class PathWatcher {
269
300
  }
270
301
 
271
302
  if (fullyUnwatched.paths.length > 0 || fullyUnwatched.parentPaths.length > 0) {
303
+ incrementWatcherSequence();
272
304
  console.info(`Unwatched PathValue watches`, {
273
305
  unwatchedPaths: fullyUnwatched.paths.length,
274
306
  unwatchedParents: fullyUnwatched.parentPaths.length,
@@ -312,7 +344,7 @@ class PathWatcher {
312
344
  // ALSO, These path values need to be ingested first.
313
345
  valuesChanged: Set<PathValue>;
314
346
 
315
- // We mutate initialTriggers
347
+ // Mutates initialTriggers.values
316
348
  initialTriggers: { values: Set<string>; parentPaths: Set<string> };
317
349
 
318
350
  onlyTriggerNodeId?: string;
@@ -339,14 +371,16 @@ class PathWatcher {
339
371
  for (let parentPath of initialTriggers.parentPaths) {
340
372
  let valuePaths = authorityStorage.getPathsFromParent(parentPath);
341
373
  for (let valuePath of valuePaths || []) {
374
+ // IMPORTANT! Add all the values to the initial triggers so then later we not only trigger them but know their initial trigger so we can make sure that the is initial trigger logic for each value runs as well.
375
+ initialTriggers.values.add(valuePath);
342
376
  triggerPaths.add(valuePath);
343
- config.initialTriggers.values.add(valuePath);
344
377
  }
345
378
 
346
379
  let latestParentWatches = this.parentWatchers.get(hack_stripPackedPath(parentPath));
347
380
  if (!latestParentWatches) continue;
348
381
  for (let { watchers } of latestParentWatches.values()) {
349
382
  for (let watcher of watchers) {
383
+ if (onlyTriggerNodeId && watcher !== onlyTriggerNodeId) continue;
350
384
  let changes = changedPerCallbacks.get(watcher);
351
385
  if (!changes) {
352
386
  changes = { values: new Set(), initialTriggers: { values: new Set(), parentPaths: new Set() } };
@@ -383,6 +417,7 @@ class PathWatcher {
383
417
  changes.values.add(value);
384
418
  }
385
419
  if (isInitialTrigger) {
420
+ changes.initialTriggers.values.add(path);
386
421
  let watcherObj = this.watchersToPaths.get(watcher);
387
422
  let fullHistory = watcherObj?.fullHistory;
388
423
  if (fullHistory) {
@@ -502,6 +537,13 @@ class PathWatcher {
502
537
  return Array.from(this.parentWatchers.keys());
503
538
  }
504
539
 
540
+ public getAllWatchedPaths(): string[] {
541
+ if (pathWatcherState) {
542
+ pathWatcherState().watcherSequence;
543
+ }
544
+ return Array.from(this.watchers.keys());
545
+ }
546
+
505
547
  public debug_harvestSyncTimes(): { start: number; end: number; path: string; }[] {
506
548
  let times = this.syncHistoryForHarvest.getAllUnordered();
507
549
  this.syncHistoryForHarvest.reset();
@@ -9,6 +9,7 @@
9
9
  After a PathServerValue starts, it can't change its prefixes, as if the routing changes it makes it too difficult for clients to reason about who they should be synchronizing with.
10
10
  */
11
11
 
12
+ import { isNode } from "typesafecss";
12
13
  import { nestArchives } from "../-a-archives/archives";
13
14
  import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
14
15
  import { archiveJSONT } from "../-a-archives/archivesJSONT";
@@ -23,9 +24,14 @@ let prefixes = archiveJSONT<PrefixObj>(() => nestArchives("shard-prefixes/", get
23
24
  const key = "all";
24
25
 
25
26
  export async function getShardPrefixes(): Promise<string[]> {
27
+ if (!isNode()) return [];
26
28
  return (await prefixes.get(key))?.prefixes ?? [];
27
29
  }
28
30
 
29
31
  export async function setShardPrefixes(_prefixes: string[]) {
32
+ console.log("Setting shard prefixes");
33
+ for (let prefix of _prefixes) {
34
+ console.log("Prefix:", prefix);
35
+ }
30
36
  await prefixes.set(key, { prefixes: _prefixes });
31
37
  }
@@ -5,7 +5,7 @@ import { isDiskAudit } from "../config";
5
5
  import { getPathFromStr } from "../path";
6
6
  import { auditLog } from "./auditLogs";
7
7
  import { PathRouter } from "./PathRouter";
8
- import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue } from "./pathValueCore";
8
+ import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue, MAX_CHANGE_AGE } from "./pathValueCore";
9
9
  import { pathWatcher } from "./PathWatcher";
10
10
  import { lockWatcher2 } from "./LockWatcher2";
11
11
  import { onPathInteracted } from "../diagnostics/pathAuditerCallback";
@@ -82,7 +82,9 @@ class ValidStateComputer {
82
82
  console.info(`Rejecting past value due to initial sync: ${debugPathValuePath(value)}`, {
83
83
  path: value.path,
84
84
  timeId: value.time.time,
85
+ timeIdFull: value.time,
85
86
  newTimeId: newValue?.time.time,
87
+ newTimeIdFull: newValue?.time,
86
88
  });
87
89
  ourValues.push({ ...value, valid: false, });
88
90
  }
@@ -158,10 +160,11 @@ class ValidStateComputer {
158
160
  }
159
161
  return !!value.valid;
160
162
  }
161
- private isLockContentionFree(lock: ReadLock): PathValue | undefined {
163
+ private isLockContentionFree(lock: ReadLock): PathValue[] | undefined {
162
164
  // sorted by -time (newest are first)
163
165
  let history = authorityStorage.getValuePlusHistory(lock.path);
164
166
 
167
+ let conflictingValues: PathValue[] | undefined;
165
168
  let index = binarySearchIndex(history.length, i => compareTime(lock.endTime, history[i].time));
166
169
  if (index < 0) index = ~index;
167
170
  else index++;
@@ -172,10 +175,11 @@ class ValidStateComputer {
172
175
  if (value.isTransparent && lock.readIsTransparent) continue;
173
176
  // If value.time is within our range, there's contention!
174
177
  if (compareTime(lock.startTime, value.time) < 0 && compareTime(value.time, lock.endTime) < 0) {
175
- return value;
178
+ if (!conflictingValues) conflictingValues = [];
179
+ conflictingValues.push(value);
176
180
  }
177
181
  }
178
- return undefined;
182
+ return conflictingValues;
179
183
  }
180
184
 
181
185
  @measureFnc
@@ -193,6 +197,10 @@ class ValidStateComputer {
193
197
 
194
198
  let valid = true;
195
199
  for (let lock of locks) {
200
+ let age = now - lock.endTime.time;
201
+ // If it's old enough, then it's always valid, we can't change the valid save something after its max change age, even if it's invalid.
202
+ if (age > MAX_CHANGE_AGE) continue;
203
+
196
204
  if (!this.isLockValid(lock)) {
197
205
  onPathInteracted(lock.path, 2);
198
206
  valid = false;
@@ -259,7 +267,7 @@ class ValidStateComputer {
259
267
  }
260
268
  }
261
269
  }
262
- private logLockContention(valueGroup: PathValue[], lock: ReadLock, conflictingValue: PathValue, now: number) {
270
+ private logLockContention(valueGroup: PathValue[], lock: ReadLock, conflictingValues: PathValue[], now: number) {
263
271
 
264
272
  // This special version indicates clientside prediction (which SHOULD be rejected).
265
273
  if (lock.endTime.version !== Number.MAX_SAFE_INTEGER) {
@@ -278,7 +286,7 @@ class ValidStateComputer {
278
286
  redMessage += `\n (original read from: ${debugTime(lock.startTime)}`;
279
287
  redMessage += `\n (at time: ${debugTime(lock.endTime)}`;
280
288
  redMessage += `\n (current time: ${Date.now()})`;
281
- redMessage += `\n (conflict write at: ${debugTime(conflictingValue.time)}`;
289
+ redMessage += `\n (conflict write at: ${debugTime(conflictingValues[0].time)}`;
282
290
  console.error(redMessage);
283
291
  }
284
292
  if (isDiskAudit()) {
@@ -287,11 +295,15 @@ class ValidStateComputer {
287
295
  lock,
288
296
  path: pathValue.path,
289
297
  timeId: pathValue.time.time,
298
+ timeIdVersion: pathValue.time.version,
299
+ timeIdFull: pathValue.time,
290
300
  lockedPath: lock.path,
291
301
  transparent: lock.readIsTransparent,
292
302
  missingPath: lock.path,
293
- conflictTimeId0: conflictingValue.time.time,
294
- conflictingWrites: conflictingValue,
303
+ conflictTimeId0: conflictingValues[0].time.time,
304
+ conflictTimeIdVersion0: conflictingValues[0].time.version,
305
+ conflictCount: conflictingValues.length,
306
+ conflictingWrites: conflictingValues,
295
307
  hadTimeId: lock.startTime.time,
296
308
  });
297
309
  }