querysub 0.403.0 → 0.405.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/bin/join.js +1 -1
- 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 +543 -0
- package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +73 -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 +491 -492
- package/src/0-path-value-core/pathValueCore.ts +195 -1496
- package/src/0-path-value-core/startupAuthority.ts +74 -0
- package/src/1-path-client/RemoteWatcher.ts +90 -82
- 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 +596 -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 +6 -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 +96 -84
- 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 +34 -16
- 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 +65 -0
- package/test.ts +298 -97
- 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/archiveMergeEntry.tsx +0 -48
- package/src/archiveapps/lockTest.ts +0 -127
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { binarySearchBasic2, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
2
|
+
import { PathRouter } from "../0-path-value-core/PathRouter";
|
|
3
|
+
import { authorityStorage, MAX_CHANGE_AGE, PathValue, compareTime, Time, createMissingEpochValue, epochTime } from "../0-path-value-core/pathValueCore";
|
|
4
|
+
import { formatNumber, formatPercent, formatTime } from "socket-function/src/formatting/format";
|
|
5
|
+
import { timeoutToError } from "../errors";
|
|
6
|
+
import { setPathInteractedCallback } from "./pathAuditerCallback";
|
|
7
|
+
import { registerResource } from "./trackResources";
|
|
8
|
+
import { registerMeasureInfo } from "socket-function/src/profiling/measure";
|
|
9
|
+
import { lazy } from "socket-function/src/caching";
|
|
10
|
+
import { runInfinitePoll, runInSerial } from "socket-function/src/batching";
|
|
11
|
+
import { authorityLookup } from "../0-path-value-core/AuthorityLookup";
|
|
12
|
+
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
13
|
+
import { validStateComputer } from "../0-path-value-core/ValidStateComputer";
|
|
14
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
15
|
+
import { decodeNodeId } from "../-a-auth/certs";
|
|
16
|
+
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
17
|
+
import { assertIsManagementUser } from "./managementPages";
|
|
18
|
+
import { isNode } from "typesafecss";
|
|
19
|
+
import { isClient } from "../config2";
|
|
20
|
+
import { isLocal } from "../config";
|
|
21
|
+
import { pathWatcher } from "../0-path-value-core/PathWatcher";
|
|
22
|
+
|
|
23
|
+
if (!isClient()) {
|
|
24
|
+
// Comment this line out to disable our functionality
|
|
25
|
+
// NOTE: I think this should stay on even in production because it should be able to fix lots of issues.
|
|
26
|
+
setPathInteractedCallback(onPathInteracted);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CHECK_INTERVAL = timeInMinute * 3;
|
|
30
|
+
const AUDIT_PATHS_PER_CHECK = 1000;
|
|
31
|
+
const DROP_AGE_THRESHOLD = CHECK_INTERVAL * 5;
|
|
32
|
+
|
|
33
|
+
const ERROR_AGE_THRESHOLD = timeInMinute;
|
|
34
|
+
const WARN_THRESHOLD = timeInSecond * 5;
|
|
35
|
+
const AUDIT_TIMEOUT = timeInMinute * 2;
|
|
36
|
+
|
|
37
|
+
type PathInteractionPriority = 0 | 1 | 2; // 0: read, 1: write, 2: rejected (higher is more important)
|
|
38
|
+
|
|
39
|
+
interface PathInteraction {
|
|
40
|
+
path: string;
|
|
41
|
+
time: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface AuditStats {
|
|
45
|
+
totalAudited: number;
|
|
46
|
+
totalDiscarded: number;
|
|
47
|
+
errorCount: number;
|
|
48
|
+
warningCount: number;
|
|
49
|
+
fixedCount: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface PendingValidityCheck {
|
|
53
|
+
path: string;
|
|
54
|
+
ourTimeId: number;
|
|
55
|
+
remoteTimeId: number | undefined;
|
|
56
|
+
ourValid: boolean;
|
|
57
|
+
remoteValid: boolean | undefined;
|
|
58
|
+
remoteNodeId: string;
|
|
59
|
+
discoveredAt: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class CyclicList<T> {
|
|
63
|
+
private items: (T | undefined)[];
|
|
64
|
+
private startIndex: number = 0;
|
|
65
|
+
private count: number = 0;
|
|
66
|
+
private capacity: number;
|
|
67
|
+
|
|
68
|
+
constructor(capacity: number) {
|
|
69
|
+
this.capacity = capacity;
|
|
70
|
+
this.items = new Array(capacity);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public push(item: T): void {
|
|
74
|
+
let writeIndex = (this.startIndex + this.count) % this.capacity;
|
|
75
|
+
this.items[writeIndex] = item;
|
|
76
|
+
if (this.count < this.capacity) {
|
|
77
|
+
this.count++;
|
|
78
|
+
} else {
|
|
79
|
+
this.startIndex = (this.startIndex + 1) % this.capacity;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public length(): number {
|
|
84
|
+
return this.count;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public at(index: number): T {
|
|
88
|
+
if (index >= this.count) throw new Error(`Index ${index} is out of bounds for list of length ${this.count}`);
|
|
89
|
+
let actualIndex = (this.startIndex + index) % this.capacity;
|
|
90
|
+
let item = this.items[actualIndex];
|
|
91
|
+
if (item === undefined) throw new Error(`Item at index ${index} is undefined`);
|
|
92
|
+
return item;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public shift(count: number): void {
|
|
96
|
+
let toRemove = Math.min(count, this.count);
|
|
97
|
+
this.startIndex = (this.startIndex + toRemove) % this.capacity;
|
|
98
|
+
this.count -= toRemove;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const CYCLIC_LIST_CAPACITY = 100000;
|
|
103
|
+
|
|
104
|
+
const pathLists: CyclicList<PathInteraction>[] = [
|
|
105
|
+
new CyclicList<PathInteraction>(CYCLIC_LIST_CAPACITY), // priority 0: read
|
|
106
|
+
new CyclicList<PathInteraction>(CYCLIC_LIST_CAPACITY), // priority 1: write
|
|
107
|
+
new CyclicList<PathInteraction>(CYCLIC_LIST_CAPACITY), // priority 2: rejected
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
let lastAuditTime: number = 0;
|
|
111
|
+
let stats: AuditStats = {
|
|
112
|
+
totalAudited: 0,
|
|
113
|
+
totalDiscarded: 0,
|
|
114
|
+
errorCount: 0,
|
|
115
|
+
warningCount: 0,
|
|
116
|
+
fixedCount: 0,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const pendingValidityChecks: PendingValidityCheck[] = [];
|
|
120
|
+
const pendingValidityCheckPaths = new Set<string>();
|
|
121
|
+
|
|
122
|
+
function onPathInteracted(path: string, priority: PathInteractionPriority): void {
|
|
123
|
+
if (PathRouter.isLocalPath(path)) return;
|
|
124
|
+
ensureAuditing();
|
|
125
|
+
pathLists[priority].push({ path, time: Date.now() });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const ensureAuditing = lazy(() => {
|
|
129
|
+
runInfinitePoll(CHECK_INTERVAL, runAuditNowSerial);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const runAuditNowSerial = runInSerial(runAuditNow);
|
|
133
|
+
|
|
134
|
+
async function runAuditNow() {
|
|
135
|
+
await PathRouter.waitUntilReady();
|
|
136
|
+
let now = Date.now();
|
|
137
|
+
|
|
138
|
+
let pathsToAudit = new Set<string>();
|
|
139
|
+
let perList = Math.floor(AUDIT_PATHS_PER_CHECK / pathLists.length);
|
|
140
|
+
|
|
141
|
+
for (let priority = 0; priority < pathLists.length; priority++) {
|
|
142
|
+
let list = pathLists[priority];
|
|
143
|
+
let len = list.length();
|
|
144
|
+
|
|
145
|
+
let splitIndex = 0;
|
|
146
|
+
for (let i = 0; i < len; i++) {
|
|
147
|
+
let item = list.at(i);
|
|
148
|
+
if (item && item.time > lastAuditTime) {
|
|
149
|
+
splitIndex = i;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let takeAfter = Math.min(Math.floor(perList / 2), len - splitIndex);
|
|
155
|
+
let takeBefore = Math.min(perList - takeAfter, splitIndex);
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < takeAfter; i++) {
|
|
158
|
+
pathsToAudit.add(list.at(splitIndex + i).path);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < takeBefore; i++) {
|
|
162
|
+
pathsToAudit.add(list.at(i).path);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let discarded = 0;
|
|
166
|
+
for (let i = 0; i < len; i++) {
|
|
167
|
+
let item = list.at(i);
|
|
168
|
+
if (now - item.time > DROP_AGE_THRESHOLD) {
|
|
169
|
+
discarded++;
|
|
170
|
+
} else {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
list.shift(discarded);
|
|
175
|
+
stats.totalDiscarded += discarded;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lastAuditTime = now;
|
|
179
|
+
let paths = Array.from(pathsToAudit);
|
|
180
|
+
// If we stopped watching it, or were never watching it in the first place, we don't want to audit it. Or if we haven't finished watching it anyways.
|
|
181
|
+
paths = paths.filter(path => pathWatcher.isWatching(path) && authorityStorage.isSynced(path) || PathRouter.isSelfAuthority(path));
|
|
182
|
+
if (paths.length === 0) return;
|
|
183
|
+
|
|
184
|
+
let pathObjs: { path: string }[] = paths.map(path => ({ path }));
|
|
185
|
+
let valuesByAuthority = PathRouter.getAllAuthoritiesForValues(pathObjs);
|
|
186
|
+
|
|
187
|
+
await Promise.all(
|
|
188
|
+
Array.from(valuesByAuthority.entries()).map(([nodeId, pathsToTest]) =>
|
|
189
|
+
timeoutToError(
|
|
190
|
+
AUDIT_TIMEOUT,
|
|
191
|
+
auditAuthority(nodeId, pathsToTest, now),
|
|
192
|
+
() => new Error(`Path audit timed out for node ${nodeId}`)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await processPendingValidityChecks(now);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function processPendingValidityChecks(now: number) {
|
|
201
|
+
let threshold = now - MAX_CHANGE_AGE;
|
|
202
|
+
let splitIndex = binarySearchBasic2(pendingValidityChecks, check => check.discoveredAt, { discoveredAt: threshold } as PendingValidityCheck);
|
|
203
|
+
if (splitIndex < 0) splitIndex = ~splitIndex;
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < splitIndex; i++) {
|
|
206
|
+
let pendingCheck = pendingValidityChecks[i];
|
|
207
|
+
let currentValue = authorityStorage.getValueAtTime(pendingCheck.path);
|
|
208
|
+
if (currentValue && currentValue.valid && currentValue.time !== epochTime) {
|
|
209
|
+
trackSyncAge({
|
|
210
|
+
path: pendingCheck.path,
|
|
211
|
+
ourTimeId: pendingCheck.ourTimeId,
|
|
212
|
+
remoteTimeId: pendingCheck.remoteTimeId,
|
|
213
|
+
ourValid: pendingCheck.ourValid,
|
|
214
|
+
remoteValid: pendingCheck.remoteValid,
|
|
215
|
+
remoteNodeId: pendingCheck.remoteNodeId,
|
|
216
|
+
reason: "Pending validity check aged past MAX_CHANGE_AGE. Forcing sync to all nodes.",
|
|
217
|
+
});
|
|
218
|
+
let allAuthorities = PathRouter.getAllAuthoritiesForValues([currentValue]);
|
|
219
|
+
for (let [authorityNodeId, _] of allAuthorities.entries()) {
|
|
220
|
+
let serialized = await pathValueSerializer.serialize([currentValue]);
|
|
221
|
+
await PathAuditerController.nodes[authorityNodeId].ingestPathValues(serialized);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (splitIndex > 0) {
|
|
227
|
+
for (let i = 0; i < splitIndex; i++) {
|
|
228
|
+
pendingValidityCheckPaths.delete(pendingValidityChecks[i].path);
|
|
229
|
+
}
|
|
230
|
+
pendingValidityChecks.splice(0, splitIndex);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function trackSyncAge(config: {
|
|
235
|
+
path: string;
|
|
236
|
+
ourTimeId: number;
|
|
237
|
+
remoteTimeId: number | undefined;
|
|
238
|
+
ourValid: boolean;
|
|
239
|
+
remoteValid: boolean | undefined;
|
|
240
|
+
remoteNodeId: string;
|
|
241
|
+
reason: string;
|
|
242
|
+
}) {
|
|
243
|
+
let age = Date.now() - config.ourTimeId;
|
|
244
|
+
if (age >= ERROR_AGE_THRESHOLD) {
|
|
245
|
+
stats.errorCount++;
|
|
246
|
+
console.error(config.reason, {
|
|
247
|
+
path: config.path,
|
|
248
|
+
age,
|
|
249
|
+
ourTimeId: config.ourTimeId,
|
|
250
|
+
remoteTimeId: config.remoteTimeId,
|
|
251
|
+
ourValid: config.ourValid,
|
|
252
|
+
remoteValid: config.remoteValid,
|
|
253
|
+
remoteNodeId: config.remoteNodeId,
|
|
254
|
+
remoteNodeThreadId: decodeNodeId(config.remoteNodeId)?.threadId,
|
|
255
|
+
});
|
|
256
|
+
} else if (age >= WARN_THRESHOLD) {
|
|
257
|
+
stats.warningCount++;
|
|
258
|
+
console.warn(config.reason, {
|
|
259
|
+
path: config.path,
|
|
260
|
+
age,
|
|
261
|
+
ourTimeId: config.ourTimeId,
|
|
262
|
+
remoteTimeId: config.remoteTimeId,
|
|
263
|
+
ourValid: config.ourValid,
|
|
264
|
+
remoteValid: config.remoteValid,
|
|
265
|
+
remoteNodeId: config.remoteNodeId,
|
|
266
|
+
remoteNodeThreadId: decodeNodeId(config.remoteNodeId)?.threadId,
|
|
267
|
+
});
|
|
268
|
+
} else {
|
|
269
|
+
stats.fixedCount++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[], now: number) {
|
|
274
|
+
let requests: PathTimeRequest[] = [];
|
|
275
|
+
|
|
276
|
+
for (let pathObj of pathsToAudit) {
|
|
277
|
+
let path = pathObj.path;
|
|
278
|
+
let ourLatest = authorityStorage.getValueAtTime(path);
|
|
279
|
+
|
|
280
|
+
// Ask for their latest valid (time: undefined means latest)
|
|
281
|
+
requests.push({ path, time: undefined });
|
|
282
|
+
|
|
283
|
+
if (ourLatest?.time) {
|
|
284
|
+
// Ask for their state at our specific time
|
|
285
|
+
requests.push({ path, time: ourLatest.time });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let history = authorityStorage.getValuePlusHistory(path);
|
|
289
|
+
if (history.length > 1) {
|
|
290
|
+
let randomIndex = Math.floor(Math.random() * (history.length - 1)) + 1;
|
|
291
|
+
let randomValue = history[randomIndex];
|
|
292
|
+
requests.push({ path, time: randomValue.time });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let responses = await PathAuditerController.nodes[nodeId].getValidStates(requests);
|
|
297
|
+
|
|
298
|
+
let valuesToRequest: PathTimeRequest[] = [];
|
|
299
|
+
let valuesToSend: PathValue[] = [];
|
|
300
|
+
let pathsToForceSync = new Set<string>();
|
|
301
|
+
|
|
302
|
+
for (let response of responses) {
|
|
303
|
+
let ourValue = authorityStorage.getValueAtTime(response.path) || createMissingEpochValue(response.path);
|
|
304
|
+
if (ourValue.isTransparent && response.isTransparent) continue;
|
|
305
|
+
|
|
306
|
+
// it's latest valid is newer than ours
|
|
307
|
+
// - Ask it for the value of the latest
|
|
308
|
+
if (response.valid && response.time && compareTime(response.time, ourValue.time) > 0) {
|
|
309
|
+
valuesToRequest.push({ path: response.path, time: response.time });
|
|
310
|
+
let authorities = PathRouter.getAllAuthorities(response.path);
|
|
311
|
+
require("debugbreak")(2);
|
|
312
|
+
debugger;
|
|
313
|
+
trackSyncAge({
|
|
314
|
+
path: response.path,
|
|
315
|
+
ourTimeId: ourValue.time.time,
|
|
316
|
+
remoteTimeId: response.time.time,
|
|
317
|
+
ourValid: ourValue.valid ?? false,
|
|
318
|
+
remoteValid: response.valid,
|
|
319
|
+
remoteNodeId: nodeId,
|
|
320
|
+
reason: "Remote has newer latest value, asking for it",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// Our latest valid = server does not have & it's latest valid is older than ours
|
|
324
|
+
// - Send it our value
|
|
325
|
+
else if (response.valid === undefined && (!response.time || compareTime(ourValue.time, response.time) > 0)) {
|
|
326
|
+
valuesToSend.push(ourValue);
|
|
327
|
+
require("debugbreak")(2);
|
|
328
|
+
debugger;
|
|
329
|
+
trackSyncAge({
|
|
330
|
+
path: response.path,
|
|
331
|
+
ourTimeId: ourValue.time.time,
|
|
332
|
+
remoteTimeId: response.time?.time,
|
|
333
|
+
ourValid: ourValue.valid ?? false,
|
|
334
|
+
remoteValid: response.valid,
|
|
335
|
+
remoteNodeId: nodeId,
|
|
336
|
+
reason: "Remote is missing our value, sending it to them",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Our latest valid = server has, but says it's not valid
|
|
340
|
+
// - If time < MAX_CHANGE_AGE old, record this, and wait until MAX_CHANGE_AGE (put it in some other kind of queue), and then check again
|
|
341
|
+
// - If time >= MAX_CHANGE_AGE old, then tell it to change it's valid state to ours. Also, tell all other authorities that use this path.
|
|
342
|
+
// NOTE: We skew towards making values valid instead of invalid. So if there's disagreement, we always take the valid value instead of the invalid value.
|
|
343
|
+
// NOTE: The opposite case, if the remote is valid and ours is invalid, is handled by our latest value being older than the remote value, as we're never going to ask about an invalid value. It'll just implicitly change what the latest valid value is.
|
|
344
|
+
else if (!response.valid && ourValue.valid && ourValue.time !== epochTime) {
|
|
345
|
+
let age = now - ourValue.time.time;
|
|
346
|
+
if (age >= MAX_CHANGE_AGE) {
|
|
347
|
+
pathsToForceSync.add(response.path);
|
|
348
|
+
require("debugbreak")(2);
|
|
349
|
+
debugger;
|
|
350
|
+
trackSyncAge({
|
|
351
|
+
path: response.path,
|
|
352
|
+
ourTimeId: ourValue.time.time,
|
|
353
|
+
remoteTimeId: response.time?.time,
|
|
354
|
+
ourValid: ourValue.valid,
|
|
355
|
+
remoteValid: response.valid,
|
|
356
|
+
remoteNodeId: nodeId,
|
|
357
|
+
reason: "Remote says our value is invalid, but we think it's valid. Telling all nodes about this value to ensure it's in sync everywhere.",
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
if (!pendingValidityCheckPaths.has(response.path)) {
|
|
361
|
+
let newCheck: PendingValidityCheck = {
|
|
362
|
+
path: response.path,
|
|
363
|
+
ourTimeId: ourValue.time.time,
|
|
364
|
+
remoteTimeId: response.time?.time,
|
|
365
|
+
ourValid: ourValue.valid ?? false,
|
|
366
|
+
remoteValid: response.valid,
|
|
367
|
+
remoteNodeId: nodeId,
|
|
368
|
+
discoveredAt: now,
|
|
369
|
+
};
|
|
370
|
+
let insertIndex = binarySearchBasic2(pendingValidityChecks, check => check.discoveredAt, newCheck);
|
|
371
|
+
if (insertIndex < 0) insertIndex = ~insertIndex;
|
|
372
|
+
pendingValidityChecks.splice(insertIndex, 0, newCheck);
|
|
373
|
+
pendingValidityCheckPaths.add(response.path);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (valuesToRequest.length > 0) {
|
|
380
|
+
let receivedValues = await PathAuditerController.nodes[nodeId].getPathValues(valuesToRequest);
|
|
381
|
+
if (receivedValues.length > 0) {
|
|
382
|
+
validStateComputer.ingestValuesAndValidStates({
|
|
383
|
+
pathValues: receivedValues,
|
|
384
|
+
parentSyncs: [],
|
|
385
|
+
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (valuesToSend.length > 0) {
|
|
391
|
+
let serialized = await pathValueSerializer.serialize(valuesToSend);
|
|
392
|
+
await PathAuditerController.nodes[nodeId].ingestPathValues(serialized);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (pathsToForceSync.size > 0) {
|
|
396
|
+
for (let path of pathsToForceSync) {
|
|
397
|
+
let valueToForce = authorityStorage.getValueAtTime(path);
|
|
398
|
+
if (valueToForce) {
|
|
399
|
+
let allAuthorities = PathRouter.getAllAuthoritiesForValues([valueToForce]);
|
|
400
|
+
for (let [authorityNodeId, _] of allAuthorities.entries()) {
|
|
401
|
+
let serialized = await pathValueSerializer.serialize([valueToForce]);
|
|
402
|
+
await PathAuditerController.nodes[authorityNodeId].ingestPathValues(serialized);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
stats.totalAudited += pathsToAudit.length;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
type PathTimeRequest = {
|
|
412
|
+
path: string;
|
|
413
|
+
time?: { time: number; version: number; creatorId: number };
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
type ValidStateResponse = {
|
|
417
|
+
path: string;
|
|
418
|
+
time?: { time: number; version: number; creatorId: number };
|
|
419
|
+
valid: boolean | undefined;
|
|
420
|
+
isTransparent: boolean;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
class PathAuditerService {
|
|
424
|
+
public async getValidStates(requests: PathTimeRequest[]): Promise<ValidStateResponse[]> {
|
|
425
|
+
let results: ValidStateResponse[] = [];
|
|
426
|
+
for (let request of requests) {
|
|
427
|
+
let value = authorityStorage.getValueAtTime(request.path, request.time);
|
|
428
|
+
results.push({
|
|
429
|
+
path: request.path,
|
|
430
|
+
time: value?.time,
|
|
431
|
+
valid: value === undefined ? undefined : value.valid,
|
|
432
|
+
isTransparent: !!value?.isTransparent,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return results;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
public async getPathValues(requests: PathTimeRequest[]): Promise<PathValue[]> {
|
|
439
|
+
let results: PathValue[] = [];
|
|
440
|
+
for (let request of requests) {
|
|
441
|
+
let value = authorityStorage.getValueAtTime(request.path, request.time);
|
|
442
|
+
if (value) {
|
|
443
|
+
results.push(value);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return results;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
public async ingestPathValues(serializedValues: Buffer[]): Promise<void> {
|
|
450
|
+
let values = await pathValueSerializer.deserialize(serializedValues);
|
|
451
|
+
validStateComputer.ingestValuesAndValidStates({
|
|
452
|
+
pathValues: values,
|
|
453
|
+
parentSyncs: [],
|
|
454
|
+
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
public async runAuditNow_forBrowser(nodeId: string): Promise<void> {
|
|
459
|
+
await PathAuditerController.nodes[nodeId].runAuditNow();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public async runAuditNow(): Promise<void> {
|
|
463
|
+
await runAuditNowSerial();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export const PathAuditerController = SocketFunction.register(
|
|
468
|
+
"PathAuditerService-019d324f-9058-71ab-b1d0-384f9418d35b",
|
|
469
|
+
new PathAuditerService(),
|
|
470
|
+
() => ({
|
|
471
|
+
getValidStates: {},
|
|
472
|
+
getPathValues: {},
|
|
473
|
+
ingestPathValues: {},
|
|
474
|
+
runAuditNow_forBrowser: {},
|
|
475
|
+
runAuditNow: {},
|
|
476
|
+
}),
|
|
477
|
+
() => ({
|
|
478
|
+
hooks: [assertIsManagementUser],
|
|
479
|
+
})
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
registerMeasureInfo(() => {
|
|
483
|
+
if (stats.totalDiscarded === 0) return "";
|
|
484
|
+
let percentage = formatPercent(stats.totalAudited / (stats.totalAudited + stats.totalDiscarded) * 100);
|
|
485
|
+
return `AUDIT ${formatNumber(stats.totalAudited)} (${percentage}) E:${stats.errorCount} W:${stats.warningCount} F:${stats.fixedCount} `;
|
|
486
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
let callback: ((path: string, priority: 0 | 1 | 2) => void) | undefined;
|
|
2
|
+
|
|
3
|
+
let disabled = false;
|
|
4
|
+
/*
|
|
5
|
+
0: read
|
|
6
|
+
1: write
|
|
7
|
+
2: rejected
|
|
8
|
+
(higher is more important)
|
|
9
|
+
*/
|
|
10
|
+
export function onPathInteracted(path: string, priority: 0 | 1 | 2) {
|
|
11
|
+
if (disabled) return;
|
|
12
|
+
callback?.(path, priority);
|
|
13
|
+
}
|
|
14
|
+
export function setPathInteractedCallback(_callback: ((path: string, priority: 0 | 1 | 2) => void) | undefined) {
|
|
15
|
+
callback = _callback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function disablePathAuditer() {
|
|
19
|
+
disabled = true;
|
|
20
|
+
}
|
|
@@ -7,9 +7,11 @@ import { registerPeriodic } from "./periodic";
|
|
|
7
7
|
import { getOwnMachineId } from "../-a-auth/certs";
|
|
8
8
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
9
|
import { formatPercent, formatTime } from "socket-function/src/formatting/format";
|
|
10
|
-
import { pathWatcher } from "../0-path-value-core/
|
|
10
|
+
import { pathWatcher } from "../0-path-value-core/PathWatcher";
|
|
11
11
|
import { addStatPeriodic, addStatSumPeriodic, addTimeProfileDistribution, registerNodeMetadata } from "../-0-hooks/hooks";
|
|
12
12
|
import { logDisk } from "./logs/diskLogger";
|
|
13
|
+
import { getDebuggerUrl } from "./listenOnDebugger";
|
|
14
|
+
import { isPublic } from "../config";
|
|
13
15
|
|
|
14
16
|
let lastProfile: MeasureProfile | undefined;
|
|
15
17
|
|
|
@@ -87,6 +89,11 @@ function logProfileMeasuresTimingsNow() {
|
|
|
87
89
|
minTimeToLog: minTimeToLog,
|
|
88
90
|
}));
|
|
89
91
|
|
|
92
|
+
if (isNode() && !isPublic()) {
|
|
93
|
+
let debuggerValue = getDebuggerUrl();
|
|
94
|
+
process.stdout.write(`\n${debuggerValue}\n`);
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
lastProfile = profile;
|
|
91
98
|
// if (isNode()) {
|
|
92
99
|
// if (SocketFunction.mountedNodeId) {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { deepCloneJSON, throttleFunction, timeInMinute } from "socket-function/src/misc";
|
|
4
4
|
import { isNode } from "typesafecss";
|
|
5
|
-
import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
|
|
6
5
|
import { atomicObjectRead, doAtomicWrites, noAtomicSchema, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
|
|
7
6
|
import { syncSchema } from "../3-path-functions/syncSchema";
|
|
8
7
|
import { Querysub } from "../4-querysub/QuerysubController";
|
|
@@ -12,6 +11,7 @@ import { logErrors } from "../errors";
|
|
|
12
11
|
import { canHaveChildren } from "socket-function/src/types";
|
|
13
12
|
import { URLOverride } from "./ATag";
|
|
14
13
|
import { lazy } from "socket-function/src/caching";
|
|
14
|
+
import { LOCAL_DOMAIN } from "../0-path-value-core/PathRouter";
|
|
15
15
|
|
|
16
16
|
export interface URLParam<T = unknown> {
|
|
17
17
|
value: T;
|
package/src/misc/hash.ts
CHANGED
package/src/path.ts
CHANGED
|
@@ -85,6 +85,10 @@ export function getPathStr4(path0: string, path1: string, path2: string, path3:
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function appendToPathStr(pathStr: string, pathPart: string) {
|
|
88
|
+
if (pathStr[pathStr.length - 1] !== pathDelimitChar) {
|
|
89
|
+
// Might be a hacked path
|
|
90
|
+
pathStr = hack_stripPackedPath(pathStr);
|
|
91
|
+
}
|
|
88
92
|
return pathStr + escapePathPart(pathPart) + pathDelimitEscaped;
|
|
89
93
|
}
|
|
90
94
|
export function prependToPathStr(pathStr: string, pathPart: string) {
|
|
@@ -143,28 +147,38 @@ export function getPathAfterLastChild(pathStr: string) {
|
|
|
143
147
|
return pathStr.slice(0, -pathDelimitEscaped.length) + afterChildSuffix;
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
function getStartOfLastPart(pathStr: string) {
|
|
151
|
+
// I'm not sure if this is what we should be doing for hacked paths. In theory, a hacked path basically represents select these children, selecting specific children. So, in theory, that should mean... The parent is the parent of those children. However, we also use it to keep track of the parent sync, in which case we are syncing the parent. And so the parent we want, if we ever recall this, would be actually the parent of that. So I think in theory we should uncomment this code, but in practice we need to keep it commented.
|
|
152
|
+
// if(hack_isPackedPath(pathStr)) {
|
|
153
|
+
// let index = pathStr.lastIndexOf(pathDelimitEscaped);
|
|
154
|
+
// if(index < 0) return pathStr.length;
|
|
155
|
+
// return index + pathDelimitEscaped.length;
|
|
156
|
+
// }
|
|
157
|
+
let index = pathStr.lastIndexOf(pathDelimitEscaped);
|
|
158
|
+
if (index < 0) return pathStr.length;
|
|
159
|
+
let index2 = pathStr.lastIndexOf(pathDelimitEscaped, index - 1);
|
|
160
|
+
if (index2 < 0) return 0;
|
|
161
|
+
return index2 + pathDelimitEscaped.length;
|
|
162
|
+
}
|
|
163
|
+
|
|
146
164
|
/** === getPathStr(getPathFromStr(pathStr).slice(0, -1)), getParentPathStr(rootPathStr) === rootPathStr */
|
|
147
165
|
export function getParentPathStr(pathStr: string) {
|
|
148
166
|
// Allow empty string? Better than throwing on it, I guess
|
|
149
167
|
if (pathStr === "") {
|
|
150
168
|
return "";
|
|
151
169
|
}
|
|
152
|
-
// NOTE: If pathStr.length is too small, our index start becomes negative, and so this works without
|
|
153
170
|
// an additional check.
|
|
154
|
-
|
|
155
|
-
return pathStr.slice(0, index + pathDelimitEscaped.length);
|
|
171
|
+
return pathStr.slice(0, getStartOfLastPart(pathStr));
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
/** === getPathFromStr(pathStr).slice(-1)[0] */
|
|
159
175
|
export function getLastPathPart(pathStr: string) {
|
|
160
|
-
|
|
161
|
-
return unescapePathPart(pathStr.slice(index + pathDelimitEscaped.length, -pathDelimitEscaped.length));
|
|
176
|
+
return unescapePathPart(pathStr.slice(getStartOfLastPart(pathStr) - pathDelimitEscaped.length, -pathDelimitEscaped.length));
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
/** === getPathStr(getPathFromStr(pathStr).slice(0, -1)) */
|
|
165
180
|
export function removePathLastPart(pathStr: string) {
|
|
166
|
-
|
|
167
|
-
return pathStr.slice(0, index + pathDelimitEscaped.length);
|
|
181
|
+
return pathStr.slice(0, getStartOfLastPart(pathStr));
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
/** getPathDepth(pathStr) === getPathFromStr(pathStr).length */
|