querysub 0.402.0 → 0.404.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/.cursorrules +2 -0
- package/bin/audit-imports.js +4 -0
- package/package.json +7 -4
- package/spec.txt +77 -0
- package/src/-a-archives/archiveCache.ts +9 -4
- package/src/-a-archives/archivesBackBlaze.ts +1039 -1039
- package/src/-a-auth/certs.ts +0 -12
- package/src/-c-identity/IdentityController.ts +12 -3
- package/src/-f-node-discovery/NodeDiscovery.ts +32 -26
- package/src/-g-core-values/NodeCapabilities.ts +12 -2
- package/src/0-path-value-core/AuthorityLookup.ts +239 -0
- package/src/0-path-value-core/LockWatcher2.ts +150 -0
- package/src/0-path-value-core/PathRouter.ts +535 -0
- package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +65 -0
- package/src/0-path-value-core/PathValueCommitter.ts +222 -488
- package/src/0-path-value-core/PathValueController.ts +277 -239
- package/src/0-path-value-core/PathWatcher.ts +534 -0
- package/src/0-path-value-core/ShardPrefixes.ts +31 -0
- package/src/0-path-value-core/ValidStateComputer.ts +303 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +1 -1
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +80 -44
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +13 -16
- package/src/0-path-value-core/auditLogs.ts +2 -0
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +97 -0
- package/src/0-path-value-core/pathValueArchives.ts +490 -492
- package/src/0-path-value-core/pathValueCore.ts +195 -1492
- package/src/0-path-value-core/startupAuthority.ts +74 -0
- package/src/1-path-client/RemoteWatcher.ts +100 -83
- package/src/1-path-client/pathValueClientWatcher.ts +808 -815
- package/src/2-proxy/PathValueProxyWatcher.ts +10 -8
- package/src/2-proxy/archiveMoveHarness.ts +182 -214
- package/src/2-proxy/garbageCollection.ts +9 -8
- package/src/2-proxy/schema2.ts +21 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +206 -180
- package/src/3-path-functions/PathFunctionRunner.ts +943 -766
- package/src/3-path-functions/PathFunctionRunnerMain.ts +5 -3
- package/src/3-path-functions/pathFunctionLoader.ts +2 -2
- package/src/3-path-functions/syncSchema.ts +592 -521
- package/src/4-deploy/deployFunctions.ts +19 -4
- package/src/4-deploy/deployGetFunctionsInner.ts +8 -2
- package/src/4-deploy/deployMain.ts +51 -68
- package/src/4-deploy/edgeClientWatcher.tsx +1 -1
- package/src/4-deploy/edgeNodes.ts +2 -2
- package/src/4-dom/qreact.tsx +2 -4
- package/src/4-dom/qreactTest.tsx +7 -13
- package/src/4-querysub/Querysub.ts +21 -8
- package/src/4-querysub/QuerysubController.ts +45 -29
- package/src/4-querysub/permissions.ts +2 -2
- package/src/4-querysub/querysubPrediction.ts +80 -70
- package/src/4-querysub/schemaHelpers.ts +5 -1
- package/src/5-diagnostics/GenericFormat.tsx +14 -9
- package/src/archiveapps/archiveGCEntry.tsx +9 -2
- package/src/archiveapps/archiveJoinEntry.ts +87 -84
- package/src/archiveapps/archiveMergeEntry.tsx +2 -0
- package/src/bits.ts +19 -0
- package/src/config.ts +21 -3
- package/src/config2.ts +23 -48
- package/src/deployManager/components/DeployPage.tsx +7 -3
- package/src/deployManager/machineSchema.ts +4 -1
- package/src/diagnostics/ActionsHistory.ts +3 -8
- package/src/diagnostics/AuditLogPage.tsx +2 -3
- package/src/diagnostics/FunctionCallInfo.tsx +141 -0
- package/src/diagnostics/FunctionCallInfoState.ts +162 -0
- package/src/diagnostics/MachineThreadInfo.tsx +1 -1
- package/src/diagnostics/NodeViewer.tsx +37 -48
- package/src/diagnostics/SyncTestPage.tsx +241 -0
- package/src/diagnostics/auditImportViolations.ts +185 -0
- package/src/diagnostics/listenOnDebugger.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +10 -4
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +24 -22
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
- package/src/diagnostics/logs/diskLogGlobalContext.ts +1 -0
- package/src/diagnostics/logs/errorNotifications2/logWatcher.ts +1 -3
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +39 -17
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +4 -6
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleInstanceTableView.tsx +36 -5
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +19 -5
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +15 -7
- package/src/diagnostics/logs/lifeCycleAnalysis/NestedLifeCycleInfo.tsx +28 -106
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +2 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMisc.ts +0 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +18 -7
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +3 -0
- package/src/diagnostics/managementPages.tsx +10 -3
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +20 -26
- package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +6 -4
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +2 -2
- package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +7 -9
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +23 -12
- package/src/diagnostics/misc-pages/archiveViewerShared.tsx +1 -1
- package/src/diagnostics/pathAuditer.ts +486 -0
- package/src/diagnostics/pathAuditerCallback.ts +20 -0
- package/src/diagnostics/watchdog.ts +8 -1
- package/src/library-components/URLParam.ts +1 -1
- package/src/misc/hash.ts +1 -0
- package/src/path.ts +21 -7
- package/src/server.ts +54 -47
- package/src/user-implementation/loginEmail.tsx +1 -1
- package/tempnotes.txt +67 -0
- package/test.ts +288 -95
- package/src/0-path-value-core/NodePathAuthorities.ts +0 -1057
- package/src/0-path-value-core/PathController.ts +0 -1
- package/src/5-diagnostics/diskValueAudit.ts +0 -218
- package/src/5-diagnostics/memoryValueAudit.ts +0 -438
- package/src/archiveapps/lockTest.ts +0 -127
|
@@ -1,37 +1,34 @@
|
|
|
1
|
-
import debugbreak from "debugbreak";
|
|
2
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
3
|
-
import {
|
|
4
|
-
import { addEpsilons,
|
|
5
|
-
import {
|
|
6
|
-
import { appendToPathStr, getParentPathStr, getPathDepth,
|
|
7
|
-
import {
|
|
8
|
-
import { IdentityController_getOwnPubKeyShort
|
|
2
|
+
import { lazy } from "socket-function/src/caching";
|
|
3
|
+
import { addEpsilons, minusEpsilon } from "../bits";
|
|
4
|
+
import { logErrors } from "../errors";
|
|
5
|
+
import { appendToPathStr, getParentPathStr, getPathDepth, getPathIndexAssert, hack_getPackedPathSuffix, hack_setPackedPathSuffix, hack_stripPackedPath } from "../path";
|
|
6
|
+
import { measureFnc, measureWrap } from "socket-function/src/profiling/measure";
|
|
7
|
+
import { IdentityController_getOwnPubKeyShort } from "../-c-identity/IdentityController";
|
|
9
8
|
import { pathValueArchives } from "./pathValueArchives";
|
|
10
|
-
import { blue
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { markArrayAsSplitable } from "socket-function/src/fixLargeNetworkCalls";
|
|
15
|
-
import { registerDynamicResource, registerMapArrayResource, registerResource } from "../diagnostics/trackResources";
|
|
16
|
-
import { binarySearchIndex, isNode, isNodeTrue, last, promiseObj, sort, timeInHour, timeInMinute, timeInSecond, QueueLimited } from "socket-function/src/misc";
|
|
17
|
-
import { isNodeTrusted, isTrusted, isTrustedByNode } from "../-d-trust/NetworkTrust2";
|
|
18
|
-
import { AuthorityPath, LOCAL_DOMAIN, LOCAL_DOMAIN_PATH, pathValueAuthority2 } from "./NodePathAuthorities";
|
|
9
|
+
import { blue } from "socket-function/src/formatting/logColors";
|
|
10
|
+
import { delay, runInfinitePoll } from "socket-function/src/batching";
|
|
11
|
+
import { registerMapArrayResource, registerResource } from "../diagnostics/trackResources";
|
|
12
|
+
import { isNode, isNodeTrue, sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
19
13
|
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
14
|
+
import { AuthoritySpec, PathRouter } from "./PathRouter";
|
|
20
15
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
21
|
-
import {
|
|
22
|
-
import { getNodeIdDomain, getNodeIdDomainMaybeUndefined, getNodeIdIP } from "socket-function/src/nodeCache";
|
|
23
|
-
import { getMachineId } from "../-a-auth/certs";
|
|
16
|
+
import { isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
24
17
|
import { PromiseRace } from "../../src/misc/PromiseRace";
|
|
25
18
|
|
|
26
19
|
import yargs from "yargs";
|
|
27
|
-
import { sha256 } from "js-sha256";
|
|
28
20
|
import { PromiseObj } from "../promise";
|
|
29
|
-
import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
|
|
30
21
|
import { auditLog, isDebugLogEnabled } from "./auditLogs";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
import { ellipsisMiddle } from "../misc";
|
|
23
|
+
import { fastHash } from "../misc/hash";
|
|
24
|
+
import { authorityLookup } from "./AuthorityLookup";
|
|
25
|
+
import { onPathInteracted } from "../diagnostics/pathAuditerCallback";
|
|
26
|
+
import { filterChildPathsBase } from "./hackedPackedPathParentFiltering";
|
|
27
|
+
|
|
28
|
+
setImmediate(async () => {
|
|
29
|
+
// Import everything will dynamically import, so the client side can tell that it's required.
|
|
30
|
+
await import("./PathWatcher");
|
|
31
|
+
});
|
|
35
32
|
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
36
33
|
.option("noarchive", { type: "boolean", alias: ["noarchives"], desc: "Don't save writes to disk. Still reads from disk." })
|
|
37
34
|
.argv || {}
|
|
@@ -71,6 +68,7 @@ export const CLIENTSIDE_PREDICT_LEEWAY = 500;
|
|
|
71
68
|
* to write well before this time.
|
|
72
69
|
* - This has to be at least MAX_CHANGE_AGE * 4.5 + the time to serialize and
|
|
73
70
|
* send our data to remote storage.
|
|
71
|
+
* TODO: Shouldn't this be MAX_TIME_UNTIL_DISK_FLUSH, which properly takes into account the parts (and adds a buffer)
|
|
74
72
|
*/
|
|
75
73
|
export const ARCHIVE_FLUSH_LIMIT = Math.max(MAX_CHANGE_AGE * 10, timeInMinute * 60);
|
|
76
74
|
|
|
@@ -96,6 +94,17 @@ export const UNDEFINED_MEMORY_CLEANUP_DELAY = MAX_CHANGE_AGE * 2;
|
|
|
96
94
|
|
|
97
95
|
export const ARCHIVE_LOOP_DELAY = MAX_CHANGE_AGE * 2;
|
|
98
96
|
|
|
97
|
+
export const MIN_WAIT_TIME_UNTIL_DISK_FLUSH = MAX_CHANGE_AGE * 2;
|
|
98
|
+
export const RAND_ADD_TIME_UNTIL_DISK_FLUSH = MAX_CHANGE_AGE * 0.5;
|
|
99
|
+
|
|
100
|
+
export const MAX_TIME_UNTIL_DISK_FLUSH = (
|
|
101
|
+
ARCHIVE_LOOP_DELAY
|
|
102
|
+
+ MIN_WAIT_TIME_UNTIL_DISK_FLUSH
|
|
103
|
+
+ RAND_ADD_TIME_UNTIL_DISK_FLUSH
|
|
104
|
+
// Plus, add a lot of buffer time
|
|
105
|
+
+ MAX_CHANGE_AGE * 5
|
|
106
|
+
);
|
|
107
|
+
|
|
99
108
|
/** Both of these are just targets, we might exceed them. */
|
|
100
109
|
export const FILE_VALUE_COUNT_LIMIT = 50_000;
|
|
101
110
|
export const FILE_SIZE_LIMIT = 50_000_000;
|
|
@@ -143,6 +152,10 @@ export type Time = {
|
|
|
143
152
|
export const epochTime: Time = { time: 0, version: 0, creatorId: 0 };
|
|
144
153
|
Object.freeze(epochTime);
|
|
145
154
|
|
|
155
|
+
export function createMissingEpochValue(path: string): PathValue {
|
|
156
|
+
return { path, value: undefined, canGCValue: true, isTransparent: true, valid: true, time: epochTime, locks: [], lockCount: 0, event: false };
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
/** `timeMinusEpsilon(time)` < time BUT, there is never a case where
|
|
147
160
|
* `timeMinusEpsilon(time) < otherTime < time` (there is no gap between the times)
|
|
148
161
|
*/
|
|
@@ -183,13 +196,6 @@ export type ReadLock = {
|
|
|
183
196
|
// is pretty big anyways, but... eh...). HOWEVER, sending if we are reading
|
|
184
197
|
// undefined is useful, and makes garbage collection possible.
|
|
185
198
|
readIsTransparent: boolean;
|
|
186
|
-
|
|
187
|
-
/** A clientside only value to cause rejected values to be used as valid values for a bit, to fix
|
|
188
|
-
* the case where we receive the function evaluation before the replacement values (which is
|
|
189
|
-
* actually fairly common, due to how permissions work, so that existing paths are faster,
|
|
190
|
-
* but new paths are slower).
|
|
191
|
-
*/
|
|
192
|
-
keepRejectedUntil?: number;
|
|
193
199
|
};
|
|
194
200
|
// NOTE: WriteState is a pared down PathValue for ReadLocks to consume.
|
|
195
201
|
// 1) This helps keep them separate, as values you read are very different from values
|
|
@@ -222,6 +228,9 @@ export type PathValue = {
|
|
|
222
228
|
path: string;
|
|
223
229
|
/** @deprecated ALWAYS access value using pathValueSerializer.getPathValue(), unless making a shallow copy. */
|
|
224
230
|
value: Value;
|
|
231
|
+
/** Every Time+path combination will be globally unique. For a transaction all parts will have the same Time.
|
|
232
|
+
* PathValues with the same Time MUST HAVE the SAME locks arrays.
|
|
233
|
+
*/
|
|
225
234
|
time: Time;
|
|
226
235
|
/** @deprecated NOT deprecated, just remember when you set this ALWAYS set lockCount, otherwise
|
|
227
236
|
* valid checking breaks!
|
|
@@ -235,12 +244,8 @@ export type PathValue = {
|
|
|
235
244
|
*/
|
|
236
245
|
lockCount: number;
|
|
237
246
|
|
|
238
|
-
/** value
|
|
239
|
-
* - MUST be set
|
|
240
|
-
* - We only GC values if they aren't eclipsing any values (or if we also remove the values they are eclipsing
|
|
241
|
-
* at the same time, in an atomic manner).
|
|
242
|
-
* - Makes reading more efficient (so we don't have to worry about lazily reading values), and makes our
|
|
243
|
-
* intentions more clear.
|
|
247
|
+
/** This means that the value is equivalent to having no value, and so we can remove it. This is slightly different than is transparent, which means we can read through the value. Sometimes it's transparent, but it's a special flag, and so we can't GC it.
|
|
248
|
+
* - When canGCValue isTransparent MUST be set (it should be a tri-state, not 2 flags). If you're equivalent to nothing, and nothing is obviously transparent, then you must be transparent.
|
|
244
249
|
*/
|
|
245
250
|
canGCValue?: boolean;
|
|
246
251
|
|
|
@@ -269,14 +274,10 @@ export type PathValue = {
|
|
|
269
274
|
isTransparent?: boolean;
|
|
270
275
|
|
|
271
276
|
|
|
272
|
-
/**
|
|
273
|
-
* -
|
|
274
|
-
*
|
|
275
|
-
|
|
276
|
-
* how this can be done to allow arbitrary rejection (as in, locking a large time range,
|
|
277
|
-
* on a unique path, and then writing to that path when we want to cause a rejection).
|
|
278
|
-
*/
|
|
279
|
-
valid?: boolean;
|
|
277
|
+
/** This should only be set by ValidStateComputer.computeValidStates. IF you want to change the valid state of something, make a shallow copy, and pass it to pathValueCommitter.ingestValuesAndValidStates()
|
|
278
|
+
* - Generally, it only makes sense to be read by that function as well, and code inside of authorityStorage
|
|
279
|
+
* - Generally this is whether or not the locks are accepted and have no contention in their ranges. */
|
|
280
|
+
readonly valid?: boolean;
|
|
280
281
|
|
|
281
282
|
// #region Serialization Only
|
|
282
283
|
|
|
@@ -296,12 +297,29 @@ export type NodeId = string;
|
|
|
296
297
|
export type WatchConfig = {
|
|
297
298
|
paths: string[];
|
|
298
299
|
parentPaths: string[];
|
|
300
|
+
// TODO: This should probably be on a per-path basis.
|
|
301
|
+
fullHistory?: boolean;
|
|
299
302
|
};
|
|
300
303
|
|
|
301
304
|
export type PathValueSnapshot = {
|
|
302
305
|
values: { [path: string]: PathValue[] };
|
|
303
306
|
};
|
|
304
307
|
|
|
308
|
+
/** NOTE: Oftentimes path values have the exact same read locks, as in the array as triple equals. This can make a lot of code more efficient. */
|
|
309
|
+
export function byLockGroup(values: PathValue[]): Map<ReadLock[], PathValue[]> {
|
|
310
|
+
let byLockGroup = new Map<ReadLock[], PathValue[]>();
|
|
311
|
+
for (let value of values) {
|
|
312
|
+
let locks = value.locks;
|
|
313
|
+
if (locks.length === 0) continue;
|
|
314
|
+
let lockGroup = byLockGroup.get(locks);
|
|
315
|
+
if (!lockGroup) {
|
|
316
|
+
lockGroup = [];
|
|
317
|
+
byLockGroup.set(locks, lockGroup);
|
|
318
|
+
}
|
|
319
|
+
lockGroup.push(value);
|
|
320
|
+
}
|
|
321
|
+
return byLockGroup;
|
|
322
|
+
}
|
|
305
323
|
|
|
306
324
|
export function getNextTime(): Time {
|
|
307
325
|
return {
|
|
@@ -359,46 +377,12 @@ export function debugPathValuePath(pathValue: { time: Time; path: string }): str
|
|
|
359
377
|
return `(${debugTime(pathValue.time)}) ${pathValue.path}`;
|
|
360
378
|
}
|
|
361
379
|
export function debugPathValue(pathValue: PathValue, write?: boolean): string {
|
|
362
|
-
return `${debugPathValuePath(pathValue)} ${write ? "=" : "==="} ${String(pathValueSerializer.getPathValue(pathValue))}`;
|
|
380
|
+
return `${debugPathValuePath(pathValue)} ${write ? "=" : "==="} ${ellipsisMiddle(String(pathValueSerializer.getPathValue(pathValue)), 200)}`;
|
|
363
381
|
}
|
|
364
382
|
export function debugTime(time: Time): string {
|
|
365
383
|
return `${time.time}[${time.version}]@${time.creatorId.toString().slice(4, 12)}`;
|
|
366
384
|
}
|
|
367
385
|
|
|
368
|
-
export function filterChildPaths(parentPath: string, packedSuffix: string, paths: Set<string> | undefined) {
|
|
369
|
-
if (!packedSuffix || !paths) return paths;
|
|
370
|
-
return filterChildPathsBase(parentPath, packedSuffix, paths);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Returns a number between 0 (inclusive) and 1 (exclusive)
|
|
374
|
-
* - See matchesParentRangeFilter, matchesParentRangeFilterPart, and filterChildPaths.
|
|
375
|
-
* FOR INTERNAL USE ONLY!
|
|
376
|
-
*/
|
|
377
|
-
export function __getRoutingHash(key: string): number {
|
|
378
|
-
let hash = sha256(key);
|
|
379
|
-
return getBufferFraction(Buffer.from(hash, "hex"));
|
|
380
|
-
}
|
|
381
|
-
// NOTE: Assumes fullPath is already a child of parentPath, and only checks for hash
|
|
382
|
-
export function matchesParentRangeFilter(config: {
|
|
383
|
-
parentPath: string;
|
|
384
|
-
fullPath: string;
|
|
385
|
-
start: number;
|
|
386
|
-
end: number;
|
|
387
|
-
}) {
|
|
388
|
-
if (config.start === 0 && config.end === 1) return true;
|
|
389
|
-
let part = getPathIndexAssert(config.fullPath, getPathDepth(config.parentPath));
|
|
390
|
-
let hash = __getRoutingHash(part);
|
|
391
|
-
return config.start <= hash && hash < config.end;
|
|
392
|
-
}
|
|
393
|
-
export function matchesParentRangeFilterPart(config: {
|
|
394
|
-
part: string;
|
|
395
|
-
start: number;
|
|
396
|
-
end: number;
|
|
397
|
-
}) {
|
|
398
|
-
if (config.start === 0 && config.end === 1) return true;
|
|
399
|
-
let hash = __getRoutingHash(config.part);
|
|
400
|
-
return config.start <= hash && hash < config.end;
|
|
401
|
-
}
|
|
402
386
|
|
|
403
387
|
let getCompressNetworkBase = () => false;
|
|
404
388
|
export const registerGetCompressNetwork = (fnc: () => boolean) => { getCompressNetworkBase = fnc; };
|
|
@@ -407,41 +391,6 @@ export function getCompressNetwork() {
|
|
|
407
391
|
}
|
|
408
392
|
|
|
409
393
|
|
|
410
|
-
const filterChildPathsBase = measureWrap(
|
|
411
|
-
function filterChildPathsBase(parentPath: string, packedSuffix: string, paths: Set<string>): Set<string> {
|
|
412
|
-
let [startFractionStr, endFractionStr] = packedSuffix.split("|");
|
|
413
|
-
let startFraction = Number(startFractionStr);
|
|
414
|
-
let endFraction = Number(endFractionStr);
|
|
415
|
-
|
|
416
|
-
let depth = getPathDepth(parentPath);
|
|
417
|
-
|
|
418
|
-
let filtered = new Set<string>();
|
|
419
|
-
for (let path of paths) {
|
|
420
|
-
let key = getPathIndexAssert(path, depth);
|
|
421
|
-
let hash = __getRoutingHash(key);
|
|
422
|
-
if (startFraction <= hash && hash < endFraction) {
|
|
423
|
-
filtered.add(path);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
//console.log(`Filtered ${paths.size} paths to ${filtered.size} paths`);
|
|
427
|
-
return filtered;
|
|
428
|
-
}
|
|
429
|
-
);
|
|
430
|
-
export function encodeParentFilter(config: {
|
|
431
|
-
path: string;
|
|
432
|
-
startFraction: number;
|
|
433
|
-
endFraction: number;
|
|
434
|
-
}) {
|
|
435
|
-
return hack_setPackedPathSuffix(config.path, `${config.startFraction}|${config.endFraction}`);
|
|
436
|
-
}
|
|
437
|
-
export function decodeParentFilter(path: string): { start: number, end: number } | undefined {
|
|
438
|
-
let packedSuffix = hack_getPackedPathSuffix(path);
|
|
439
|
-
if (!packedSuffix) return undefined;
|
|
440
|
-
let [startStr, endStr] = packedSuffix.split("|");
|
|
441
|
-
return { start: Number(startStr), end: Number(endStr) };
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
394
|
class AuthorityPathValueStorage {
|
|
446
395
|
// path => sorted by -time (so newest are first)
|
|
447
396
|
// NOTE: We automatically drop historical values that can no longer influence
|
|
@@ -496,6 +445,9 @@ class AuthorityPathValueStorage {
|
|
|
496
445
|
time?: Time,
|
|
497
446
|
readInvalid = false
|
|
498
447
|
): PathValue | undefined {
|
|
448
|
+
if (!this.isEventPath(path)) {
|
|
449
|
+
onPathInteracted(path, 0);
|
|
450
|
+
}
|
|
499
451
|
let overrideValue: PathValue | undefined;
|
|
500
452
|
if (this.overrides.size > 0) {
|
|
501
453
|
for (let override of this.overrides) {
|
|
@@ -535,19 +487,18 @@ class AuthorityPathValueStorage {
|
|
|
535
487
|
}
|
|
536
488
|
return undefined;
|
|
537
489
|
}
|
|
538
|
-
|
|
490
|
+
/** Returns all values valid or invalid. You should check the valid flag if you want to know if it's valid.
|
|
491
|
+
* - Almost every listener wants this for the purposes of doing validation, and therefore they will want to know if values are invalid, as invalid values can cause another value to become invalid or valid.
|
|
492
|
+
* sorted by -time (so newest are first)
|
|
493
|
+
*/
|
|
494
|
+
public getValuePlusHistory(path: string): PathValue[] {
|
|
495
|
+
if (!this.isEventPath(path)) {
|
|
496
|
+
onPathInteracted(path, 0);
|
|
497
|
+
}
|
|
539
498
|
if (this.overrides.size > 0) {
|
|
540
499
|
throw new Error(`AuthorityPathValueStorage.getValuePlusHistory support for temporaryOverride not implemented yet.`);
|
|
541
500
|
}
|
|
542
|
-
|
|
543
|
-
let values = this.values.get(path);
|
|
544
|
-
if (values) {
|
|
545
|
-
for (let value of values) {
|
|
546
|
-
if (!value.valid && !returnInvalid) continue;
|
|
547
|
-
valuesToReturn.push(value);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return valuesToReturn;
|
|
501
|
+
return this.values.get(path) || [];
|
|
551
502
|
}
|
|
552
503
|
|
|
553
504
|
// TODO: Optimize this via an index
|
|
@@ -560,8 +511,11 @@ class AuthorityPathValueStorage {
|
|
|
560
511
|
public getPathsFromParent(parentPath: string): Set<string> | undefined {
|
|
561
512
|
let packedSuffix = hack_getPackedPathSuffix(parentPath);
|
|
562
513
|
parentPath = hack_stripPackedPath(parentPath);
|
|
563
|
-
let
|
|
564
|
-
|
|
514
|
+
let paths = this.parentPathLookup.get(parentPath);
|
|
515
|
+
if (packedSuffix && paths) {
|
|
516
|
+
paths = filterChildPathsBase(parentPath, packedSuffix, paths);
|
|
517
|
+
}
|
|
518
|
+
return paths;
|
|
565
519
|
}
|
|
566
520
|
|
|
567
521
|
@measureFnc
|
|
@@ -583,7 +537,7 @@ class AuthorityPathValueStorage {
|
|
|
583
537
|
// 1) Need to maintain the values for other nodes
|
|
584
538
|
// 2) Don't need to worry about the values getting out of date,
|
|
585
539
|
// because we will get any new values whether we want to or not!
|
|
586
|
-
if (
|
|
540
|
+
if (PathRouter.isSelfAuthority(path)) return;
|
|
587
541
|
|
|
588
542
|
if (this.DEBUG_UNWATCH) {
|
|
589
543
|
console.log(blue(`Unsyncing path at ${Date.now()}`), path);
|
|
@@ -591,7 +545,6 @@ class AuthorityPathValueStorage {
|
|
|
591
545
|
|
|
592
546
|
this.isSyncedCache.delete(path);
|
|
593
547
|
this.removePathFromStorage(path, "unwatched");
|
|
594
|
-
writeValidStorage.deleteRemovedPath(path);
|
|
595
548
|
}
|
|
596
549
|
public markParentPathAsUnwatched(path: string) {
|
|
597
550
|
// NOTE: I don't think we have to handle the case where we are the authority,
|
|
@@ -615,73 +568,6 @@ class AuthorityPathValueStorage {
|
|
|
615
568
|
return time;
|
|
616
569
|
}
|
|
617
570
|
|
|
618
|
-
|
|
619
|
-
@measureFnc
|
|
620
|
-
public async getSnapshot(authority: AuthorityPath): Promise<PathValueSnapshot> {
|
|
621
|
-
let snapshot: PathValueSnapshot = {
|
|
622
|
-
values: Object.create(null),
|
|
623
|
-
};
|
|
624
|
-
let now = Date.now();
|
|
625
|
-
let count = 0;
|
|
626
|
-
for (let [path, values] of this.values) {
|
|
627
|
-
count++;
|
|
628
|
-
if (count > 10_000) {
|
|
629
|
-
await delay("paintLoop");
|
|
630
|
-
count = 0;
|
|
631
|
-
}
|
|
632
|
-
// NOTE: We can't return extra values, as they might be outdated values which were
|
|
633
|
-
// set to undefined, and then deleted, so returning them will create spurious values.
|
|
634
|
-
if (!pathValueAuthority2.isInAuthority(authority, path)) continue;
|
|
635
|
-
this.clearRedundantOldValues(values, now);
|
|
636
|
-
if (values.length === 0) continue;
|
|
637
|
-
snapshot.values[path] = values;
|
|
638
|
-
}
|
|
639
|
-
return snapshot;
|
|
640
|
-
}
|
|
641
|
-
@measureFnc
|
|
642
|
-
public ingestSnapshot(snapshot: PathValueSnapshot) {
|
|
643
|
-
// NOTE: It is important that we don't try to archive these values, as they either came
|
|
644
|
-
// from the archive, or have already been archived.
|
|
645
|
-
|
|
646
|
-
let now = Date.now();
|
|
647
|
-
|
|
648
|
-
for (let [path, values] of Object.entries(snapshot.values)) {
|
|
649
|
-
let existingValues = this.values.get(path);
|
|
650
|
-
for (let value of values) {
|
|
651
|
-
if (value.canGCValue) {
|
|
652
|
-
this.possiblyUndefinedPaths().value.add(value.path);
|
|
653
|
-
}
|
|
654
|
-
if (value.event) {
|
|
655
|
-
this.eventPaths().value.add(value.path);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (!existingValues) {
|
|
659
|
-
this.values.set(path, values);
|
|
660
|
-
this.addParentPath(path);
|
|
661
|
-
} else {
|
|
662
|
-
let allValues = existingValues.concat(values);
|
|
663
|
-
allValues.sort((a, b) => compareTime(b.time, a.time));
|
|
664
|
-
// Remove duplicates
|
|
665
|
-
for (let i = 0; i < allValues.length - 1; i++) {
|
|
666
|
-
if (allValues[i].time.time === allValues[i + 1].time.time &&
|
|
667
|
-
allValues[i].time.creatorId === allValues[i + 1].time.creatorId) {
|
|
668
|
-
allValues.splice(i, 1);
|
|
669
|
-
i--;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
values = allValues;
|
|
673
|
-
}
|
|
674
|
-
this.clearRedundantOldValues(values, now);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
public resetForInitialTrigger(path: string) {
|
|
679
|
-
if (isDebugLogEnabled()) {
|
|
680
|
-
auditLog("REMOVE FOR INITIAL SYNC", { path });
|
|
681
|
-
}
|
|
682
|
-
this.removePathFromStorage(path, "reset for initial trigger");
|
|
683
|
-
}
|
|
684
|
-
|
|
685
571
|
public DEBUG_hasAnyValues(path: string) {
|
|
686
572
|
let values = this.values.get(path);
|
|
687
573
|
if (!values) return false;
|
|
@@ -691,7 +577,7 @@ class AuthorityPathValueStorage {
|
|
|
691
577
|
return true;
|
|
692
578
|
}
|
|
693
579
|
public isSynced(path: string, ignoreParentSync = false) {
|
|
694
|
-
if (
|
|
580
|
+
if (PathRouter.isSelfAuthority(path)) return true;
|
|
695
581
|
|
|
696
582
|
if (this.isSyncedCache.has(path)) return true;
|
|
697
583
|
|
|
@@ -718,54 +604,69 @@ class AuthorityPathValueStorage {
|
|
|
718
604
|
}
|
|
719
605
|
public isParentSynced(path: string) {
|
|
720
606
|
// We have to check a test child path to see if we are a self authority
|
|
721
|
-
if (
|
|
607
|
+
if (PathRouter.isSelfAuthority(appendToPathStr(path, ""))) return true;
|
|
722
608
|
|
|
723
609
|
return this.parentsSynced.get(path) === true;
|
|
724
610
|
}
|
|
725
611
|
|
|
612
|
+
/** Obviously just for debugging. Doesn't trigger any watchers, just erases it as if we never had it. There shouldn't be really any downstream caches within the same process at least, and so this should result in the next read having the value missing. If we in the future add downstream caches, we're going to have to invalidate them as well.
|
|
613
|
+
- This is for testing synchronization issues cross process. As any cross-process notifications might be lost. However, internally, we can verify with one hundred percent certainty, if our code is correct, that all of our internal lookups and caching is always in sync.
|
|
614
|
+
*/
|
|
615
|
+
public async DEBUG_secretlyDeleteAllValuesWithPath(path: string) {
|
|
616
|
+
this.values.delete(path);
|
|
617
|
+
this.lastDeleteAt.delete(path);
|
|
618
|
+
this.lastDeleteAtOld.delete(path);
|
|
619
|
+
this.lastDeleteAtOlder.delete(path);
|
|
620
|
+
// Actually, we don't want to delete it from the isSynced cache, as we still want the path to be considered synchronized. We just want the values to be wrong, as in missing values.
|
|
621
|
+
// this.parentsSynced.delete(path);
|
|
622
|
+
// this.isSyncedCache.delete(path);
|
|
623
|
+
}
|
|
624
|
+
|
|
726
625
|
private getMultiNodesForParent: (path: string) => Map<string, unknown> | undefined = () => undefined;
|
|
727
626
|
public setGetMultiNodesForParent(fnc: (path: string) => Map<string, unknown> | undefined) {
|
|
728
627
|
this.getMultiNodesForParent = fnc;
|
|
729
628
|
}
|
|
629
|
+
|
|
630
|
+
public addParentSyncs(parentSyncs: { parentPath: string; sourceNodeId: string }[]) {
|
|
631
|
+
|
|
632
|
+
for (let obj of parentSyncs) {
|
|
633
|
+
let parentPath = obj.parentPath;
|
|
634
|
+
let sourceNodeId = obj.sourceNodeId;
|
|
635
|
+
let prevSynced = this.parentsSynced.get(parentPath);
|
|
636
|
+
if (prevSynced === true) continue;
|
|
637
|
+
let remoteNodeIds = this.getMultiNodesForParent(parentPath);
|
|
638
|
+
// NOTE: If there is only 1 authority, assume we received data from it, as who else would we receive it from?
|
|
639
|
+
if (!remoteNodeIds || remoteNodeIds.size === 1) {
|
|
640
|
+
this.parentsSynced.set(parentPath, true);
|
|
641
|
+
} else {
|
|
642
|
+
if (!prevSynced) {
|
|
643
|
+
prevSynced = new Set(remoteNodeIds.keys());
|
|
644
|
+
this.parentsSynced.set(parentPath, prevSynced);
|
|
645
|
+
}
|
|
646
|
+
prevSynced.delete(sourceNodeId);
|
|
647
|
+
if (prevSynced.size === 0) {
|
|
648
|
+
this.parentsSynced.set(parentPath, true);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** @deprecated Should only be called by ValidStateComputer. Otherwise, the valid states can change and it won't be able to detect them, and so triggering nested dependencies will not work. */
|
|
730
655
|
@measureFnc
|
|
731
656
|
public ingestValues(
|
|
732
657
|
newValues: PathValue[],
|
|
733
|
-
|
|
734
|
-
parentSyncedSources: Map<string, string[]> | undefined,
|
|
735
|
-
// Skips archiving (used if we received the values from another node, in which case
|
|
736
|
-
// not only should the values already be archives, they are likely too old to be archives
|
|
737
|
-
// again!)
|
|
738
|
-
skipArchive?: "skipArchive"
|
|
658
|
+
config?: { doNotArchive?: boolean },
|
|
739
659
|
) {
|
|
660
|
+
for (let value of newValues) {
|
|
661
|
+
if (value.event) continue;
|
|
662
|
+
onPathInteracted(value.path, 1);
|
|
663
|
+
}
|
|
664
|
+
let { doNotArchive } = config ?? {};
|
|
740
665
|
let now = Date.now();
|
|
741
666
|
|
|
742
|
-
if (isDebugLogEnabled()) {
|
|
667
|
+
if (isDebugLogEnabled() && !config?.doNotArchive) {
|
|
743
668
|
for (let value of newValues) {
|
|
744
|
-
auditLog("INGEST VALUE", { path: value.path,
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
if (parentsSynced) {
|
|
749
|
-
for (let parentPath of parentsSynced) {
|
|
750
|
-
let prevSynced = this.parentsSynced.get(parentPath);
|
|
751
|
-
if (prevSynced === true) continue;
|
|
752
|
-
let remoteNodeIds = this.getMultiNodesForParent(parentPath);
|
|
753
|
-
let sourceNodeIds = parentSyncedSources?.get(parentPath);
|
|
754
|
-
// NOTE: If there is only 1 authority, assume we received data from it, as who else would we receive it from?
|
|
755
|
-
if (!remoteNodeIds || remoteNodeIds.size === 1 || !sourceNodeIds) {
|
|
756
|
-
this.parentsSynced.set(parentPath, true);
|
|
757
|
-
} else {
|
|
758
|
-
if (!prevSynced) {
|
|
759
|
-
prevSynced = new Set(remoteNodeIds.keys());
|
|
760
|
-
this.parentsSynced.set(parentPath, prevSynced);
|
|
761
|
-
}
|
|
762
|
-
for (let sourceNodeId of sourceNodeIds) {
|
|
763
|
-
prevSynced.delete(sourceNodeId);
|
|
764
|
-
}
|
|
765
|
-
if (prevSynced.size === 0) {
|
|
766
|
-
this.parentsSynced.set(parentPath, true);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
669
|
+
auditLog("INGEST VALUE", { path: value.path, timeId: value.time.time });
|
|
769
670
|
}
|
|
770
671
|
}
|
|
771
672
|
|
|
@@ -778,27 +679,13 @@ class AuthorityPathValueStorage {
|
|
|
778
679
|
this.possiblyUndefinedPaths().value.add(value.path);
|
|
779
680
|
}
|
|
780
681
|
|
|
781
|
-
if (!
|
|
782
|
-
let
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
let values = this.pendingArchiveValues.get(authorityPath);
|
|
787
|
-
if (!values) {
|
|
788
|
-
values = [];
|
|
789
|
-
this.pendingArchiveValues.set(authorityPath, values);
|
|
790
|
-
}
|
|
791
|
-
// If our value has no locks it can never be rejected, so if it is the latest, don't bother to keep the others
|
|
792
|
-
if (value.lockCount === 0 && values.length > 0 && values.every(x => compareTime(x.time, value.time) < 0)) {
|
|
793
|
-
validateLockCount(value);
|
|
794
|
-
if (values.length > 1) {
|
|
795
|
-
values.splice(1, values.length - 1);
|
|
796
|
-
}
|
|
797
|
-
values[0] = value;
|
|
798
|
-
} else {
|
|
799
|
-
values.push(value);
|
|
800
|
-
}
|
|
682
|
+
if (!doNotArchive && !value.event && !PathRouter.isLocalPath(value.path) && PathRouter.isSelfAuthority(value.path)) {
|
|
683
|
+
let values = this.pendingArchiveValues.get(value.path);
|
|
684
|
+
if (!values) {
|
|
685
|
+
values = [];
|
|
686
|
+
this.pendingArchiveValues.set(value.path, values);
|
|
801
687
|
}
|
|
688
|
+
values.push(value);
|
|
802
689
|
}
|
|
803
690
|
|
|
804
691
|
// Store in global lookup
|
|
@@ -816,13 +703,15 @@ class AuthorityPathValueStorage {
|
|
|
816
703
|
} else {
|
|
817
704
|
let prev = values[insertIndex];
|
|
818
705
|
if (prev && compareTime(prev.time, value.time) === 0) {
|
|
706
|
+
// NOTE: Before, we used to try to just update the valid state. However, it's kind of unsafe to do this as it results in mutating values, which could make our committer miss a valid state change. Assignment should be just as good.
|
|
707
|
+
values[insertIndex] = value;
|
|
819
708
|
// Just update the valid state (we likely aren't an authority on the path anyways)
|
|
820
|
-
prev.valid = value.valid;
|
|
709
|
+
//prev.valid = value.valid;
|
|
821
710
|
} else {
|
|
822
711
|
// Special case values that always have no locks, by just setting the value, and never splicing
|
|
823
712
|
if (insertIndex === 0 && value.lockCount === 0 && values.length <= 1) {
|
|
824
713
|
validateLockCount(value);
|
|
825
|
-
if (!isCoreQuiet) {
|
|
714
|
+
if (!isCoreQuiet && !doNotArchive) {
|
|
826
715
|
if (values.length === 1) {
|
|
827
716
|
console.log(`Clobbering ${debugPathValuePath(values[0])} with ${debugPathValuePath(value)}`);
|
|
828
717
|
}
|
|
@@ -835,7 +724,7 @@ class AuthorityPathValueStorage {
|
|
|
835
724
|
}
|
|
836
725
|
}
|
|
837
726
|
if (!specialGoldenCase) {
|
|
838
|
-
this.clearRedundantOldValues(values, now);
|
|
727
|
+
this.clearRedundantOldValues(values, now, doNotArchive);
|
|
839
728
|
}
|
|
840
729
|
}
|
|
841
730
|
}
|
|
@@ -850,8 +739,8 @@ class AuthorityPathValueStorage {
|
|
|
850
739
|
this.archiveLoop();
|
|
851
740
|
}
|
|
852
741
|
|
|
853
|
-
//
|
|
854
|
-
private pendingArchiveValues = registerMapArrayResource("pendingArchiveValues", new Map<
|
|
742
|
+
// path => values
|
|
743
|
+
private pendingArchiveValues = registerMapArrayResource("pendingArchiveValues", new Map<string, PathValue[]>());
|
|
855
744
|
private shuttingDown = new PromiseObj<"shutdown">();
|
|
856
745
|
private pendingShutdownWrites: PromiseObj | undefined;
|
|
857
746
|
|
|
@@ -875,7 +764,7 @@ class AuthorityPathValueStorage {
|
|
|
875
764
|
// Wait long enough for all values to be old enough to have a fixed valid state.
|
|
876
765
|
// Also wait some extra time randomly, to reduce the chances of archive being
|
|
877
766
|
// called at the same time on multiple nodes at once.
|
|
878
|
-
let delayTime =
|
|
767
|
+
let delayTime = MIN_WAIT_TIME_UNTIL_DISK_FLUSH + Math.random() * RAND_ADD_TIME_UNTIL_DISK_FLUSH;
|
|
879
768
|
if (this.shuttingDown.resolved) {
|
|
880
769
|
delayTime = 0;
|
|
881
770
|
}
|
|
@@ -890,14 +779,16 @@ class AuthorityPathValueStorage {
|
|
|
890
779
|
}
|
|
891
780
|
|
|
892
781
|
try {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
782
|
+
// Only take the valid values, and only the latest. We don't need the history, because these values are all old enough that they can't be rejected (And any one that is referencing the non-latest value, by definition, has to be older than that, and therefore old enough that it can also no longer be rejected)
|
|
783
|
+
let finalValues: PathValue[] = [];
|
|
784
|
+
for (let values of pending.values()) {
|
|
785
|
+
// Valid, and not epoch time
|
|
786
|
+
values = values.filter(x => x.valid && x.time.time);
|
|
787
|
+
if (values.length === 0) continue;
|
|
788
|
+
values.sort((a, b) => compareTime(b.time, a.time));
|
|
789
|
+
finalValues.push(values[0]);
|
|
790
|
+
}
|
|
791
|
+
await pathValueArchives.archiveValues(finalValues);
|
|
901
792
|
} finally {
|
|
902
793
|
this.pendingShutdownWrites?.resolve();
|
|
903
794
|
}
|
|
@@ -907,12 +798,11 @@ class AuthorityPathValueStorage {
|
|
|
907
798
|
if (yargObj.noarchive) return;
|
|
908
799
|
this.shuttingDown.resolve("shutdown");
|
|
909
800
|
if (this.pendingArchiveValues.size === 0) return;
|
|
910
|
-
await
|
|
801
|
+
let topology = await authorityLookup.getTopology();
|
|
911
802
|
// If there are other nodes, we CAN'T just archive our values, as some maybe rejected in the future.
|
|
912
|
-
let
|
|
913
|
-
let otherReadNodes = nodes.filter(x => !isOwnNodeId(x));
|
|
803
|
+
let otherReadNodes = topology.filter(x => !isOwnNodeId(x.nodeId));
|
|
914
804
|
if (otherReadNodes.length > 0 && this.pendingArchiveValues.size > 0) {
|
|
915
|
-
console.log(`Other authorities (${otherReadNodes.length}), discarding ${this.pendingArchiveValues.size} pending writes
|
|
805
|
+
console.log(`Other authorities (${otherReadNodes.length}), discarding ${this.pendingArchiveValues.size} pending writes`, otherReadNodes, topology.filter(x => isOwnNodeId(x.nodeId)));
|
|
916
806
|
}
|
|
917
807
|
if (otherReadNodes.length === 0) {
|
|
918
808
|
// Wait the PromiseRace to finish
|
|
@@ -930,7 +820,7 @@ class AuthorityPathValueStorage {
|
|
|
930
820
|
// really happen too often (and the real issue is not moving values from memory => storage, which when we do,
|
|
931
821
|
// we will call this anyways).
|
|
932
822
|
// NOTE: values are sorted sorted by -time (so newest are first)
|
|
933
|
-
public clearRedundantOldValues(values: PathValue[], now: number) {
|
|
823
|
+
public clearRedundantOldValues(values: PathValue[], now: number, doNotArchive?: boolean) {
|
|
934
824
|
// NOTE: We can also delete golden rejections BEFORE the valid golden value. But...
|
|
935
825
|
// that is less important, as there are likely to be few rejected values, so this
|
|
936
826
|
// is really just designed for removing older golden values (which are probably valid).
|
|
@@ -953,7 +843,7 @@ class AuthorityPathValueStorage {
|
|
|
953
843
|
&& values[0].canGCValue
|
|
954
844
|
&& values[0].time.time < now - VALUE_GC_THRESHOLD
|
|
955
845
|
) {
|
|
956
|
-
if (!isCoreQuiet) {
|
|
846
|
+
if (!isCoreQuiet && !doNotArchive) {
|
|
957
847
|
console.log(`GCing ALL ${debugPathValuePath(values[0])}`);
|
|
958
848
|
for (let value of values) {
|
|
959
849
|
auditLog("GCing", { path: value.path });
|
|
@@ -977,7 +867,7 @@ class AuthorityPathValueStorage {
|
|
|
977
867
|
if (firstGoldenIndex === 0) {
|
|
978
868
|
// Remove everything but the latest value
|
|
979
869
|
let removed = values.splice(1, values.length - 1);
|
|
980
|
-
if (!isCoreQuiet) {
|
|
870
|
+
if (!isCoreQuiet && !doNotArchive) {
|
|
981
871
|
console.log(`GCing everything older than ${debugPathValuePath(values[0])}`);
|
|
982
872
|
for (let value of removed) {
|
|
983
873
|
auditLog("GCing", { path: value.path });
|
|
@@ -988,7 +878,7 @@ class AuthorityPathValueStorage {
|
|
|
988
878
|
if (firstGoldenIndex >= 0 && values.length > firstGoldenIndex) {
|
|
989
879
|
for (let i = values.length - 1; i > firstGoldenIndex; i--) {
|
|
990
880
|
let gced = values.pop();
|
|
991
|
-
if (!isCoreQuiet && gced) {
|
|
881
|
+
if (!isCoreQuiet && !doNotArchive && gced) {
|
|
992
882
|
auditLog("GCing", { path: gced.path });
|
|
993
883
|
}
|
|
994
884
|
}
|
|
@@ -996,7 +886,7 @@ class AuthorityPathValueStorage {
|
|
|
996
886
|
}
|
|
997
887
|
}
|
|
998
888
|
|
|
999
|
-
|
|
889
|
+
public removePathFromStorage(path: string, reason: string) {
|
|
1000
890
|
let values = this.values.get(path);
|
|
1001
891
|
if (values?.length) {
|
|
1002
892
|
this.lastDeleteAt.set(path, values[0].time);
|
|
@@ -1023,7 +913,8 @@ class AuthorityPathValueStorage {
|
|
|
1023
913
|
private possiblyUndefinedPaths = lazy((): { value: Set<string> } => {
|
|
1024
914
|
let pathsToCheckPointer = { value: new Set<string>() };
|
|
1025
915
|
|
|
1026
|
-
runInfinitePoll(UNDEFINED_MEMORY_CLEANUP_DELAY, () => {
|
|
916
|
+
runInfinitePoll(UNDEFINED_MEMORY_CLEANUP_DELAY, async () => {
|
|
917
|
+
const { pathWatcher } = await import("./PathWatcher");
|
|
1027
918
|
let now = Date.now();
|
|
1028
919
|
let deadTime = now - UNDEFINED_MEMORY_CLEANUP_DELAY;
|
|
1029
920
|
let pathsToCheck = pathsToCheckPointer.value;
|
|
@@ -1061,11 +952,16 @@ class AuthorityPathValueStorage {
|
|
|
1061
952
|
return pathsToCheckPointer;
|
|
1062
953
|
});
|
|
1063
954
|
|
|
955
|
+
private isEventPath(path: string) {
|
|
956
|
+
return this.eventPaths().value.has(path);
|
|
957
|
+
}
|
|
958
|
+
|
|
1064
959
|
private eventPaths = lazy((): { value: Set<string> } => {
|
|
1065
960
|
let pathsToCheckPointer = { value: new Set<string>() };
|
|
1066
961
|
|
|
1067
962
|
let garbageCollectTime = MAX_CHANGE_AGE * 2;
|
|
1068
|
-
runInfinitePoll(garbageCollectTime, () => {
|
|
963
|
+
runInfinitePoll(garbageCollectTime, async () => {
|
|
964
|
+
const { pathWatcher } = await import("./PathWatcher");
|
|
1069
965
|
let deadTime = Date.now() - garbageCollectTime;
|
|
1070
966
|
let pathsToCheck = pathsToCheckPointer.value;
|
|
1071
967
|
let nextPathsToCheck = new Set<string>();
|
|
@@ -1129,1241 +1025,48 @@ class AuthorityPathValueStorage {
|
|
|
1129
1025
|
public __auditValues() {
|
|
1130
1026
|
return this.values;
|
|
1131
1027
|
}
|
|
1132
|
-
}
|
|
1133
|
-
export const authorityStorage = new AuthorityPathValueStorage();
|
|
1134
|
-
export const values = authorityStorage;
|
|
1135
|
-
export const pathValues = authorityStorage;
|
|
1136
|
-
|
|
1137
1028
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
// isOwnNodeId(nodeId) means localOnValueCallback is called (instead of PathValueController.forwardWrites)
|
|
1143
|
-
export type PathWatcherCallback = NodeId;
|
|
1144
|
-
class PathWatcher {
|
|
1145
|
-
private watchers = registerResource("paths|PathWatcher.watchers", new Map<string, {
|
|
1146
|
-
watchers: Set<PathWatcherCallback>;
|
|
1147
|
-
requestedTime: number;
|
|
1148
|
-
receivedTime: number;
|
|
1149
|
-
}>());
|
|
1150
|
-
// A fast way to lookup parentPath => childPaths in watchers, without having
|
|
1151
|
-
// to brute force search.
|
|
1152
|
-
private watcherParentToPath = registerResource("paths|PathWatcher.watcherParentToPath", new Map<string, Set<string>>());
|
|
1153
|
-
// If we don't limit the length, then this will memory leak. And if we only track
|
|
1154
|
-
// the values in watchers, then rolling key syncs will be lost.
|
|
1155
|
-
// DOES NOT track local values, as they are used internally, but we aren't waiting for them to be synced.
|
|
1156
|
-
private syncHistoryForHarvest = new QueueLimited<{ start: number; end: number; path: string; }>(1_000_000);
|
|
1157
|
-
private syncHistoryForDebug = new QueueLimited<{ start: number; end: number; path: string; }>(1_000_000);
|
|
1158
|
-
|
|
1159
|
-
//private watchers = registerResource("paths|PathWatcher.watchers", new Map<string, Set<PathWatcherCallback>>());
|
|
1160
|
-
|
|
1161
|
-
// realPath => packedPath => watcher => { depth, start, end }[]
|
|
1162
|
-
private parentWatchers = registerResource("paths|parentWatchers", new Map<string,
|
|
1163
|
-
// packedPath
|
|
1164
|
-
Map<string, {
|
|
1165
|
-
watchers: Set<PathWatcherCallback>;
|
|
1166
|
-
start: number;
|
|
1167
|
-
end: number;
|
|
1168
|
-
}>
|
|
1169
|
-
>());
|
|
1170
|
-
private watchersToPaths = registerResource("paths|watchersToPaths", new Map<PathWatcherCallback, {
|
|
1171
|
-
paths: Set<string>;
|
|
1172
|
-
parents: Set<string>;
|
|
1173
|
-
}>());
|
|
1174
|
-
|
|
1175
|
-
/** Automatically unwatches on callback disconnect */
|
|
1176
|
-
@measureFnc
|
|
1177
|
-
public watchPath(config: WatchConfig & {
|
|
1178
|
-
callback: PathWatcherCallback;
|
|
1179
|
-
debugName?: string;
|
|
1180
|
-
noInitialTrigger?: boolean;
|
|
1181
|
-
/** Force trigger, even if we are already watching the values. */
|
|
1182
|
-
initialTrigger?: boolean;
|
|
1183
|
-
}) {
|
|
1184
|
-
if (config.callback) {
|
|
1185
|
-
this.ensureUnwatchingOnDisconnect(config.callback);
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
let newPathsWatched = new Set<string>();
|
|
1189
|
-
let newParentsWatched = new Set<string>();
|
|
1190
|
-
|
|
1191
|
-
let time = Date.now();
|
|
1192
|
-
|
|
1193
|
-
for (let path of config.paths) {
|
|
1194
|
-
let watchers = this.watchers.get(path);
|
|
1195
|
-
if (!watchers) {
|
|
1196
|
-
watchers = {
|
|
1197
|
-
watchers: new Set(),
|
|
1198
|
-
requestedTime: time,
|
|
1199
|
-
receivedTime: 0,
|
|
1200
|
-
};
|
|
1201
|
-
this.watchers.set(path, watchers);
|
|
1202
|
-
let parentPath = getParentPathStr(path);
|
|
1203
|
-
let parentWatchers = this.watcherParentToPath.get(parentPath);
|
|
1204
|
-
if (!parentWatchers) {
|
|
1205
|
-
parentWatchers = new Set();
|
|
1206
|
-
this.watcherParentToPath.set(parentPath, parentWatchers);
|
|
1207
|
-
}
|
|
1208
|
-
parentWatchers.add(path);
|
|
1209
|
-
}
|
|
1210
|
-
if (watchers.watchers.has(config.callback)) continue;
|
|
1211
|
-
newPathsWatched.add(path);
|
|
1212
|
-
watchers.watchers.add(config.callback);
|
|
1213
|
-
|
|
1214
|
-
let watchObj = this.watchersToPaths.get(config.callback);
|
|
1215
|
-
if (!watchObj) {
|
|
1216
|
-
watchObj = { paths: new Set(), parents: new Set() };
|
|
1217
|
-
this.watchersToPaths.set(config.callback, watchObj);
|
|
1218
|
-
}
|
|
1219
|
-
watchObj.paths.add(path);
|
|
1220
|
-
}
|
|
1221
|
-
for (let path of config.parentPaths) {
|
|
1222
|
-
let basePath = hack_stripPackedPath(path);
|
|
1223
|
-
let watchersObj = this.parentWatchers.get(basePath);
|
|
1224
|
-
if (!watchersObj) {
|
|
1225
|
-
watchersObj = new Map();
|
|
1226
|
-
this.parentWatchers.set(basePath, watchersObj);
|
|
1227
|
-
}
|
|
1228
|
-
let obj = watchersObj.get(path);
|
|
1229
|
-
if (!obj) {
|
|
1230
|
-
let range = decodeParentFilter(path) || { start: 0, end: 1 };
|
|
1231
|
-
obj = { watchers: new Set(), start: range.start, end: range.end };
|
|
1232
|
-
watchersObj.set(path, obj);
|
|
1233
|
-
}
|
|
1234
|
-
if (obj.watchers.has(config.callback)) continue;
|
|
1235
|
-
obj.watchers.add(config.callback);
|
|
1236
|
-
|
|
1237
|
-
newParentsWatched.add(path);
|
|
1238
|
-
|
|
1239
|
-
let watchObj = this.watchersToPaths.get(config.callback);
|
|
1240
|
-
if (!watchObj) {
|
|
1241
|
-
watchObj = { paths: new Set(), parents: new Set() };
|
|
1242
|
-
this.watchersToPaths.set(config.callback, watchObj);
|
|
1243
|
-
}
|
|
1244
|
-
watchObj.parents.add(path);
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
if (newPathsWatched.size > 0 || newParentsWatched.size > 0) {
|
|
1248
|
-
if (isOwnNodeId(config.callback)) {
|
|
1249
|
-
for (let path of newPathsWatched) {
|
|
1250
|
-
auditLog("new local WATCH VALUE", { path });
|
|
1251
|
-
}
|
|
1252
|
-
for (let path of newParentsWatched) {
|
|
1253
|
-
auditLog("new local WATCH PARENT", { path });
|
|
1254
|
-
}
|
|
1255
|
-
} else {
|
|
1256
|
-
for (let path of newPathsWatched) {
|
|
1257
|
-
auditLog("new non-local WATCH VALUE", { path, watcher: config.callback });
|
|
1258
|
-
}
|
|
1259
|
-
for (let path of newParentsWatched) {
|
|
1260
|
-
auditLog("new non-local WATCH PARENT", { path, watcher: config.callback });
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
console.info(`New PathValue watches`, {
|
|
1264
|
-
newPathsWatched: newPathsWatched.size,
|
|
1265
|
-
newParentsWatched: newParentsWatched.size,
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
// Treat everything as a new watch
|
|
1270
|
-
if (config.initialTrigger) {
|
|
1271
|
-
newPathsWatched = new Set(config.paths);
|
|
1272
|
-
newParentsWatched = new Set(config.parentPaths);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
if (!config.noInitialTrigger) {
|
|
1276
|
-
// Trigger all initial values (for paths we have synced)
|
|
1277
|
-
let initialValues = new Set<PathValue>();
|
|
1278
|
-
let newPaths = new Set(Array.from(newPathsWatched).filter(x => authorityStorage.isSynced(x)));
|
|
1279
|
-
for (let path of newParentsWatched) {
|
|
1280
|
-
// If we haven't synced the parent, we can't forward it. This is important for proxies
|
|
1281
|
-
// (when they receive the parent synced they will forward the values).
|
|
1282
|
-
if (!authorityStorage.isParentSynced(path)) continue;
|
|
1283
|
-
let paths = authorityStorage.getPathsFromParent(path);
|
|
1284
|
-
if (paths) {
|
|
1285
|
-
for (let path of paths) {
|
|
1286
|
-
newPaths.add(path);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
let childPaths = this.watcherParentToPath.get(path);
|
|
1291
|
-
if (childPaths) {
|
|
1292
|
-
for (let childPath of childPaths) {
|
|
1293
|
-
newPaths.add(childPath);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
if (newPaths.size > 0 || newParentsWatched.size > 0) {
|
|
1299
|
-
for (let path of newPaths) {
|
|
1300
|
-
let history = authorityStorage.getValuePlusHistory(path);
|
|
1301
|
-
for (let value of history) {
|
|
1302
|
-
initialValues.add(value);
|
|
1303
|
-
}
|
|
1304
|
-
if (history.length === 0) {
|
|
1305
|
-
// We always have to send something, or the client won't know when they are ready to show the data!
|
|
1306
|
-
initialValues.add({ path, value: undefined, canGCValue: true, isTransparent: true, valid: true, time: epochTime, locks: [], lockCount: 0, event: false });
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
this.triggerLatestWatcher(
|
|
1310
|
-
config.callback,
|
|
1311
|
-
Array.from(initialValues),
|
|
1312
|
-
config.parentPaths,
|
|
1313
|
-
config.initialTrigger ? "initialTrigger" : undefined,
|
|
1314
|
-
);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
// NOTE: Automatically unwatches from remoteWatcher on all paths that are fully unwatched
|
|
1319
|
-
@measureFnc
|
|
1320
|
-
public unwatchPath(config: WatchConfig & { callback: PathWatcherCallback; }) {
|
|
1321
|
-
const { callback } = config;
|
|
1322
|
-
|
|
1323
|
-
let fullyUnwatched: WatchConfig = {
|
|
1324
|
-
paths: [],
|
|
1325
|
-
parentPaths: [],
|
|
1326
|
-
};
|
|
1327
|
-
|
|
1328
|
-
let pathsWatched = this.watchersToPaths.get(callback);
|
|
1329
|
-
|
|
1330
|
-
for (let path of config.parentPaths) {
|
|
1331
|
-
if (pathsWatched) {
|
|
1332
|
-
pathsWatched.parents.delete(path);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
let unpackedPath = hack_stripPackedPath(path);
|
|
1336
|
-
|
|
1337
|
-
let watchersObj = this.parentWatchers.get(unpackedPath);
|
|
1338
|
-
if (!watchersObj) continue;
|
|
1339
|
-
let obj = watchersObj.get(path);
|
|
1340
|
-
if (!obj) continue;
|
|
1341
|
-
obj.watchers.delete(callback);
|
|
1342
|
-
|
|
1343
|
-
if (isOwnNodeId(callback)) {
|
|
1344
|
-
auditLog("local UNWATCH PARENT", { path });
|
|
1345
|
-
} else {
|
|
1346
|
-
auditLog("non-local UNWATCH PARENT", { path, watcher: callback });
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
if (obj.watchers.size === 0) {
|
|
1350
|
-
watchersObj.delete(path);
|
|
1351
|
-
this.parentWatchers.delete(path);
|
|
1352
|
-
fullyUnwatched.parentPaths.push(path);
|
|
1353
|
-
authorityStorage.markParentPathAsUnwatched(path);
|
|
1354
|
-
|
|
1355
|
-
if (watchersObj.size === 0) {
|
|
1356
|
-
this.parentWatchers.delete(unpackedPath);
|
|
1357
|
-
|
|
1358
|
-
let childPaths = authorityStorage.getPathsFromParent(unpackedPath);
|
|
1359
|
-
// Destroy values that now have no watchers (no value watchers, and now no parent watcher
|
|
1360
|
-
if (childPaths) {
|
|
1361
|
-
for (let childPath of childPaths) {
|
|
1362
|
-
if (!this.watchers.has(childPath)) {
|
|
1363
|
-
authorityStorage.destroyPath(childPath);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// NOTE: Unwatch parents, then values, as parent paths might keep alive value path watches.
|
|
1372
|
-
for (let path of config.paths) {
|
|
1373
|
-
if (pathsWatched) {
|
|
1374
|
-
pathsWatched.paths.delete(path);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
let watchers = this.watchers.get(path);
|
|
1378
|
-
if (!watchers) continue;
|
|
1379
|
-
watchers.watchers.delete(callback);
|
|
1380
|
-
|
|
1381
|
-
if (isOwnNodeId(callback)) {
|
|
1382
|
-
auditLog("local UNWATCH VALUE", { path });
|
|
1383
|
-
} else {
|
|
1384
|
-
auditLog("non-local UNWATCH VALUE", { path, watcher: callback });
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
if (watchers.watchers.size === 0) {
|
|
1388
|
-
this.watchers.delete(path);
|
|
1389
|
-
|
|
1390
|
-
let parentPath = getParentPathStr(path);
|
|
1391
|
-
let parentWatchers = this.watcherParentToPath.get(parentPath);
|
|
1392
|
-
if (parentWatchers) {
|
|
1393
|
-
parentWatchers.delete(path);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
fullyUnwatched.paths.push(path);
|
|
1397
|
-
// NOTE: If the parent is being watched, don't destroy the value.
|
|
1398
|
-
// - The fact that we only do the check here does mean that if the path is unwatched
|
|
1399
|
-
// first and then later the parent path is unwatched we will fail to properly destroy
|
|
1400
|
-
// the path. However, in practice, either both are unwatched at the same time, or more
|
|
1401
|
-
// likely, the value watch only goes away because it's made redundant by the parent watch.
|
|
1402
|
-
if (!this.parentWatchers.has(parentPath)) {
|
|
1403
|
-
authorityStorage.destroyPath(path);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
if (fullyUnwatched.paths.length > 0 || fullyUnwatched.parentPaths.length > 0) {
|
|
1409
|
-
console.info(`Unwatched PathValue watches`, {
|
|
1410
|
-
unwatchedPaths: fullyUnwatched.paths.length,
|
|
1411
|
-
unwatchedParents: fullyUnwatched.parentPaths.length,
|
|
1412
|
-
});
|
|
1413
|
-
for (let unwatchCallback of this.unwatchedCallbacks) {
|
|
1414
|
-
unwatchCallback(fullyUnwatched);
|
|
1415
|
-
}
|
|
1029
|
+
public getValueCount() {
|
|
1030
|
+
let totalCount = 0;
|
|
1031
|
+
for (let values of this.values.values()) {
|
|
1032
|
+
totalCount += values.length;
|
|
1416
1033
|
}
|
|
1034
|
+
return totalCount;
|
|
1417
1035
|
}
|
|
1418
1036
|
|
|
1419
|
-
public
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
}
|
|
1431
|
-
let parentWatchers = this.parentWatchers.get(path);
|
|
1432
|
-
if (parentWatchers) {
|
|
1433
|
-
this.parentWatchers.delete(path);
|
|
1434
|
-
for (let { watchers } of parentWatchers.values()) {
|
|
1435
|
-
for (let watcher of watchers) {
|
|
1436
|
-
let watchObj = this.watchersToPaths.get(watcher);
|
|
1437
|
-
if (watchObj) {
|
|
1438
|
-
watchObj.parents.delete(path);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
public triggerValuesChanged(valuesChanged: Set<PathValue>, parentsSynced?: string[], initialTriggers?: { values: Set<string>; parentPaths: Set<string> }) {
|
|
1447
|
-
let changedPerCallbacks = this.getWatchers(valuesChanged, { parentsSynced, pathsAreSynced: true });
|
|
1448
|
-
for (let [watch, changes] of changedPerCallbacks) {
|
|
1449
|
-
if (initialTriggers) {
|
|
1450
|
-
let valuesFromInitialTrigger = new Set<PathValue>();
|
|
1451
|
-
let parentsFromInitialTrigger = new Set<string>();
|
|
1452
|
-
let valuesOther = new Set<PathValue>();
|
|
1453
|
-
let parentsOther = new Set<string>();
|
|
1454
|
-
for (let value of changes) {
|
|
1455
|
-
if (initialTriggers.values.has(value.path)) {
|
|
1456
|
-
valuesFromInitialTrigger.add(value);
|
|
1457
|
-
} else {
|
|
1458
|
-
valuesOther.add(value);
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
for (let parent of initialTriggers.parentPaths) {
|
|
1462
|
-
parentsFromInitialTrigger.add(parent);
|
|
1463
|
-
}
|
|
1464
|
-
if (parentsSynced) {
|
|
1465
|
-
for (let parent of parentsSynced) {
|
|
1466
|
-
if (!initialTriggers.parentPaths.has(parent)) {
|
|
1467
|
-
parentsOther.add(parent);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
if (valuesFromInitialTrigger.size > 0 || parentsFromInitialTrigger.size > 0) {
|
|
1473
|
-
this.triggerLatestWatcher(watch, Array.from(valuesFromInitialTrigger), Array.from(parentsFromInitialTrigger), "initialTrigger");
|
|
1474
|
-
}
|
|
1475
|
-
if (valuesOther.size > 0 || parentsOther.size > 0) {
|
|
1476
|
-
this.triggerLatestWatcher(watch, Array.from(valuesOther), Array.from(parentsOther));
|
|
1477
|
-
}
|
|
1478
|
-
} else {
|
|
1479
|
-
this.triggerLatestWatcher(watch, Array.from(changes), parentsSynced);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
public getWatchers<T extends { path: string }>(
|
|
1484
|
-
valuesChanged: Set<T>,
|
|
1485
|
-
config?: {
|
|
1486
|
-
parentsSynced?: string[];
|
|
1487
|
-
pathsAreSynced?: boolean;
|
|
1488
|
-
}
|
|
1489
|
-
) {
|
|
1490
|
-
let changedPerCallbacks: Map<PathWatcherCallback, Set<T>> = new Map();
|
|
1491
|
-
let time = Date.now();
|
|
1492
|
-
for (let value of valuesChanged) {
|
|
1493
|
-
let path = value.path;
|
|
1494
|
-
let latestWatches = this.watchers.get(path);
|
|
1495
|
-
if (config?.pathsAreSynced && latestWatches && !latestWatches.receivedTime && !path.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
1496
|
-
latestWatches.receivedTime = time;
|
|
1497
|
-
let obj = { start: latestWatches.requestedTime, end: time, path };
|
|
1498
|
-
this.syncHistoryForHarvest.push(obj);
|
|
1499
|
-
this.syncHistoryForDebug.push(obj);
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
function triggerNodeChanged(watcher: NodeId) {
|
|
1503
|
-
let changes = changedPerCallbacks.get(watcher);
|
|
1504
|
-
if (!changes) {
|
|
1505
|
-
changes = new Set();
|
|
1506
|
-
changedPerCallbacks.set(watcher, changes);
|
|
1507
|
-
}
|
|
1508
|
-
// @ts-ignore
|
|
1509
|
-
changes.add(value);
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
if (latestWatches) {
|
|
1513
|
-
for (let watch of latestWatches.watchers) {
|
|
1514
|
-
triggerNodeChanged(watch);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
let parentPath = getParentPathStr(path);
|
|
1519
|
-
let latestParentWatches = this.parentWatchers.get(parentPath);
|
|
1520
|
-
if (latestParentWatches) {
|
|
1521
|
-
let pathRoutingHash: number | undefined = undefined;
|
|
1522
|
-
for (let { start, end, watchers } of latestParentWatches.values()) {
|
|
1523
|
-
if (!matchesParentRangeFilter({
|
|
1524
|
-
parentPath,
|
|
1525
|
-
fullPath: path,
|
|
1526
|
-
start,
|
|
1527
|
-
end,
|
|
1528
|
-
})) {
|
|
1529
|
-
continue;
|
|
1530
|
-
}
|
|
1531
|
-
for (let watch of watchers) {
|
|
1532
|
-
triggerNodeChanged(watch);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
for (let parentPath of config?.parentsSynced ?? []) {
|
|
1538
|
-
let latestParentWatches = this.parentWatchers.get(parentPath);
|
|
1539
|
-
if (latestParentWatches) {
|
|
1540
|
-
for (let { watchers } of latestParentWatches.values()) {
|
|
1541
|
-
for (let watcher of watchers) {
|
|
1542
|
-
let changes = changedPerCallbacks.get(watcher);
|
|
1543
|
-
if (!changes) {
|
|
1544
|
-
changes = new Set();
|
|
1545
|
-
changedPerCallbacks.set(watcher, changes);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
return changedPerCallbacks;
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
public isWatching(path: string): boolean {
|
|
1555
|
-
return this.watchers.has(path) || this.parentWatchers.has(getParentPathStr(path));
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
@measureFnc
|
|
1559
|
-
private triggerLatestWatcher(
|
|
1560
|
-
watcher: PathWatcherCallback,
|
|
1561
|
-
changes: PathValue[],
|
|
1562
|
-
parentPaths?: string[],
|
|
1563
|
-
initialTrigger?: "initialTrigger"
|
|
1564
|
-
) {
|
|
1565
|
-
if (isOwnNodeId(watcher)) {
|
|
1566
|
-
for (let callback of this.localTriggerCallbacks) {
|
|
1567
|
-
callback(changes, parentPaths ?? []);
|
|
1568
|
-
}
|
|
1569
|
-
} else {
|
|
1570
|
-
if (!isCoreQuiet) {
|
|
1571
|
-
console.log(`(${Date.now()}) Sending values to client: ${changes.length} (${watcher})`);
|
|
1572
|
-
}
|
|
1573
|
-
this.ensureUnwatchingOnDisconnect(watcher);
|
|
1574
|
-
ignoreErrors((async () => {
|
|
1575
|
-
let allowSource = await isNodeTrusted(watcher) || getNodeIdIP(watcher) === "127.0.0.1";
|
|
1576
|
-
let buffers = await pathValueSerializer.serialize(changes, {
|
|
1577
|
-
noLocks: true,
|
|
1578
|
-
compress: getCompressNetwork(),
|
|
1579
|
-
stripSource: !allowSource,
|
|
1580
|
-
});
|
|
1581
|
-
|
|
1582
|
-
if (isDebugLogEnabled() || isDiskAudit()) {
|
|
1583
|
-
for (let pathValue of changes) {
|
|
1584
|
-
auditLog("SEND VALUE", {
|
|
1585
|
-
path: pathValue.path,
|
|
1586
|
-
time: pathValue.time.time,
|
|
1587
|
-
watcher,
|
|
1588
|
-
nodeId: debugNodeId(watcher),
|
|
1589
|
-
targetNodeId: debugNodeId(watcher),
|
|
1590
|
-
targetNodeThreadId: debugNodeThread(watcher),
|
|
1591
|
-
transparent: pathValue.isTransparent,
|
|
1592
|
-
canGC: pathValue.canGCValue,
|
|
1593
|
-
});
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
await PathValueController.nodes[watcher].forwardWrites(
|
|
1597
|
-
buffers,
|
|
1598
|
-
parentPaths,
|
|
1599
|
-
undefined,
|
|
1600
|
-
initialTrigger,
|
|
1601
|
-
);
|
|
1602
|
-
})());
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
private ensureUnwatchingOnDisconnect = cache((nodeId: string) => {
|
|
1607
|
-
if (isOwnNodeId(nodeId)) return;
|
|
1608
|
-
SocketFunction.onNextDisconnect(nodeId, async () => {
|
|
1609
|
-
this.ensureUnwatchingOnDisconnect.clear(nodeId);
|
|
1610
|
-
|
|
1611
|
-
// Wait while, so Querysub doesn't thrash when clients connect and disconnect
|
|
1612
|
-
// TODO: We should be smarter about our wait time, waiting less if we have a lot of resource
|
|
1613
|
-
// pressure, and more (potentially forever), if we don't have much resource pressure
|
|
1614
|
-
// (memory, cpu, and network).
|
|
1615
|
-
setTimeout(() => {
|
|
1616
|
-
let watches = this.watchersToPaths.get(nodeId);
|
|
1617
|
-
this.watchersToPaths.delete(nodeId);
|
|
1618
|
-
if (watches) {
|
|
1619
|
-
this.unwatchPath({ paths: Array.from(watches.paths), parentPaths: Array.from(watches.parents), callback: nodeId });
|
|
1620
|
-
}
|
|
1621
|
-
}, MAX_CHANGE_AGE);
|
|
1622
|
-
});
|
|
1623
|
-
});
|
|
1624
|
-
|
|
1625
|
-
private localTriggerCallbacks = new Set<(changes: PathValue[], parentPaths: string[]) => void>();
|
|
1626
|
-
public watchAllLocalTriggers(callback: (changes: PathValue[], parentPaths: string[]) => void) {
|
|
1627
|
-
this.localTriggerCallbacks.add(callback);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
public unwatchedCallbacks = new Set<(config: WatchConfig) => void>();
|
|
1631
|
-
public watchUnwatched(callback: (config: WatchConfig) => void) {
|
|
1632
|
-
this.unwatchedCallbacks.add(callback);
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
public getAllParentWatches() {
|
|
1636
|
-
return Array.from(this.parentWatchers.keys());
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
public debug_harvestSyncTimes(): { start: number; end: number; path: string; }[] {
|
|
1640
|
-
let times = this.syncHistoryForHarvest.getAllUnordered();
|
|
1641
|
-
this.syncHistoryForHarvest.reset();
|
|
1642
|
-
return times;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
public debug_getSyncHistory(): { start: number; end: number; path: string; }[] {
|
|
1646
|
-
return this.syncHistoryForDebug.getAllUnordered();
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
export const pathWatcher = new PathWatcher();
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
class WriteValidStorage {
|
|
1653
|
-
// We could have a more efficient storage for non-range, but... we need range storage anyways,
|
|
1654
|
-
// so... this is fine.
|
|
1655
|
-
// (Not sorted)
|
|
1656
|
-
// - May have duplicates for the same path (for lockless writes), always with the same valid state.
|
|
1657
|
-
// path => WriteState[]
|
|
1658
|
-
// WriteState is UNSORTED (it is only kept around for MAX_CHANGE_AGE, so this should be the fastest way to do it)
|
|
1659
|
-
private validStorage = registerMapArrayResource("validStorage", new Map<string, WriteState[]>());
|
|
1660
|
-
|
|
1661
|
-
private delayedInvalidate = ((lock: ReadLock, targetTime: number, valueGroupForPredictionHack: PathValue[]) => {
|
|
1662
|
-
const tryNow = () => {
|
|
1663
|
-
if (Date.now() >= targetTime) {
|
|
1664
|
-
setTimeout(tryNow, Date.now() - targetTime + 50);
|
|
1665
|
-
return;
|
|
1666
|
-
}
|
|
1667
|
-
pathValueCommitter.ingestValidStates(valueGroupForPredictionHack.map(x => ({
|
|
1668
|
-
path: x.path,
|
|
1669
|
-
isValid: true,
|
|
1670
|
-
time: x.time,
|
|
1671
|
-
isTransparent: x.isTransparent || false,
|
|
1672
|
-
})), undefined, "recomputeValidState");
|
|
1673
|
-
};
|
|
1674
|
-
tryNow();
|
|
1675
|
-
});
|
|
1676
|
-
|
|
1677
|
-
public getWriteState(readLock: ReadLock, now: number): WriteState {
|
|
1678
|
-
let isValid = this.isLockValid(readLock, now);
|
|
1679
|
-
return { path: readLock.path, isValid, time: readLock.startTime, isTransparent: readLock.readIsTransparent };
|
|
1680
|
-
}
|
|
1681
|
-
public isLockValid(readLock: ReadLock, now: number, valueGroupForPredictionHack?: PathValue[]): boolean {
|
|
1682
|
-
let isValid = this.isLockValidBase(readLock, now);
|
|
1683
|
-
if (!isValid && readLock.keepRejectedUntil && now < readLock.keepRejectedUntil && valueGroupForPredictionHack) {
|
|
1684
|
-
isValid = true;
|
|
1685
|
-
// TODO: Test this code. We haven't run it, as it is a bit annoying to set up (we need to predict
|
|
1686
|
-
// writes to a new location). It will be pretty obvious if it is failing, but... I don't think it will?
|
|
1687
|
-
console.log(yellow(`Delaying rejection of ${readLock.path} to try to fix out of order prediction invalidation`));
|
|
1688
|
-
// Trigger a recheck when the keepRejectedUntil time is reached
|
|
1689
|
-
this.delayedInvalidate(readLock, readLock.keepRejectedUntil, valueGroupForPredictionHack);
|
|
1690
|
-
}
|
|
1691
|
-
return isValid;
|
|
1692
|
-
}
|
|
1693
|
-
public isLockValidBase(readLock: ReadLock, now: number): boolean {
|
|
1694
|
-
// If it old enough it must be valid, otherwise how would a client NOT know it was invalid
|
|
1695
|
-
// (assuming clients clear their reads aftering being disconnecting for long enough).
|
|
1696
|
-
if (readLock.startTime.time < now - MAX_CHANGE_AGE) {
|
|
1697
|
-
return true;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
let values = this.validStorage.get(readLock.path);
|
|
1701
|
-
let time = readLock.startTime;
|
|
1702
|
-
if (readLock.readIsTransparent) {
|
|
1703
|
-
// EDIT: Actually... isn't this done so that rejections can cascade from other nodes, with our node
|
|
1704
|
-
// not even evaluating the actual rejection reason, but just directly using the reject value?
|
|
1705
|
-
// // EDIT: I *think* that the range check should be sufficient for undefined read locks.
|
|
1706
|
-
// // If that is the case, remove isUndefinedAtLock entirely.
|
|
1707
|
-
// // - The reason the range check is good enough is that is is checking for values. Actually,
|
|
1708
|
-
// // if we always checked for values the range check would always be sufficient. HOWEVER,
|
|
1709
|
-
// // the range + valid check is an optimization, so locks don't need to store their values.
|
|
1710
|
-
// // But in this case, with the weird stuff we are doing with predictions... and the fact that
|
|
1711
|
-
// // we already store the value... we simply don't need the check.
|
|
1712
|
-
// return true;
|
|
1713
|
-
return this.isUndefinedAtLock(readLock);
|
|
1714
|
-
}
|
|
1715
|
-
if (!values) {
|
|
1716
|
-
// We have no record of any values on this path, so it must have never been committed!
|
|
1717
|
-
return false;
|
|
1718
|
-
}
|
|
1719
|
-
let value = values.find(x => compareTime(x.time, time) === 0);
|
|
1720
|
-
if (!value) {
|
|
1721
|
-
// If it is golden, it was probably just removed, so assume it was accepted
|
|
1722
|
-
// (nodes only ask for valid states of values they created, or that were
|
|
1723
|
-
// just created, so this is fine)
|
|
1724
|
-
if (time.time < now - MAX_CHANGE_AGE) {
|
|
1725
|
-
return true;
|
|
1726
|
-
}
|
|
1727
|
-
return false;
|
|
1728
|
-
}
|
|
1729
|
-
return value.isValid;
|
|
1730
|
-
}
|
|
1731
|
-
private isUndefinedAtLock(lock: ReadLock): boolean {
|
|
1732
|
-
let values = this.validStorage.get(lock.path);
|
|
1733
|
-
let time = lock.startTime;
|
|
1734
|
-
if (values) {
|
|
1735
|
-
// Find the write at read time
|
|
1736
|
-
let latestValue: WriteState | undefined;
|
|
1737
|
-
for (let value of values) {
|
|
1738
|
-
// Skip invalid
|
|
1739
|
-
if (!value.isValid) continue;
|
|
1740
|
-
// Skip writes AFTER our read (but not ON our read, as then we skip our read itself!)
|
|
1741
|
-
if (compareTime(value.time, time) > 0) continue;
|
|
1742
|
-
// Take the newest write that would affect us
|
|
1743
|
-
if (!latestValue || compareTime(value.time, latestValue.time) > 0) {
|
|
1744
|
-
latestValue = value;
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
if (latestValue && !latestValue.isTransparent) {
|
|
1748
|
-
return false;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
return true;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
private getWritesInRange(lock: ReadLock): WriteState[] | undefined {
|
|
1755
|
-
let values = this.validStorage.get(lock.path);
|
|
1756
|
-
if (!values) return undefined;
|
|
1757
|
-
let times: WriteState[] | undefined;
|
|
1758
|
-
for (let value of values) {
|
|
1759
|
-
if (!value.isValid) continue;
|
|
1760
|
-
// NOTE: This is the same comparison as in getValidStateChangedTriggers
|
|
1761
|
-
if (compareTime(lock.startTime, value.time) < 0 && compareTime(value.time, lock.endTime) < 0) {
|
|
1762
|
-
if (!times) times = [];
|
|
1763
|
-
times.push(value);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
return times;
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
// Returns true if the valid state changed
|
|
1770
|
-
public setWriteValidStateValue(pathValue: PathValue): boolean {
|
|
1771
|
-
this.ensureGarbageCollectOldState();
|
|
1772
|
-
|
|
1773
|
-
let time = pathValue.time;
|
|
1774
|
-
// Clone time, so we aren't referencing any of the original object. This is very important for garbage collection.
|
|
1775
|
-
time = { time: time.time, version: time.version, creatorId: time.creatorId, };
|
|
1776
|
-
return this.setWriteValidState({
|
|
1777
|
-
path: pathValue.path,
|
|
1778
|
-
isValid: pathValue.valid || false,
|
|
1779
|
-
time: time,
|
|
1780
|
-
isTransparent: pathValue.isTransparent || false,
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
public setWriteValidState(write: WriteState, lockless?: boolean): boolean {
|
|
1784
|
-
if (write.isValid) {
|
|
1785
|
-
auditLog("ACCEPTING VALUE", { path: write.path, time: write.time.time, reason: write.reason });
|
|
1786
|
-
} else {
|
|
1787
|
-
auditLog("REJECTING VALUE", { path: write.path, time: write.time.time, reason: write.reason });
|
|
1788
|
-
}
|
|
1789
|
-
this.ensureGarbageCollectOldState();
|
|
1790
|
-
|
|
1791
|
-
// If lockless then it starts valid, and always stays valid.
|
|
1792
|
-
if (!lockless) {
|
|
1793
|
-
let authorityValue = authorityStorage.getValueExactIgnoreInvalid(write.path, write.time);
|
|
1794
|
-
if (authorityValue) {
|
|
1795
|
-
authorityValue.valid = write.isValid;
|
|
1796
|
-
// NOTE: I think it is fine now for us to set the valid state early? Hopefully...
|
|
1797
|
-
// because it happens A LOT.
|
|
1798
|
-
// if (!isCoreQuiet) {
|
|
1799
|
-
// console.log(`Setting valid state of ${debugPathValuePath(authorityValue)} to ${write.isValid}`);
|
|
1800
|
-
// }
|
|
1801
|
-
} else {
|
|
1802
|
-
// if (isNode()) {
|
|
1803
|
-
// console.error(`Setting valid state of ${write.path}@${debugTime(write.time)} to ${write.isValid}, but the ValuePath was not found. If the ValuePath is found later, it might not have the valid state set correctly.`);
|
|
1804
|
-
// }
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
let path = write.path;
|
|
1809
|
-
let time = write.time;
|
|
1810
|
-
let isValid = write.isValid;
|
|
1811
|
-
let values = this.validStorage.get(path);
|
|
1812
|
-
if (!values) {
|
|
1813
|
-
values = [];
|
|
1814
|
-
this.validStorage.set(path, values);
|
|
1815
|
-
}
|
|
1816
|
-
// NOTE: We always have to search, even if lockless, so we can return false when the isValid state hasn't changed.
|
|
1817
|
-
// Search from the end, as it is most likely time will have been recently added
|
|
1818
|
-
let index = values.length - 1;
|
|
1819
|
-
while (index >= 0) {
|
|
1820
|
-
let diff = compareTime(values[index].time, time);
|
|
1821
|
-
if (diff === 0) {
|
|
1822
|
-
break;
|
|
1823
|
-
}
|
|
1824
|
-
index--;
|
|
1825
|
-
}
|
|
1826
|
-
if (index >= 0) {
|
|
1827
|
-
if (values[index].isValid === isValid) {
|
|
1828
|
-
return false;
|
|
1829
|
-
}
|
|
1830
|
-
values[index].isValid = isValid;
|
|
1831
|
-
values[index].reason = write.reason;
|
|
1832
|
-
} else {
|
|
1833
|
-
values.push(write);
|
|
1834
|
-
}
|
|
1835
|
-
return true;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
/** SOMEWHAT of a hack. Updates the inputs value states from cache. Needed, as it is possible for valid states
|
|
1840
|
-
to be processed before PathValues. Which would cause the PathValues to have outdated valid states when received.
|
|
1841
|
-
Valid states are always correct, and always updated (if they are sent at all), so we will use the latest valid
|
|
1842
|
-
state instead of latest write state.
|
|
1843
|
-
*/
|
|
1844
|
-
@measureFnc
|
|
1845
|
-
public updateValidStatesFromCache(values: PathValue[]) {
|
|
1846
|
-
for (let value of values) {
|
|
1847
|
-
let states = this.validStorage.get(value.path);
|
|
1848
|
-
if (!states) continue;
|
|
1849
|
-
let state = states.find(x => compareTime(x.time, value.time) === 0);
|
|
1850
|
-
if (!state) continue;
|
|
1851
|
-
value.valid = state.isValid;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
// NOTE: Mutates the input PathValue.valid states to be the new states
|
|
1856
|
-
// NOTE: Only mutates paths we are an authority on
|
|
1857
|
-
// NOTE: Also sets up watches, so the valid states will update when their readLocks change
|
|
1858
|
-
// (including talking to remote watchers, etc, etc)
|
|
1859
|
-
@measureFnc
|
|
1860
|
-
public computeValidStates(values: PathValue[], now: number, alreadyWatching?: "alreadyWatching"): WriteState[] {
|
|
1861
|
-
// Calculate valid states
|
|
1862
|
-
// - For things we are not an authority on, assumes their state is true.
|
|
1863
|
-
// - If we AREN'T an authority, whomever sent us the values will send us new
|
|
1864
|
-
// values if these become invalid.
|
|
1865
|
-
// - Also, if we created the values, we will get sent valid states if
|
|
1866
|
-
// they become invalid.
|
|
1867
|
-
// - We don't need to watch locks for pathValues, as any valid states changes
|
|
1868
|
-
// automatically impact any values that use them (we only need to watch locks
|
|
1869
|
-
// for values we are computing).
|
|
1870
|
-
// - For everything we ARE an authority on, computes them
|
|
1871
|
-
// - Also sets up watches for computed values, so the valid states will stay up to date).
|
|
1872
|
-
|
|
1873
|
-
let changes: WriteState[] = [];
|
|
1874
|
-
|
|
1875
|
-
// If alreadyWatching... then these are triggers caused by a local lock watch. In which case,
|
|
1876
|
-
// are must want to know if the values changed (as in, for call prediction).
|
|
1877
|
-
if (!alreadyWatching) {
|
|
1878
|
-
measureBlock(function computeValidStates_getSelfValues() {
|
|
1879
|
-
let selfValues: PathValue[] = [];
|
|
1037
|
+
public getAllValues(config: {
|
|
1038
|
+
spec: AuthoritySpec;
|
|
1039
|
+
// Only returns values with a create time in this window
|
|
1040
|
+
startTime: number;
|
|
1041
|
+
endTime: number;
|
|
1042
|
+
// Use pathValueSerializer.deserialize(valueBuffers) on the output buffers
|
|
1043
|
+
}): PathValue[] {
|
|
1044
|
+
let { spec, startTime, endTime } = config;
|
|
1045
|
+
let result: PathValue[] = [];
|
|
1046
|
+
for (let [path, values] of this.values) {
|
|
1047
|
+
if (PathRouter.matchesAuthoritySpec(spec, path)) {
|
|
1880
1048
|
for (let value of values) {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
let changed = writeValidStorage.setWriteValidStateValue(value);
|
|
1885
|
-
if (changed) {
|
|
1886
|
-
changes.push({ path: value.path, isValid: value.valid || false, time: value.time, isTransparent: value.isTransparent || false });
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
values = selfValues;
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
if (values.length === 0) return changes;
|
|
1895
|
-
|
|
1896
|
-
// Watch the value locks, for all SELF VALUES
|
|
1897
|
-
// (watches remote locks if necessary)
|
|
1898
|
-
if (!alreadyWatching) {
|
|
1899
|
-
lockWatcher.watchValueLocks(values);
|
|
1900
|
-
}
|
|
1901
|
-
let lockGroups = new Map<ReadLock[], PathValue[]>();
|
|
1902
|
-
for (let value of values) {
|
|
1903
|
-
if (value.lockCount === 0) {
|
|
1904
|
-
validateLockCount(value);
|
|
1905
|
-
let change: WriteState = {
|
|
1906
|
-
path: value.path,
|
|
1907
|
-
isValid: true,
|
|
1908
|
-
time: value.time,
|
|
1909
|
-
isTransparent: value.isTransparent || false
|
|
1910
|
-
};
|
|
1911
|
-
let changed = writeValidStorage.setWriteValidState(change, true);
|
|
1912
|
-
if (changed) {
|
|
1913
|
-
changes.push(change);
|
|
1914
|
-
}
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
let lockGroup = lockGroups.get(value.locks);
|
|
1918
|
-
if (!lockGroup) {
|
|
1919
|
-
lockGroup = [];
|
|
1920
|
-
lockGroups.set(value.locks, lockGroup);
|
|
1921
|
-
}
|
|
1922
|
-
lockGroup.push(value);
|
|
1923
|
-
}
|
|
1924
|
-
for (let [locks, valueGroup] of lockGroups) {
|
|
1925
|
-
if (valueGroup.length === 0) continue;
|
|
1926
|
-
|
|
1927
|
-
let rejected = false;
|
|
1928
|
-
for (let lock of locks) {
|
|
1929
|
-
if (!this.isLockValid(lock, now)) {
|
|
1930
|
-
rejected = true;
|
|
1931
|
-
|
|
1932
|
-
// This is good... unless it happens A LOT. Then the app needs to change (unless it
|
|
1933
|
-
// is a game, or something with realtime competition, in which case this is probably unavoidable).
|
|
1934
|
-
if ((!isCoreQuiet || !isNode()) && debugRejections || valueGroup.length > 0 && lock.endTime.version !== Number.MAX_SAFE_INTEGER && lock.startTime.version !== -2) {
|
|
1935
|
-
if (!isNode()) {
|
|
1936
|
-
debugger;
|
|
1937
|
-
}
|
|
1938
|
-
let timeToReject = now - valueGroup[0].time.time;
|
|
1939
|
-
let message = `!!! VALUE REJECTED DUE TO USING MISSING / REJECTED READ!!!(rejected after ${timeToReject}ms at ${Date.now()})`;
|
|
1940
|
-
message += `\n${debugTime(valueGroup[0].time)} (write)`;
|
|
1941
|
-
message += `\n rejected as the server could not find: `;
|
|
1942
|
-
if (lock.readIsTransparent) {
|
|
1943
|
-
message += `\n${debugTime(lock.startTime)} to ${debugTime(lock.endTime)} ${getPathFromStr(lock.path).join(".")} `;
|
|
1944
|
-
} else {
|
|
1945
|
-
message += `\n${debugTime(lock.startTime)} ${getPathFromStr(lock.path).join(".")} `;
|
|
1946
|
-
}
|
|
1947
|
-
if (lock.readIsTransparent) {
|
|
1948
|
-
message += `\n (read was undefined, so presumably a value exists which the writer missed)`;
|
|
1949
|
-
}
|
|
1950
|
-
message += `\nFull list of writes rejected: `;
|
|
1951
|
-
for (let pathValue of valueGroup) {
|
|
1952
|
-
message += `\n${debugPathValue(pathValue)}`;
|
|
1953
|
-
}
|
|
1954
|
-
console.log(red(message));
|
|
1955
|
-
|
|
1956
|
-
if (debugRejections) {
|
|
1957
|
-
debugbreak(2);
|
|
1958
|
-
debugger;
|
|
1959
|
-
this.isLockValid(lock, now);
|
|
1960
|
-
debugger;
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
break;
|
|
1964
|
-
}
|
|
1965
|
-
let inRange = this.getWritesInRange(lock);
|
|
1966
|
-
if (inRange?.length) {
|
|
1967
|
-
// If all the offending writes are undefined (or invalid), then we can ignore the contention
|
|
1968
|
-
if (lock.readIsTransparent && inRange.every(x => x.isTransparent || !x.isValid)) {
|
|
1969
|
-
continue;
|
|
1970
|
-
}
|
|
1971
|
-
rejected = true;
|
|
1972
|
-
// This is good... unless it happens A LOT. Then the app needs to change (unless it
|
|
1973
|
-
// is a game, or something with realtime competition, in which case this is probably unavoidable).
|
|
1974
|
-
if (
|
|
1975
|
-
(!isCoreQuiet || !isNode())
|
|
1976
|
-
// This special version indicates clientside prediction (which SHOULD be rejected).
|
|
1977
|
-
&& lock.endTime.version !== Number.MAX_SAFE_INTEGER
|
|
1978
|
-
|| debugRejections
|
|
1979
|
-
) {
|
|
1980
|
-
let changed = valueGroup.filter(x => x.valid);
|
|
1981
|
-
if (changed.length > 0) {
|
|
1982
|
-
let timeToReject = now - changed[0].time.time;
|
|
1983
|
-
let redMessage = `!!! LOCK CONTENTION FIXED VIA REJECTION OF VALUE!!!(rejected after ${timeToReject}ms)`;
|
|
1984
|
-
for (let pathValue of changed) {
|
|
1985
|
-
redMessage += `\n${debugPathValue(pathValue)}`;
|
|
1986
|
-
}
|
|
1987
|
-
redMessage += `\n (write rejected due to original read not noticing value at: ${lock.path})`;
|
|
1988
|
-
redMessage += `\n (original read from: ${debugTime(lock.startTime)}`;
|
|
1989
|
-
redMessage += `\n (at time: ${debugTime(lock.endTime)}`;
|
|
1990
|
-
redMessage += `\n (current time: ${Date.now()})`;
|
|
1991
|
-
for (let lockFailed of inRange) {
|
|
1992
|
-
redMessage += `\n (conflict write at: ${debugTime(lockFailed.time)}`;
|
|
1993
|
-
}
|
|
1994
|
-
console.log(red(redMessage));
|
|
1995
|
-
if (debugRejections) {
|
|
1996
|
-
debugbreak(2);
|
|
1997
|
-
debugger;
|
|
1998
|
-
this.getWritesInRange(lock);
|
|
1999
|
-
debugger;
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
break;
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
// NOTE: The if statement is equivalent to this one line, we just iterate so we can
|
|
2007
|
-
// have better debug info, and rejection metrics tracking.
|
|
2008
|
-
//values.forEach(value => value.valid = !rejected);
|
|
2009
|
-
if (rejected) {
|
|
2010
|
-
for (let pathValue of valueGroup) {
|
|
2011
|
-
pathValue.valid = false;
|
|
2012
|
-
rejections.value++;
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
for (let value of valueGroup) {
|
|
2017
|
-
let change: WriteState = {
|
|
2018
|
-
path: value.path,
|
|
2019
|
-
isValid: value.valid || false,
|
|
2020
|
-
time: value.time,
|
|
2021
|
-
isTransparent: value.isTransparent || false
|
|
2022
|
-
};
|
|
2023
|
-
let changed = writeValidStorage.setWriteValidState(change);
|
|
2024
|
-
if (changed) {
|
|
2025
|
-
changes.push(change);
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
return changes;
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
public deleteRemovedPath(path: string) {
|
|
2034
|
-
this.validStorage.delete(path);
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
private ensureGarbageCollectOldState = lazy(() => runInfinitePoll(MAX_CHANGE_AGE, () => {
|
|
2038
|
-
// Clear all valid states after they are old enough. We will just tell anyone who asks that
|
|
2039
|
-
// they are valid, because... the must be if someone still cares about them (as either they have been
|
|
2040
|
-
// syncing for a long time, and so they must have never gotten an invalid message and it is too late
|
|
2041
|
-
// for that to change now, or... they are new, which means they will either be given old valid values,
|
|
2042
|
-
// or new values which may be invalid but are too new for us to garbage collect).
|
|
2043
|
-
let garbageCollectTime = Date.now() - MAX_CHANGE_AGE;
|
|
2044
|
-
for (let [key, values] of this.validStorage) {
|
|
2045
|
-
values = values.filter(x => x.time.time > garbageCollectTime);
|
|
2046
|
-
if (values.length === 0) {
|
|
2047
|
-
this.validStorage.delete(key);
|
|
2048
|
-
} else {
|
|
2049
|
-
this.validStorage.set(key, values);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
}));
|
|
2053
|
-
}
|
|
2054
|
-
export const writeValidStorage = new WriteValidStorage();
|
|
2055
|
-
|
|
2056
|
-
let rejections = { value: 0 };
|
|
2057
|
-
export function internalGetRejections(): { value: number } {
|
|
2058
|
-
return rejections;
|
|
2059
|
-
}
|
|
2060
|
-
export function internalTestResetRejections() {
|
|
2061
|
-
rejections.value = 0;
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
class LockWatcher {
|
|
2065
|
-
@measureFnc
|
|
2066
|
-
public watchValueLocks(values: PathValue[]): void {
|
|
2067
|
-
let { newRemoteLocks } = lockWatchDeduper.localWatchNewLocks({ values: values });
|
|
2068
|
-
if (newRemoteLocks.size > 0) {
|
|
2069
|
-
logErrors(this.watchRemoteLocks(newRemoteLocks));
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
private watchRemoteLocks = batchFunction(
|
|
2073
|
-
{ delay: "afterio" },
|
|
2074
|
-
async (locksBatched: Set<ReadLock>[]) => {
|
|
2075
|
-
let locks = new Set(locksBatched.flatMap(x => Array.from(x)));
|
|
2076
|
-
let locksByAuthority = new Map<string, Set<ReadLock>>();
|
|
2077
|
-
for (let lock of locks) {
|
|
2078
|
-
if (pathValueAuthority2.isSelfAuthority(lock.path)) continue;
|
|
2079
|
-
|
|
2080
|
-
let authorityId = await pathValueAuthority2.getSingleReadNodePromise(lock.path);
|
|
2081
|
-
// If they don't trust us, we just have to assume the value we read is correct.
|
|
2082
|
-
if (!await isTrustedByNode(authorityId)) continue;
|
|
2083
|
-
let locks = locksByAuthority.get(authorityId);
|
|
2084
|
-
if (!locks) {
|
|
2085
|
-
locks = new Set();
|
|
2086
|
-
locksByAuthority.set(authorityId, locks);
|
|
2087
|
-
}
|
|
2088
|
-
locks.add(lock);
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
for (let [authorityId, locks] of locksByAuthority) {
|
|
2092
|
-
this.watchLocksOnAuthority(authorityId, locks);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
);
|
|
2096
|
-
private watchLocksOnAuthority(authorityId: string, locks: Set<ReadLock>) {
|
|
2097
|
-
if (!Array.from(locks).some(x => lockWatchDeduper.isLockStillWatched(x))) return;
|
|
2098
|
-
|
|
2099
|
-
let alreadyReconnected = false;
|
|
2100
|
-
const reconnectWatches = () => {
|
|
2101
|
-
if (alreadyReconnected) return;
|
|
2102
|
-
alreadyReconnected = true;
|
|
2103
|
-
if (!Array.from(locks).some(x => lockWatchDeduper.isLockStillWatched(x))) return;
|
|
2104
|
-
logErrors(this.watchRemoteLocks(locks));
|
|
2105
|
-
};
|
|
2106
|
-
this.onNextDisconnectList(authorityId).add(reconnectWatches);
|
|
2107
|
-
setTimeout(() => {
|
|
2108
|
-
this.onNextDisconnectList(authorityId).delete(reconnectWatches);
|
|
2109
|
-
}, MAX_CHANGE_AGE * 2);
|
|
2110
|
-
|
|
2111
|
-
let connection = PathValueController.nodes[authorityId].watchLockValid(
|
|
2112
|
-
Array.from(locks.values())
|
|
2113
|
-
);
|
|
2114
|
-
logErrors(connection);
|
|
2115
|
-
connection.catch(() => reconnectWatches());
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
private onNextDisconnectList = cache((nodeId: string) => {
|
|
2119
|
-
let callbacks = new Set<() => void>();
|
|
2120
|
-
SocketFunction.onNextDisconnect(nodeId, () => {
|
|
2121
|
-
this.onNextDisconnectList.clear(nodeId);
|
|
2122
|
-
for (let callback of callbacks) {
|
|
2123
|
-
callback();
|
|
2124
|
-
}
|
|
2125
|
-
});
|
|
2126
|
-
return callbacks;
|
|
2127
|
-
});
|
|
2128
|
-
}
|
|
2129
|
-
export const lockWatcher = new LockWatcher();
|
|
2130
|
-
|
|
2131
|
-
class LockToCallbackLookup {
|
|
2132
|
-
// time.creatorId => time => callbacks
|
|
2133
|
-
private validWatchers = registerResource("paths|validWatchers", new Map<number, Map<number, Set<WriteCallback>>>());
|
|
2134
|
-
|
|
2135
|
-
// path => sorted by endTime
|
|
2136
|
-
private validRangeWatchers = registerResource("paths|validRangeWatchers", new Map<string, {
|
|
2137
|
-
startTime: Time;
|
|
2138
|
-
endTime: Time;
|
|
2139
|
-
// Called back if there is a valid value between startTime (exclusive) and endTime (exclusive)
|
|
2140
|
-
callback: WriteCallback;
|
|
2141
|
-
}[]>());
|
|
2142
|
-
|
|
2143
|
-
/** Watchs both the startTime, and the range */
|
|
2144
|
-
public watchLock(lock: ReadLock, callback: WriteCallback) {
|
|
2145
|
-
this.ensureGarbageCollectLoop();
|
|
2146
|
-
// Add valid watcher
|
|
2147
|
-
if (lock.endTime.time > 0) {
|
|
2148
|
-
const time = lock.startTime;
|
|
2149
|
-
let maps = this.validWatchers.get(time.creatorId);
|
|
2150
|
-
if (!maps) {
|
|
2151
|
-
maps = new Map();
|
|
2152
|
-
this.validWatchers.set(time.creatorId, maps);
|
|
2153
|
-
}
|
|
2154
|
-
let callbacksList = maps.get(time.time);
|
|
2155
|
-
if (!callbacksList) {
|
|
2156
|
-
callbacksList = new Set();
|
|
2157
|
-
maps.set(time.time, callbacksList);
|
|
2158
|
-
}
|
|
2159
|
-
callbacksList.add(callback);
|
|
2160
|
-
}
|
|
2161
|
-
// Add valid range watcher
|
|
2162
|
-
if (compareTime(lock.startTime, lock.endTime) !== 0) {
|
|
2163
|
-
let { path, startTime, endTime } = lock;
|
|
2164
|
-
let watchers = this.validRangeWatchers.get(path);
|
|
2165
|
-
if (!watchers) {
|
|
2166
|
-
watchers = [];
|
|
2167
|
-
this.validRangeWatchers.set(path, watchers);
|
|
2168
|
-
}
|
|
2169
|
-
// Find the index to insert into, maintaining a sort by time
|
|
2170
|
-
let index = -1;
|
|
2171
|
-
for (let i = watchers.length - 1; i >= 0; i--) {
|
|
2172
|
-
if (watchers[i].endTime.time < endTime.time) {
|
|
2173
|
-
index = i + 1;
|
|
2174
|
-
break;
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
if (index === -1) {
|
|
2178
|
-
index = watchers.length;
|
|
2179
|
-
}
|
|
2180
|
-
watchers.splice(index, 0, { startTime, endTime, callback });
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
public garbageCollectOldWatchers() {
|
|
2185
|
-
const disposeTime = Date.now() - MAX_CHANGE_AGE * 2;
|
|
2186
|
-
|
|
2187
|
-
for (let [path, times] of this.validRangeWatchers) {
|
|
2188
|
-
let expiredIndex = times.findIndex(x => x.endTime.time < disposeTime);
|
|
2189
|
-
if (expiredIndex >= 0) {
|
|
2190
|
-
times.splice(expiredIndex, times.length - expiredIndex);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
for (let [key, maps] of this.validWatchers) {
|
|
2195
|
-
for (let time of maps.keys()) {
|
|
2196
|
-
if (time < disposeTime) {
|
|
2197
|
-
maps.delete(time);
|
|
2198
|
-
if (maps.size === 0) {
|
|
2199
|
-
this.validWatchers.delete(key);
|
|
1049
|
+
let time = value.time.time;
|
|
1050
|
+
if (startTime <= time && time <= endTime) {
|
|
1051
|
+
result.push(value);
|
|
2200
1052
|
}
|
|
2201
1053
|
}
|
|
2202
1054
|
}
|
|
2203
1055
|
}
|
|
2204
|
-
|
|
2205
|
-
for (let [path, watchers] of this.validRangeWatchers) {
|
|
2206
|
-
for (let i = watchers.length - 1; i >= 0; i--) {
|
|
2207
|
-
let watcher = watchers[i];
|
|
2208
|
-
if (watcher.endTime.time < disposeTime) {
|
|
2209
|
-
watchers.splice(i, 1);
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
if (watchers.length === 0) {
|
|
2213
|
-
this.validRangeWatchers.delete(path);
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
1056
|
+
return result;
|
|
2216
1057
|
}
|
|
2217
|
-
|
|
2218
|
-
public getValidStateChangedTriggers(
|
|
2219
|
-
remoteValue: WriteState
|
|
2220
|
-
): WriteCallback[] {
|
|
2221
|
-
let allCallbacks: WriteCallback[] = [];
|
|
2222
|
-
let validMaps = this.validWatchers.get(remoteValue.time.creatorId);
|
|
2223
|
-
if (validMaps) {
|
|
2224
|
-
let callbacks = validMaps.get(remoteValue.time.time);
|
|
2225
|
-
if (callbacks) {
|
|
2226
|
-
for (let callbackList of callbacks) {
|
|
2227
|
-
allCallbacks.push(callbackList);
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
let rangeWatchers = this.validRangeWatchers.get(remoteValue.path);
|
|
2232
|
-
if (rangeWatchers) {
|
|
2233
|
-
for (let watcher of rangeWatchers) {
|
|
2234
|
-
if (
|
|
2235
|
-
// NOTE: This is the same comparison as in getWritesInRange
|
|
2236
|
-
// If the time is > startTime (exclusive, as startTime MUST exist, as we depend on it)
|
|
2237
|
-
compareTime(watcher.startTime, remoteValue.time) < 0
|
|
2238
|
-
// And < endTime (exclusive, as endTime is assumed to exist, as it is probably when we are writing!)
|
|
2239
|
-
&& compareTime(remoteValue.time, watcher.endTime) < 0
|
|
2240
|
-
) {
|
|
2241
|
-
allCallbacks.push(watcher.callback);
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
return allCallbacks;
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
private ensureGarbageCollectLoop = lazy(() => {
|
|
2249
|
-
runInfinitePoll(MAX_CHANGE_AGE, () => lockToCallback.garbageCollectOldWatchers());
|
|
2250
|
-
});
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
1058
|
}
|
|
2254
|
-
export const
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
// there will be no equivalent but !== Locks. Fair assumptions, which save so
|
|
2258
|
-
// much time it is probably worth maintaining them. AND, if they are invalidated,
|
|
2259
|
-
// the only penalty is a memory leak, which isn't so bad (arguably having identical
|
|
2260
|
-
// values with difference instances is a memory leak too).
|
|
2261
|
-
class LockWatchDeduper {
|
|
2262
|
-
private watchedRemoteLocks = registerResource("paths|watchedRemoteLocks", new Map<ReadLock[], Set<PathValue>>());
|
|
2263
|
-
private watchedLockFlat = registerResource("paths|watchedLockFlat", new Set<ReadLock>());
|
|
2264
|
-
|
|
2265
|
-
/** Calls validWatcher with any new readLocks, using the values as the callback.
|
|
2266
|
-
* SHOULD ONLY BE USED IF YOU ARE WATCHING THE LOCKS, aka, if you are
|
|
2267
|
-
* the authority on the values. Otherwise, you are NOT watching readLocks,
|
|
2268
|
-
* you are watching the valid state, via the actual authority of the paths.
|
|
2269
|
-
*/
|
|
2270
|
-
public localWatchNewLocks(config: { values: PathValue[]; }): {
|
|
2271
|
-
newRemoteLocks: Set<ReadLock>;
|
|
2272
|
-
} {
|
|
2273
|
-
this.ensureLockCleanupLoop();
|
|
2274
|
-
|
|
2275
|
-
let byLock = new Map<ReadLock[], PathValue[]>();
|
|
2276
|
-
for (let value of config.values) {
|
|
2277
|
-
validateLockCount(value);
|
|
2278
|
-
if (value.lockCount === 0) continue;
|
|
2279
|
-
let values = byLock.get(value.locks);
|
|
2280
|
-
if (!values) {
|
|
2281
|
-
values = [];
|
|
2282
|
-
byLock.set(value.locks, values);
|
|
2283
|
-
}
|
|
2284
|
-
values.push(value);
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
let newLocks: ReadLock[] = [];
|
|
2288
|
-
for (let value of config.values) {
|
|
2289
|
-
validateLockCount(value);
|
|
2290
|
-
if (value.lockCount === 0) continue;
|
|
2291
|
-
let values = this.watchedRemoteLocks.get(value.locks);
|
|
2292
|
-
if (!values) {
|
|
2293
|
-
values = new Set();
|
|
2294
|
-
this.watchedRemoteLocks.set(value.locks, values);
|
|
2295
|
-
newLocks.push(...value.locks);
|
|
2296
|
-
}
|
|
2297
|
-
values.add(value);
|
|
2298
|
-
}
|
|
2299
|
-
for (let locks of byLock.keys()) {
|
|
2300
|
-
for (let lock of locks) {
|
|
2301
|
-
this.watchedLockFlat.add(lock);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
let newRemoteLocks = new Set<ReadLock>();
|
|
2305
|
-
|
|
2306
|
-
for (let [locks, values] of byLock) {
|
|
2307
|
-
for (let lock of locks) {
|
|
2308
|
-
// NOTE: We watch LOCAL_DOMAIN paths, as they might depend on a remote value, which is invalidated.
|
|
2309
|
-
// This can happen for routing, where you click something like "go to main node", before
|
|
2310
|
-
// the remote node list is loaded. You will go to the wrong page, BUT, once the remote node list
|
|
2311
|
-
// is loaded, it should reject (and then, ideally we rerun the function clientside on detection
|
|
2312
|
-
// of the rejection).
|
|
2313
|
-
|
|
2314
|
-
if (!pathValueAuthority2.isSelfAuthority(lock.path)) {
|
|
2315
|
-
newRemoteLocks.add(lock);
|
|
2316
|
-
}
|
|
2317
|
-
// NOTE: I BELIEVE this is valid, even if we don't own the locks. We might have some
|
|
2318
|
-
// unnecessary cascading, but... if we depended on a value we will have it, and
|
|
2319
|
-
// if we are missing any range lock conflicts we will know about them due to
|
|
2320
|
-
// the remote locks. AND, any values we have are synced, so we don't need to worry about
|
|
2321
|
-
// any range lock conflicts themselves being rejected, missing that rejection, and so
|
|
2322
|
-
// incorrectly invalidating valid values that we just received.
|
|
2323
|
-
lockToCallback.watchLock(lock, values);
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
return { newRemoteLocks };
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
private ensureLockCleanupLoop = lazy(() => {
|
|
2331
|
-
runInfinitePoll(MAX_CHANGE_AGE, () => this.cleanupDeadLocks());
|
|
2332
|
-
});
|
|
1059
|
+
export const authorityStorage = new AuthorityPathValueStorage();
|
|
1060
|
+
export const values = authorityStorage;
|
|
1061
|
+
export const pathValues = authorityStorage;
|
|
2333
1062
|
|
|
2334
|
-
@measureFnc
|
|
2335
|
-
private cleanupDeadLocks() {
|
|
2336
|
-
let deadTime = Date.now() - MAX_CHANGE_AGE;
|
|
2337
|
-
for (let [locks, values] of this.watchedRemoteLocks) {
|
|
2338
|
-
let isDead = true;
|
|
2339
|
-
for (let value of values) {
|
|
2340
|
-
if (value.time.time > deadTime) {
|
|
2341
|
-
isDead = false;
|
|
2342
|
-
break;
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
if (isDead) {
|
|
2346
|
-
for (let lock of locks) {
|
|
2347
|
-
this.watchedLockFlat.delete(lock);
|
|
2348
|
-
}
|
|
2349
|
-
this.watchedRemoteLocks.delete(locks);
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
1063
|
|
|
2354
|
-
public isLockStillWatched(lock: ReadLock): boolean {
|
|
2355
|
-
return this.watchedLockFlat.has(lock);
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
export const lockWatchDeduper = new LockWatchDeduper();
|
|
2359
1064
|
|
|
2360
1065
|
// Async import, as we are not allowed synchronous imports of values in
|
|
2361
1066
|
// higher tiers than us. We can't expose a callback, as nothing else
|
|
2362
1067
|
// can trigger the import of memoryValueAudit for all clients as consistently
|
|
2363
1068
|
// as the core.
|
|
2364
1069
|
setImmediate(() => {
|
|
2365
|
-
//logErrors(import("../5-diagnostics/memoryValueAudit").then(x => x.startMemoryAuditLoop()));
|
|
2366
|
-
//logErrors(import("../5-diagnostics/diskValueAudit").then(x => x.startDiskAuditLoop()));
|
|
2367
1070
|
logErrors(import("../5-diagnostics/synchronousLagTracking").then(x => x.trackSynchronousLag()));
|
|
2368
1071
|
});
|
|
2369
1072
|
|