querysub 0.462.0 → 0.463.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/-d-trust/NetworkTrust2.ts +1 -1
- package/src/0-path-value-core/LockWatcher2.ts +2 -5
- package/src/0-path-value-core/PathRouter.ts +2 -3
- package/src/0-path-value-core/PathRouterConstants.ts +4 -0
- package/src/0-path-value-core/PathValueCommitter.ts +0 -1
- package/src/0-path-value-core/PathValueController.ts +1 -1
- package/src/0-path-value-core/PathWatcher.ts +14 -5
- package/src/0-path-value-core/ValidStateComputer.ts +234 -86
- package/src/0-path-value-core/pathValueCore.ts +57 -78
- package/src/1-path-client/RemoteWatcher.ts +2 -1
- package/src/1-path-client/pathValueClientWatcher.ts +28 -2
- package/src/2-proxy/PathValueProxyWatcher.ts +29 -24
- package/src/2-proxy/TransactionDelayer.ts +44 -22
- package/src/2-proxy/archiveMoveHarness.ts +2 -2
- package/src/3-path-functions/PathFunctionRunner.ts +30 -22
- package/src/3-path-functions/PathFunctionRunnerMain.ts +1 -3
- package/src/4-deploy/deployFunctions.ts +1 -1
- package/src/4-deploy/deployMain.ts +1 -1
- package/src/4-deploy/edgeClientWatcher.tsx +1 -1
- package/src/4-deploy/edgeNodes.ts +1 -1
- package/src/4-dom/qreactTest.tsx +1 -1
- package/src/4-querysub/Querysub.ts +8 -9
- package/src/4-querysub/QuerysubController.ts +19 -1
- package/src/4-querysub/permissions.ts +1 -1
- package/src/4-querysub/predictionQueue.tsx +1 -1
- package/src/4-querysub/querysubPrediction.ts +25 -12
- package/src/5-diagnostics/GenericFormat.tsx +1 -1
- package/src/5-diagnostics/qreactDebug.tsx +2 -2
- package/src/archiveapps/archiveGCEntry.tsx +1 -1
- package/src/archiveapps/archiveJoinEntry.ts +3 -3
- package/src/config.ts +5 -1
- package/src/config2.ts +9 -7
- package/src/deployManager/components/CommitModal.tsx +1 -1
- package/src/deployManager/components/DeployProgressView.tsx +1 -1
- package/src/deployManager/components/MachineDetailPage.tsx +1 -1
- package/src/deployManager/components/MachinesListPage.tsx +1 -1
- package/src/deployManager/components/ServiceDetailPage.tsx +1 -1
- package/src/deployManager/components/ServicesListPage.tsx +1 -1
- package/src/deployManager/components/Tools.tsx +1 -1
- package/src/deployManager/machineApplyMainCode.ts +1 -1
- package/src/deployManager/machineController.ts +1 -1
- package/src/deployManager/machineSchema.ts +1 -1
- package/src/deployManager/setupMachineMain.ts +1 -1
- package/src/diagnostics/FunctionCallInfoState.ts +6 -3
- package/src/diagnostics/MachineThreadInfo.tsx +1 -1
- package/src/diagnostics/NodeViewer.tsx +2 -1
- package/src/diagnostics/StatWarning.tsx +56 -0
- package/src/diagnostics/StatsHeader.tsx +248 -0
- package/src/diagnostics/StatsOverrides.ts +50 -0
- package/src/diagnostics/SyncTestPage.tsx +3 -3
- package/src/diagnostics/TimeDebug.tsx +1 -1
- package/src/diagnostics/debugger/mcp-server.ts +1 -1
- package/src/diagnostics/grossStats/GrossStatsPage.tsx +1 -1
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +1 -1
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +1 -1
- package/src/diagnostics/logs/TimeRangeSelector.tsx +1 -1
- package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -1
- package/src/diagnostics/logs/errorNotifications2/ErrorWarning.tsx +0 -1
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +1 -1
- package/src/diagnostics/logs/errorNotifications2/errorWatchEntry.ts +1 -1
- package/src/diagnostics/logs/errorNotifications2/errorWatcher.ts +1 -1
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +1 -1
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +1 -1
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +1 -1
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +1 -1
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +1 -1
- package/src/diagnostics/managementPages.tsx +4 -9
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +1 -1
- package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +1 -1
- package/src/diagnostics/misc-pages/DNSPage.tsx +1 -1
- package/src/diagnostics/pathAuditer.ts +6 -6
- package/src/diagnostics/statsDefinitions.tsx +253 -0
- package/src/functional/throttleRender.ts +1 -1
- package/src/library-components/AspectSizedComponent.tsx +1 -1
- package/src/library-components/Button.tsx +1 -1
- package/src/library-components/ButtonSelector.tsx +1 -1
- package/src/library-components/DropdownCustom.tsx +1 -1
- package/src/library-components/DropdownSelector.tsx +1 -1
- package/src/library-components/Histogram.tsx +1 -1
- package/src/library-components/InlinePopup.tsx +1 -1
- package/src/library-components/LazyComponent.tsx +5 -1
- package/src/library-components/StickyBottomScroll.tsx +1 -1
- package/src/library-components/TypedConfigEditor.tsx +1 -1
- package/src/library-components/URLParam.ts +5 -9
- package/src/library-components/drag.ts +1 -1
- package/src/misc/formatJSX.tsx +1 -1
- package/src/user-implementation/userData.ts +1 -1
- package/test2.ts +1 -1
- package/testEntry2.ts +1 -1
- package/valid.md +205 -0
- package/src/deployManager/LaunchTrackingHeader.tsx +0 -65
- package/src/diagnostics/FunctionCallInfo.tsx +0 -141
- package/src/diagnostics/PathDistributionInfo.tsx +0 -110
- package/src/diagnostics/ValuePathWarning.tsx +0 -68
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ import debugbreak from "debugbreak";
|
|
|
13
13
|
import { devDebugbreak, getDomain, isDevDebugbreak, isPublic, isRecovery } from "../config";
|
|
14
14
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
15
15
|
import { runInSerial } from "socket-function/src/batching";
|
|
16
|
-
import { Querysub } from "../4-querysub/
|
|
16
|
+
import { Querysub } from "../4-querysub/Querysub";
|
|
17
17
|
import { magenta } from "socket-function/src/formatting/logColors";
|
|
18
18
|
|
|
19
19
|
// Cache the untrust list, to prevent bugs from causing too many backend reads (while also allowing
|
|
@@ -6,10 +6,6 @@ import { MAX_CHANGE_AGE, Time, PathValue, byLockGroup, compareTime } from "./pat
|
|
|
6
6
|
import { PathRouter } from "./PathRouter";
|
|
7
7
|
import { getInternalValueWatchPrefix, pathWatcher } from "./PathWatcher";
|
|
8
8
|
|
|
9
|
-
function isGoldenLock(time: number, now: number): boolean {
|
|
10
|
-
return time < now - MAX_CHANGE_AGE * 2;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
9
|
function getLockWatcher2NodeId() {
|
|
14
10
|
return getInternalValueWatchPrefix() + "_LockWatcher2";
|
|
15
11
|
}
|
|
@@ -63,8 +59,9 @@ class LockWatcher2 {
|
|
|
63
59
|
if (values.length === 0) return;
|
|
64
60
|
this.ensureGarbageCollectLoop();
|
|
65
61
|
let lockGroups = byLockGroup(values);
|
|
62
|
+
let threshold = now - MAX_CHANGE_AGE;
|
|
66
63
|
for (let [locks, valueGroup] of lockGroups) {
|
|
67
|
-
locks = locks.filter(x =>
|
|
64
|
+
locks = locks.filter(x => x.endTime.time > threshold);
|
|
68
65
|
if (locks.length === 0) continue;
|
|
69
66
|
for (let lock of locks) {
|
|
70
67
|
if (!PathRouter.isSelfAuthority(lock.path)) {
|
|
@@ -13,6 +13,8 @@ import { rangesOverlap, removeRange } from "../rangeMath";
|
|
|
13
13
|
import { decodeParentFilter } from "./hackedPackedPathParentFiltering";
|
|
14
14
|
import { getBufferInt } from "../bits";
|
|
15
15
|
|
|
16
|
+
import { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH } from "./PathRouterConstants";
|
|
17
|
+
export { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH };
|
|
16
18
|
|
|
17
19
|
// Cases
|
|
18
20
|
// 1) Whole path hash
|
|
@@ -22,9 +24,6 @@ import { getBufferInt } from "../bits";
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
|
|
25
|
-
export const LOCAL_DOMAIN = "LOCAL";
|
|
26
|
-
export const LOCAL_DOMAIN_PATH = getPathStr1(LOCAL_DOMAIN);
|
|
27
|
-
|
|
28
27
|
// NOTE: The goal is for a user on the site to require talking to as few authorities as possible. Because most things are nested in lookups, if we have a prefix per root lookup, this should mean if you're on a page, most of the data should be talking to the same authority, as most of the data should have the exact same hash.
|
|
29
28
|
// - We also support the exclude default case, which allows you to only handle certain prefixes, so you can make a function only run on certain servers.
|
|
30
29
|
export type AuthoritySpec = {
|
|
@@ -123,7 +123,6 @@ class PathValueCommitter {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// NOTE: This function will just ignore writes we aren't an authority on, so we can just give it everything.
|
|
127
126
|
validStateComputer.ingestValuesAndValidStates({
|
|
128
127
|
pathValues: pathValuesToIngest,
|
|
129
128
|
parentSyncs: [],
|
|
@@ -323,7 +323,7 @@ export class PathValueControllerBase {
|
|
|
323
323
|
public async getValuesByPathAndTime(entries: AuditSnapshotEntry[]): Promise<Buffer[]> {
|
|
324
324
|
let values: PathValue[] = [];
|
|
325
325
|
for (let entry of entries) {
|
|
326
|
-
let value = authorityStorage.
|
|
326
|
+
let value = authorityStorage.getValueExactMaybeRejected(entry.path, entry.time);
|
|
327
327
|
if (!value) continue;
|
|
328
328
|
if (!value.valid) continue;
|
|
329
329
|
if (value.isTransparent) continue;
|
|
@@ -87,6 +87,9 @@ class PathWatcher {
|
|
|
87
87
|
parents: Set<string>;
|
|
88
88
|
fullHistory?: boolean;
|
|
89
89
|
}>());
|
|
90
|
+
public debugGetWatcherPaths(nodeId: string) {
|
|
91
|
+
return this.watchersToPaths.get(nodeId);
|
|
92
|
+
}
|
|
90
93
|
|
|
91
94
|
/** Automatically watches the remote if it's a remote path.
|
|
92
95
|
* Automatically unwatches the remote when the paths no longer have any callbacks.
|
|
@@ -346,7 +349,7 @@ class PathWatcher {
|
|
|
346
349
|
valuesChanged: Set<PathValue>;
|
|
347
350
|
|
|
348
351
|
// Mutates initialTriggers.values
|
|
349
|
-
initialTriggers: { values: Set<string>; parentPaths: Set<string> };
|
|
352
|
+
initialTriggers: { values: Set<string>; parentPaths: Set<string>; initialTriggerNonHistoryWatchers?: Set<string> };
|
|
350
353
|
|
|
351
354
|
onlyTriggerNodeId?: string;
|
|
352
355
|
}) {
|
|
@@ -365,6 +368,9 @@ class PathWatcher {
|
|
|
365
368
|
for (let path of initialTriggers?.values ?? []) {
|
|
366
369
|
triggerPaths.add(path);
|
|
367
370
|
}
|
|
371
|
+
for (let path of initialTriggers?.initialTriggerNonHistoryWatchers ?? []) {
|
|
372
|
+
triggerPaths.add(path);
|
|
373
|
+
}
|
|
368
374
|
|
|
369
375
|
let valuePerPath = keyByArray(Array.from(valuesChanged), x => x.path);
|
|
370
376
|
|
|
@@ -409,6 +415,7 @@ class PathWatcher {
|
|
|
409
415
|
|
|
410
416
|
// Important. We can't do the initial trigger if we aren't synced. If we're the authority, this doesn't matter. This is only important for proxy watchers.
|
|
411
417
|
let isInitialTrigger = !!initialTriggers?.values.has(path) && authorityStorage.isSynced(path);
|
|
418
|
+
let isInitialTriggerNonHistory = !!initialTriggers?.initialTriggerNonHistoryWatchers?.has(path);
|
|
412
419
|
const triggerNodeChanged = (watcher: NodeId) => {
|
|
413
420
|
if (onlyTriggerNodeId && watcher !== onlyTriggerNodeId) return;
|
|
414
421
|
let changes = changedPerCallbacks.get(watcher);
|
|
@@ -421,12 +428,14 @@ class PathWatcher {
|
|
|
421
428
|
for (let value of values) {
|
|
422
429
|
changes.values.add(value);
|
|
423
430
|
}
|
|
424
|
-
|
|
431
|
+
let watcherObj = this.watchersToPaths.get(watcher);
|
|
432
|
+
let fullHistory = watcherObj?.fullHistory;
|
|
433
|
+
if (isInitialTrigger || isInitialTriggerNonHistory && !fullHistory) {
|
|
425
434
|
changes.initialTriggers.values.add(path);
|
|
426
|
-
let watcherObj = this.watchersToPaths.get(watcher);
|
|
427
|
-
let fullHistory = watcherObj?.fullHistory;
|
|
428
435
|
if (fullHistory) {
|
|
429
436
|
let history = authorityStorage.getValuePlusHistory(path);
|
|
437
|
+
// There's no point in sending them invalid values. This is going to clobber any existing values. So if they had a value that was valid and it's now invalid, if we just don't send it, they'll remove it (and removal literally means setting the valid state to false, so it's identical).
|
|
438
|
+
history = history.filter(x => x.valid);
|
|
430
439
|
for (let historicalValue of history) {
|
|
431
440
|
changes.values.add(historicalValue);
|
|
432
441
|
}
|
|
@@ -435,7 +444,7 @@ class PathWatcher {
|
|
|
435
444
|
}
|
|
436
445
|
} else {
|
|
437
446
|
// NOTE: We won't be given a value if it isn't synced, which will be never because we check if we're synced above. However, the type system doesn't know that.
|
|
438
|
-
let value = authorityStorage.
|
|
447
|
+
let value = authorityStorage.getValueAtOrBeforeTime(path);
|
|
439
448
|
if (value) {
|
|
440
449
|
changes.values.add(value);
|
|
441
450
|
}
|
|
@@ -5,31 +5,25 @@ import { isDiskAudit } from "../config";
|
|
|
5
5
|
import { getParentPathStr, getPathFromStr } from "../path";
|
|
6
6
|
import { auditLog } from "./auditLogs";
|
|
7
7
|
import { PathRouter } from "./PathRouter";
|
|
8
|
-
import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue, MAX_CHANGE_AGE } from "./pathValueCore";
|
|
8
|
+
import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue, MAX_CHANGE_AGE, createMissingEpochValue, Time, MISSING_TRANSACTION_PART_TIMEOUT, isOurPrediction, DEFER_LOCK_WINDOW } from "./pathValueCore";
|
|
9
9
|
import { pathWatcher } from "./PathWatcher";
|
|
10
10
|
import { lockWatcher2 } from "./LockWatcher2";
|
|
11
11
|
import { onPathInteracted } from "../diagnostics/pathAuditerCallback";
|
|
12
|
+
import { isMissingTransactionPart } from "../2-proxy/TransactionDelayer";
|
|
13
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
14
|
+
import { isDefined } from "../misc";
|
|
12
15
|
|
|
13
16
|
class ValidStateComputer {
|
|
14
17
|
|
|
15
|
-
private capturePrevValidStates(valuePaths: PathValue[]): Map<PathValue, boolean | undefined> {
|
|
16
|
-
let prevValidStates = new Map<PathValue, boolean | undefined>();
|
|
17
|
-
for (let value of valuePaths) {
|
|
18
|
-
let prevValidState = authorityStorage.getValueAtTime(value.path, value.time)?.valid;
|
|
19
|
-
prevValidStates.set(value, prevValidState);
|
|
20
|
-
}
|
|
21
|
-
return prevValidStates;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
@measureFnc
|
|
25
19
|
public ingestValuesAndValidStates(config: {
|
|
26
20
|
pathValues: PathValue[];
|
|
27
21
|
parentSyncs: { parentPath: string; sourceNodeId: string }[];
|
|
28
22
|
initialTriggers: { values: Set<string>; parentPaths: Set<string> };
|
|
29
23
|
doNotArchive?: boolean;
|
|
30
|
-
|
|
31
24
|
}) {
|
|
32
|
-
let { pathValues, parentSyncs,
|
|
25
|
+
let { pathValues, parentSyncs, doNotArchive } = config;
|
|
26
|
+
let initialTriggers = { ...config.initialTriggers, initialTriggerNonHistoryWatchers: new Set<string>() };
|
|
33
27
|
|
|
34
28
|
// TODO: We might want to add back optimizations for "no watches and no locks"?
|
|
35
29
|
// TODO: If we find it is a serious performance problem, we could just send the valid state instead of sending the full path value and values are rejected (this is what we used to do).
|
|
@@ -38,15 +32,47 @@ class ValidStateComputer {
|
|
|
38
32
|
|
|
39
33
|
authorityStorage.addParentSyncs(parentSyncs);
|
|
40
34
|
|
|
35
|
+
// Dedup by (path, time): if multiple values for the same (path, time) arrive in this batch,
|
|
36
|
+
// keep only the last one. Otherwise the storage-vs-incoming dedup below can drop the wrong one
|
|
37
|
+
// (e.g. a correction whose valid state happens to match stale storage gets filtered, while the
|
|
38
|
+
// earlier incorrect value in the same batch survives and clobbers storage).
|
|
39
|
+
{
|
|
40
|
+
let byPath = new Map<string, PathValue[]>();
|
|
41
|
+
for (let value of pathValues) {
|
|
42
|
+
let arr = byPath.get(value.path);
|
|
43
|
+
if (!arr) {
|
|
44
|
+
arr = [];
|
|
45
|
+
byPath.set(value.path, arr);
|
|
46
|
+
}
|
|
47
|
+
let index = binarySearchIndex(arr.length, i => compareTime(arr![i].time, value.time));
|
|
48
|
+
if (index >= 0) {
|
|
49
|
+
arr[index] = value;
|
|
50
|
+
} else {
|
|
51
|
+
arr.splice(~index, 0, value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let deduped: PathValue[] = [];
|
|
55
|
+
for (let arr of byPath.values()) {
|
|
56
|
+
for (let v of arr) {
|
|
57
|
+
deduped.push(v);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
deduped.sort((a, b) => compareTime(a.time, b.time));
|
|
61
|
+
pathValues = deduped;
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
let ourValues: PathValue[] = [];
|
|
42
65
|
let notOurValues: PathValue[] = [];
|
|
66
|
+
// Split into our values and not our values
|
|
67
|
+
// - Also Ignore values we already received with no valid state change. Required to prevent authorities from infinitely looping while exchanging values.
|
|
43
68
|
pathValues = pathValues.filter(value => {
|
|
44
69
|
// NOTE: I think if it's initial trigger we need to process it anyways?
|
|
45
70
|
if (!config.initialTriggers.values.has(value.path)) {
|
|
46
71
|
// Ignore values we already have. We receive duplicates, often due to values being broadcast multiple times in order to prevent data loss.
|
|
47
|
-
let existingValue = authorityStorage.
|
|
72
|
+
let existingValue = authorityStorage.getValueExactMaybeRejected(value.path, value.time);
|
|
48
73
|
// If we already have the value and the valid state is the exact same, then remove it from path values and don't even add it to our values. Pretend like we never received it.
|
|
49
|
-
|
|
74
|
+
// NOTE: We don't do any coercion here because we want the undefined state to not equal the false state.
|
|
75
|
+
if (existingValue && existingValue.valid === value.valid) {
|
|
50
76
|
let parentPath = getParentPathStr(value.path);
|
|
51
77
|
// We check for the parent trigger late because getting the parent path is a little bit expensive
|
|
52
78
|
if (!config.initialTriggers.parentPaths.has(parentPath)) {
|
|
@@ -70,12 +96,37 @@ class ValidStateComputer {
|
|
|
70
96
|
}
|
|
71
97
|
return true;
|
|
72
98
|
});
|
|
73
|
-
|
|
99
|
+
|
|
100
|
+
// NOTE: Watch value locks, will internally watch remote locks if we aren't the authority on the values
|
|
101
|
+
lockWatcher2.watchValueLocks(ourValues, now);
|
|
102
|
+
|
|
103
|
+
// ALL of the direct values are assumed changed
|
|
104
|
+
let valuesChanged = new Set<PathValue>(pathValues);
|
|
105
|
+
// ONLY Put values that we are the owner of in this. As for the other ones, we just ingest the updates, but we don't calculate their valid state.
|
|
106
|
+
let dependenciesChanged = new Set<PathValue>(ourValues);
|
|
107
|
+
// AND, Even if they're not our values, we may still be watching them. But then we need to trigger the actual watcher, not the not our value. All watchers are owned by us, but the things that are watched, as in the observed values, might not be owned by us.
|
|
108
|
+
// - AND, Inside of the loop, after we update a value, we also find all the watchers for it. So we're essentially doing one step of the loop early here. That way we don't have to compute the valid state values for ourselves. We short short-circuit that because we're not the owner. But for values that we do own, we do compute the valid state, and then we find the watcher.
|
|
109
|
+
for (let value of notOurValues) {
|
|
110
|
+
let watchers = lockWatcher2.getValuePathWatchers(value);
|
|
111
|
+
for (let watcher of watchers) {
|
|
112
|
+
dependenciesChanged.add(watcher);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!value.valid) {
|
|
116
|
+
initialTriggers.initialTriggerNonHistoryWatchers.add(value.path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
74
119
|
|
|
75
120
|
|
|
76
|
-
//
|
|
121
|
+
// NOTE: The code should still work if we have duplicate PathValues (same path, same time, but !==). It can be less efficient, but it should still work because this happens in certain cases.
|
|
122
|
+
let initialPrevValidStates = new Map<PathValue, boolean | undefined>();
|
|
123
|
+
for (let value of dependenciesChanged) {
|
|
124
|
+
let prevValidState = authorityStorage.getValueExactMaybeRejected(value.path, value.time)?.valid;
|
|
125
|
+
initialPrevValidStates.set(value, prevValidState);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create valid state false for old values on initial sync paths (ONLY for notOurValues)
|
|
77
129
|
{
|
|
78
|
-
// TODO: This code is similar to the code in trigger values change, but it's also dealing with watchers and other stuff, so I think it makes sense to have it also here.
|
|
79
130
|
let fullInitialPaths = new Set<string>();
|
|
80
131
|
for (let path of initialTriggers?.values ?? []) {
|
|
81
132
|
fullInitialPaths.add(path);
|
|
@@ -92,89 +143,101 @@ class ValidStateComputer {
|
|
|
92
143
|
let fullInitialPathsArr = Array.from(fullInitialPaths);
|
|
93
144
|
fullInitialPathsArr = fullInitialPathsArr.filter(x => !PathRouter.isSelfAuthority(x));
|
|
94
145
|
|
|
146
|
+
// Remove all other values (except for predictions)
|
|
147
|
+
// NOTE: We do this before we ingest our values. And the caller makes sure that the values that we have are actually came after this initial sync on the wire or are part of the initial sync, or are the latest, regardless. So, we don't need to worry about issues there, such as not clobbering values that we're about to ingest. That's fine.
|
|
148
|
+
|
|
149
|
+
|
|
95
150
|
let valueByPath = keyByArray(notOurValues, x => x.path);
|
|
151
|
+
|
|
152
|
+
|
|
96
153
|
for (let path of fullInitialPathsArr) {
|
|
154
|
+
if (PathRouter.isSelfAuthority(path)) {
|
|
155
|
+
console.error(`Try to apply an initial sync of a value that we're the authority on. This should have been filtered out much earlier. Path: ${path}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
97
158
|
let newValues = valueByPath.get(path) ?? [];
|
|
98
|
-
let prevValues = authorityStorage.getValuePlusHistory(path);
|
|
99
|
-
|
|
100
|
-
newValues.sort((a, b) => compareTime(a.time, b.time));
|
|
101
159
|
|
|
160
|
+
let prevValues = authorityStorage.getValuePlusHistory(path);
|
|
102
161
|
for (let value of prevValues) {
|
|
103
|
-
|
|
104
|
-
if
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
162
|
+
if (!value.valid) continue;
|
|
163
|
+
// Don't remove predictions. They'll remove themselves anyway. They always get clobbered by a server, write, and if they don't get clobbered, they have a timeout. If we remove predictions, then cases where we're constantly writing to new paths are really annoying and start to glitch out on the client, where we predict it, we remove it, and then we add it back.
|
|
164
|
+
// NOTE: If not removing predictions breaks things, we could probably remove predictions, at least when isServer().
|
|
165
|
+
if (isOurPrediction(value)) continue;
|
|
166
|
+
|
|
167
|
+
// NOTE: This isn't required for correctness, it just makes our logs a lot easier to read.
|
|
168
|
+
if (newValues.some(x => compareTime(x.time, value.time) === 0)) continue;
|
|
169
|
+
|
|
170
|
+
// NOTE: If it's one of the new values, ingest values will change the valid state anyway, so it's fine to set it here.
|
|
171
|
+
console.info(`Rejecting past value due to initial sync (might be immediately set again): ${debugPathValuePath(value)}`, {
|
|
172
|
+
path: value.path,
|
|
173
|
+
timeId: value.time.time,
|
|
174
|
+
timeIdFull: value.time,
|
|
175
|
+
});
|
|
176
|
+
initialPrevValidStates.set(value, true);
|
|
177
|
+
// @ts-expect-error
|
|
178
|
+
value.valid = false;
|
|
179
|
+
valuesChanged.add(value);
|
|
115
180
|
}
|
|
116
181
|
}
|
|
117
182
|
}
|
|
118
183
|
|
|
119
|
-
|
|
184
|
+
authorityStorage.ingestValues(notOurValues, { doNotArchive });
|
|
120
185
|
authorityStorage.ingestValues(ourValues, { doNotArchive });
|
|
121
186
|
|
|
122
|
-
// NOTE: Watch value locks, will internally watch remote locks if necessary.
|
|
123
|
-
lockWatcher2.watchValueLocks(ourValues, now);
|
|
124
|
-
|
|
125
|
-
// NOTE: Initial values always get put into computeValidStates, which then ingests, but does not compute values we are not the authority for.
|
|
126
|
-
let valuesChanged = new Set<PathValue>(pathValues);
|
|
127
|
-
let dependenciesChanged = new Set<PathValue>(ourValues);
|
|
128
|
-
let alreadyTriggered = new Set<PathValue>(dependenciesChanged);
|
|
129
|
-
// NOTE: For our own values, we don't care what the input valid state is, compute valid states will calculate if it's changed.
|
|
130
|
-
for (let value of notOurValues) {
|
|
131
|
-
// NOTE: We could be smarter and determine if the value we're receiving has a valid state change. However, it's probably fine.
|
|
132
|
-
// NOTE: Any watchers will get their valid states computed. I think that's really the only reason to watch something is if we own it and want to compute its valid state.
|
|
133
|
-
let watchers = lockWatcher2.getValuePathWatchers(value);
|
|
134
|
-
for (let watcher of watchers) {
|
|
135
|
-
if (alreadyTriggered.has(watcher)) continue;
|
|
136
|
-
dependenciesChanged.add(watcher);
|
|
137
|
-
alreadyTriggered.add(watcher);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
187
|
|
|
141
188
|
let loopCount = 0;
|
|
189
|
+
let deferredAll = new Set<PathValue>();
|
|
142
190
|
|
|
143
191
|
while (dependenciesChanged.size > 0) {
|
|
144
192
|
loopCount++;
|
|
145
193
|
if (loopCount > 3000) {
|
|
194
|
+
require("debugbreak")(2);
|
|
195
|
+
debugger;
|
|
146
196
|
console.error("Too many loops in valid state loop. aborting.", {
|
|
147
197
|
loopCount,
|
|
148
198
|
changedCount: dependenciesChanged.size,
|
|
149
|
-
totalTriggered: alreadyTriggered.size,
|
|
150
199
|
exampleChanged: Array.from(dependenciesChanged)[0].path,
|
|
151
200
|
});
|
|
152
201
|
break;
|
|
153
202
|
}
|
|
154
203
|
// Just because the dependencies change doesn't mean the valid state has changed. We actually have to compute to see if it has changed. Of course, if we're not the authority, we won't compute the valid site at all. It'll just ignore it, which is good. If we're not the authority, we don't want to be doing recursive computation. Because we prepopulate it, the values change with all of the input values, we'll still get a trigger. We just won't get a nested trigger.
|
|
155
|
-
let
|
|
204
|
+
let result = validStateComputer.computeValidStates(
|
|
156
205
|
Array.from(dependenciesChanged),
|
|
157
206
|
now,
|
|
158
207
|
initialPrevValidStates,
|
|
159
|
-
)
|
|
160
|
-
|
|
208
|
+
);
|
|
209
|
+
let validStateChanged = result.changed;
|
|
210
|
+
for (let d of result.deferred) {
|
|
211
|
+
deferredAll.add(d);
|
|
212
|
+
}
|
|
213
|
+
for (let change of validStateChanged) {
|
|
214
|
+
initialPrevValidStates.set(change, change.valid);
|
|
215
|
+
deferredAll.delete(change);
|
|
216
|
+
}
|
|
161
217
|
dependenciesChanged.clear();
|
|
162
218
|
|
|
163
219
|
for (let validStateChange of validStateChanged) {
|
|
164
220
|
valuesChanged.add(validStateChange);
|
|
165
|
-
|
|
221
|
+
|
|
222
|
+
// NOTE: This approach should be faster than doing a full initial sync, but we'll see if it breaks anything.
|
|
166
223
|
if (!validStateChange.valid) {
|
|
167
|
-
|
|
168
|
-
initialTriggers.values.add(validStateChange.path);
|
|
224
|
+
initialTriggers.initialTriggerNonHistoryWatchers.add(validStateChange.path);
|
|
169
225
|
}
|
|
226
|
+
|
|
227
|
+
// // NOTE: As of right now, I think the only reason to force an initial trigger in this case is so that we can bust all the values through the query sub http server to the clients. Because if we just tell the HTTP server that one value has been rejected, it can go tell its clients, but that won't give them the latest value. Which isn't great. We should really have something where we can tell them. HMM...
|
|
228
|
+
// // IMPORTANT! The consequence of this is that if someone predicts a value, sends it to us, it's not valid, we will see it's not valid, and we will tell all of the nodes that are watching that path about all of the values on that path. This is inefficient. However, rejected values should be quite rare, so it shouldn't be too inefficient.
|
|
229
|
+
// if (!validStateChange.valid) {
|
|
230
|
+
// // NOTE: TECHNICALLY, We could restrict this change in the case when a node just created the value, so we haven't told anyone about it. However, also, this is replacing the case where we send that update to all the other nodes. And so, if we do the math, let's say we have a history window of 100 seconds, and 1% of the writes get rejected, and we're writing once per second. Then this would result in sending twice as many values in total, which isn't great, but also a 1% rejection rate is pretty high.
|
|
231
|
+
// initialTriggers.values.add(validStateChange.path);
|
|
232
|
+
// }
|
|
170
233
|
}
|
|
171
234
|
// And then for everything that had a valid state change, go check if it has a dependency that was watching it.
|
|
235
|
+
// NOTE: We used to make sure we don't re-trigger something that triggered before, which prevents always running computeValidStates twice on new values. BUT... it causes issues if you have out of order evaluation. We try to fix out of order evaluation in computeValidStates, but I think locks could be setup so it's not possible. So instead... we just allow evaluating values multiple times, which is twice as slow, but... it's probably not a bottleneck.
|
|
236
|
+
// IF we change this, test with a simple x++ update, where the function has no shard hints, to verify it doesn't break (queue about 100 events at once to test)
|
|
172
237
|
for (let value of validStateChanged) {
|
|
173
238
|
let watchers = lockWatcher2.getValuePathWatchers(value);
|
|
174
239
|
for (let watcher of watchers) {
|
|
175
|
-
if (alreadyTriggered.has(watcher)) continue;
|
|
176
240
|
dependenciesChanged.add(watcher);
|
|
177
|
-
alreadyTriggered.add(watcher);
|
|
178
241
|
}
|
|
179
242
|
}
|
|
180
243
|
}
|
|
@@ -183,13 +246,40 @@ class ValidStateComputer {
|
|
|
183
246
|
valuesChanged,
|
|
184
247
|
initialTriggers
|
|
185
248
|
});
|
|
249
|
+
|
|
250
|
+
// If any lockGroups deferred decision (waiting on a value from a foreign authority that
|
|
251
|
+
// may still be in flight), schedule a re-evaluation after DEFER_LOCK_DELAY. By then the
|
|
252
|
+
// age check in isLockValid will treat any still-missing locks as definitively rejected.
|
|
253
|
+
if (deferredAll.size > 0) {
|
|
254
|
+
let deferredArr = Array.from(deferredAll);
|
|
255
|
+
let deferredPaths = new Set<string>();
|
|
256
|
+
for (let v of deferredArr) {
|
|
257
|
+
deferredPaths.add(v.path);
|
|
258
|
+
}
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
this.ingestValuesAndValidStates({
|
|
261
|
+
pathValues: deferredArr,
|
|
262
|
+
parentSyncs: [],
|
|
263
|
+
// Add deferred paths to initialTriggers.values to bypass the storage-vs-incoming
|
|
264
|
+
// dedup (which would otherwise filter these out since storage already has them).
|
|
265
|
+
initialTriggers: { values: deferredPaths, parentPaths: new Set() },
|
|
266
|
+
});
|
|
267
|
+
}, DEFER_LOCK_WINDOW);
|
|
268
|
+
}
|
|
186
269
|
}
|
|
187
270
|
|
|
188
|
-
private isLockValid(lock: ReadLock):
|
|
189
|
-
|
|
271
|
+
private isLockValid(lock: ReadLock, now: number): true | false | "defer" {
|
|
272
|
+
if (lock.readIsTransparent) return true;
|
|
273
|
+
let value = authorityStorage.getValueExactMaybeRejected(lock.path, lock.startTime);
|
|
190
274
|
if (!value) {
|
|
191
275
|
// If not synced, we don't want to reject it yet
|
|
192
|
-
|
|
276
|
+
if (!authorityStorage.isSynced(lock.path)) return true;
|
|
277
|
+
// Value missing on a foreign-authority path. It may still be in flight; defer the decision
|
|
278
|
+
// until DEFER_LOCK_DELAY has elapsed since the lock's startTime.
|
|
279
|
+
if (!PathRouter.isSelfAuthority(lock.path) && now - lock.startTime.time < DEFER_LOCK_WINDOW) {
|
|
280
|
+
return "defer";
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
193
283
|
}
|
|
194
284
|
return !!value.valid;
|
|
195
285
|
}
|
|
@@ -219,55 +309,115 @@ class ValidStateComputer {
|
|
|
219
309
|
public computeValidStates(
|
|
220
310
|
valuePaths: PathValue[],
|
|
221
311
|
now: number,
|
|
222
|
-
prevValidStates
|
|
223
|
-
): { changed: PathValue[] } {
|
|
312
|
+
prevValidStates: Map<PathValue, boolean | undefined>,
|
|
313
|
+
): { changed: PathValue[]; deferred: PathValue[] } {
|
|
224
314
|
let changed: PathValue[] = [];
|
|
315
|
+
let deferred: PathValue[] = [];
|
|
225
316
|
|
|
226
317
|
let lockGroups = byLockGroup(valuePaths);
|
|
227
318
|
|
|
319
|
+
// Sort by the lock time to remove unneeded cycles (although we still always loop at least once).
|
|
320
|
+
let entries = Array.from(lockGroups);
|
|
321
|
+
entries = entries.filter(x => x[0].length > 0);
|
|
322
|
+
entries = entries.sort((a, b) => compareTime(a[0][0].endTime, b[0][0].endTime));
|
|
323
|
+
lockGroups = new Map(entries);
|
|
324
|
+
|
|
228
325
|
for (let [locks, valueGroup] of lockGroups) {
|
|
229
326
|
if (valueGroup.length === 0) continue;
|
|
327
|
+
if (locks.length === 0) continue;
|
|
230
328
|
|
|
231
|
-
let valid = true;
|
|
232
|
-
for (let lock of locks) {
|
|
233
|
-
let age = now - lock.endTime.time;
|
|
234
|
-
// If it's old enough, then it's always valid, we can't change the valid save something after its max change age, even if it's invalid.
|
|
235
|
-
if (age > MAX_CHANGE_AGE) continue;
|
|
236
329
|
|
|
237
|
-
|
|
238
|
-
|
|
330
|
+
let valid: true | false | "defer" = true;
|
|
331
|
+
|
|
332
|
+
let lockTime = locks[0].endTime;
|
|
333
|
+
for (let lock of locks) {
|
|
334
|
+
if (compareTime(lock.endTime, lockTime) !== 0) {
|
|
335
|
+
require("debugbreak")(2);
|
|
336
|
+
debugger;
|
|
337
|
+
console.error(`Lock has inconsistent end times. ${debugTime(lock.endTime)} !== ${debugTime(lockTime)}. Path: ${lock.path}`);
|
|
239
338
|
valid = false;
|
|
240
|
-
this.logLockInvalid(valueGroup, lock, now);
|
|
241
|
-
break;
|
|
242
339
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (let value of valueGroup) {
|
|
343
|
+
if (!PathRouter.isSelfAuthority(value.path)) {
|
|
344
|
+
require("debugbreak")(2);
|
|
345
|
+
debugger;
|
|
346
|
+
console.error(`Somehow we're attempting to compute the valid state of a value we don't own. This should be impossible. ${debugPathValue(value)}`);
|
|
246
347
|
valid = false;
|
|
247
|
-
this.logLockContention(valueGroup, lock, contention, now);
|
|
248
|
-
break;
|
|
249
348
|
}
|
|
250
349
|
}
|
|
350
|
+
|
|
351
|
+
let invalidReason: string | undefined;
|
|
352
|
+
let age = now - lockTime.time;
|
|
353
|
+
// If it's old enough, then it's always valid, we can't change the valid save something after its max change age, even if it's invalid.
|
|
354
|
+
if (age < MAX_CHANGE_AGE && valid === true) {
|
|
355
|
+
for (let lock of locks) {
|
|
356
|
+
let lockResult = this.isLockValid(lock, now);
|
|
357
|
+
if (lockResult === "defer") {
|
|
358
|
+
valid = "defer";
|
|
359
|
+
// Don't break — a later lock might be definitively false, which beats defer.
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (!lockResult) {
|
|
363
|
+
onPathInteracted(lock.path, 2);
|
|
364
|
+
valid = false;
|
|
365
|
+
let lockValue = authorityStorage.getValueExactMaybeRejected(lock.path, lock.startTime);
|
|
366
|
+
invalidReason = `lock-invalid on ${lock.path} at ${debugTime(lock.startTime)} (value=${lockValue ? debugPathValue(lockValue) : "missing"}, synced=${authorityStorage.isSynced(lock.path)}, transparent=${lock.readIsTransparent})`;
|
|
367
|
+
this.logLockInvalid(valueGroup, lock, now);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
let contention = this.isLockContentionFree(lock);
|
|
371
|
+
if (contention) {
|
|
372
|
+
onPathInteracted(lock.path, 2);
|
|
373
|
+
valid = false;
|
|
374
|
+
invalidReason = `lock-contention on ${lock.path} range ${debugTime(lock.startTime)}..${debugTime(lock.endTime)} conflicts=[${contention.map(x => debugTime(x.time)).join(", ")}]`;
|
|
375
|
+
this.logLockContention(valueGroup, lock, contention, now);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// NOTE: It's pretty easy for us to look at all the valid path values for the locks and then see if there's an inconsistent transaction. However, I don't think this is a common case. I think the common case is were depending on just a single write, which happened to land on a different server than us.
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (valid === "defer") {
|
|
384
|
+
// Don't decide; leave pathValue.valid and storage alone. Schedule retry at outer level.
|
|
385
|
+
for (let pathValue of valueGroup) {
|
|
386
|
+
deferred.push(pathValue);
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
251
391
|
for (let pathValue of valueGroup) {
|
|
252
392
|
let prevValidState = prevValidStates.get(pathValue);
|
|
393
|
+
|
|
253
394
|
// IMPORTANT! This means if it didn't previously exist and it's presently rejected, we count that as a change, as it this is going from undefined to false. This is actually very useful, as a lot of places want to know if a write is rejected, so they can display it in the UI for the developer.
|
|
254
395
|
if (valid === prevValidState) continue;
|
|
255
396
|
|
|
256
397
|
changed.push(pathValue);
|
|
257
398
|
|
|
258
|
-
// NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
|
|
259
|
-
|
|
399
|
+
// NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
|
|
400
|
+
// @ts-expect-error
|
|
401
|
+
pathValue.valid = valid;
|
|
402
|
+
|
|
403
|
+
// HACK: There are times when we might be evaluating the same path value multiple times at once (ex, multiple initial syncs at once). In which case we might not equal the stored path value, so we need to forcefully update the valid state of it.
|
|
404
|
+
let storedPathValue = authorityStorage.getValueExactMaybeRejected(pathValue.path, pathValue.time);
|
|
405
|
+
// I think this is fine. I think it happens if we receive it multiple times due to sharding. It's a bit inefficient, but... SHOULD be fine.
|
|
406
|
+
if (storedPathValue && pathValue !== storedPathValue) {
|
|
407
|
+
// @ts-expect-error
|
|
408
|
+
storedPathValue.valid = valid;
|
|
409
|
+
}
|
|
260
410
|
}
|
|
261
411
|
}
|
|
262
412
|
|
|
263
|
-
return { changed };
|
|
413
|
+
return { changed, deferred };
|
|
264
414
|
}
|
|
265
415
|
|
|
266
416
|
private logLockInvalid(valueGroup: PathValue[], lock: ReadLock, now: number) {
|
|
267
417
|
// This is good... unless it happens A LOT. Then the app needs to change (unless it
|
|
268
418
|
// is a game, or something with realtime competition, in which case this is probably unavoidable).
|
|
269
419
|
if (valueGroup.length > 0 && lock.endTime.version !== Number.MAX_SAFE_INTEGER && lock.startTime.version !== -2) {
|
|
270
|
-
if (
|
|
420
|
+
if (true) {
|
|
271
421
|
let timeToReject = now - valueGroup[0].time.time;
|
|
272
422
|
let message = `!!! (VALUE REJECTED) VALUE REJECTED DUE TO USING MISSING / REJECTED READ!!!(rejected after ${timeToReject}ms at ${Date.now()})`;
|
|
273
423
|
message += `\n${debugTime(valueGroup[0].time)} (write)`;
|
|
@@ -307,9 +457,7 @@ class ValidStateComputer {
|
|
|
307
457
|
|
|
308
458
|
let changed = valueGroup.filter(x => x.valid);
|
|
309
459
|
if (changed.length > 0) {
|
|
310
|
-
if (
|
|
311
|
-
|| debugRejections
|
|
312
|
-
) {
|
|
460
|
+
if (true) {
|
|
313
461
|
let timeToReject = now - changed[0].time.time;
|
|
314
462
|
let redMessage = `!!! (VALUE REJECTED) LOCK CONTENTION FIXED VIA REJECTION OF VALUE!!!(rejected after ${timeToReject}ms)`;
|
|
315
463
|
for (let pathValue of changed) {
|