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.
- package/bin/audit-disk-values.js +7 -0
- package/bin/deploy-prefixes.js +7 -0
- package/package.json +5 -3
- package/src/-a-archives/archiveCache.ts +12 -9
- package/src/-a-auth/certs.ts +1 -1
- package/src/-c-identity/IdentityController.ts +9 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +63 -10
- package/src/0-path-value-core/AuthorityLookup.ts +14 -4
- package/src/0-path-value-core/PathRouter.ts +247 -117
- package/src/0-path-value-core/PathRouterRouteOverride.ts +1 -1
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +4 -2
- package/src/0-path-value-core/PathValueCommitter.ts +68 -31
- package/src/0-path-value-core/PathValueController.ts +77 -8
- package/src/0-path-value-core/PathWatcher.ts +46 -4
- package/src/0-path-value-core/ShardPrefixes.ts +6 -0
- package/src/0-path-value-core/ValidStateComputer.ts +20 -8
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +18 -55
- package/src/0-path-value-core/pathValueArchives.ts +19 -8
- package/src/0-path-value-core/pathValueCore.ts +75 -27
- package/src/0-path-value-core/startupAuthority.ts +9 -9
- package/src/1-path-client/RemoteWatcher.ts +217 -178
- package/src/1-path-client/pathValueClientWatcher.ts +6 -11
- package/src/2-proxy/pathValueProxy.ts +2 -3
- package/src/3-path-functions/PathFunctionRunner.ts +3 -1
- package/src/3-path-functions/syncSchema.ts +6 -2
- package/src/4-deploy/deployGetFunctionsInner.ts +1 -1
- package/src/4-deploy/deployPrefixes.ts +14 -0
- package/src/4-deploy/edgeNodes.ts +1 -1
- package/src/4-querysub/Querysub.ts +17 -5
- package/src/4-querysub/QuerysubController.ts +21 -10
- package/src/4-querysub/predictionQueue.tsx +3 -0
- package/src/4-querysub/querysubPrediction.ts +27 -20
- package/src/5-diagnostics/nodeMetadata.ts +17 -0
- package/src/diagnostics/NodeConnectionsPage.tsx +167 -0
- package/src/diagnostics/NodeViewer.tsx +11 -15
- package/src/diagnostics/PathDistributionInfo.tsx +102 -0
- package/src/diagnostics/SyncTestPage.tsx +19 -8
- package/src/diagnostics/auditDiskValues.ts +221 -0
- package/src/diagnostics/auditDiskValuesEntry.ts +43 -0
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
- package/src/diagnostics/logs/TimeRangeSelector.tsx +3 -3
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +2 -0
- package/src/diagnostics/managementPages.tsx +10 -1
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +3 -2
- package/src/diagnostics/pathAuditer.ts +21 -0
- package/src/path.ts +9 -2
- package/src/rangeMath.ts +41 -0
- package/tempnotes.txt +5 -58
- package/test.ts +13 -295
- package/src/diagnostics/benchmark.ts +0 -139
- package/src/diagnostics/runSaturationTest.ts +0 -416
- package/src/diagnostics/satSchema.ts +0 -64
- package/src/test/mongoSatTest.tsx +0 -55
- package/src/test/satTest.ts +0 -193
- 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
|
|
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
|
-
|
|
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))
|
|
320
|
-
|
|
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 (
|
|
341
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
178
|
+
if (!conflictingValues) conflictingValues = [];
|
|
179
|
+
conflictingValues.push(value);
|
|
176
180
|
}
|
|
177
181
|
}
|
|
178
|
-
return
|
|
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,
|
|
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(
|
|
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:
|
|
294
|
-
|
|
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
|
}
|