querysub 0.136.0 → 0.138.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/0-path-value-core/PathValueCommitter.ts +84 -22
- package/src/0-path-value-core/PathValueController.ts +6 -6
- package/src/0-path-value-core/{debugLogs.ts → auditLogs.ts} +11 -7
- package/src/0-path-value-core/pathValueCore.ts +23 -9
- package/src/1-path-client/RemoteWatcher.ts +125 -10
- package/src/1-path-client/pathValueClientWatcher.ts +5 -5
- package/src/2-proxy/PathValueProxyWatcher.ts +5 -1
- package/src/4-deploy/deployMain.ts +0 -11
- package/src/4-deploy/edgeBootstrap.ts +3 -1
- package/src/5-diagnostics/diskValueAudit.ts +3 -3
- package/src/5-diagnostics/memoryValueAudit.ts +5 -5
- package/src/diagnostics/AuditLogPage.tsx +148 -0
- package/src/diagnostics/managementPages.tsx +8 -1
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import { errorToUndefined, logErrors, timeoutToUndefined } from "../errors";
|
|
|
11
11
|
import { getPathFromStr } from "../path";
|
|
12
12
|
import { nodePathAuthority, pathValueAuthority2 } from "./NodePathAuthorities";
|
|
13
13
|
import { PathValueController } from "./PathValueController";
|
|
14
|
-
import { PathValue, MAX_ACCEPTED_CHANGE_AGE, lockWatcher, authorityStorage, writeValidStorage, WriteState, lockToCallback, pathWatcher, MAX_CHANGE_AGE, debugPathValue, debugPathValuePath } from "./pathValueCore";
|
|
14
|
+
import { PathValue, MAX_ACCEPTED_CHANGE_AGE, lockWatcher, authorityStorage, writeValidStorage, WriteState, lockToCallback, pathWatcher, MAX_CHANGE_AGE, debugPathValue, debugPathValuePath, compareTime, epochTime } from "./pathValueCore";
|
|
15
15
|
import debugbreak from "debugbreak";
|
|
16
16
|
import { red } from "socket-function/src/formatting/logColors";
|
|
17
17
|
import { registerDynamicResource } from "../diagnostics/trackResources";
|
|
@@ -20,7 +20,7 @@ import { formatStats } from "socket-function/src/profiling/statsFormat";
|
|
|
20
20
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
21
21
|
import { isClient } from "../config2";
|
|
22
22
|
import { remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
23
|
-
import {
|
|
23
|
+
import { auditLog, isDebugLogEnabled } from "./auditLogs";
|
|
24
24
|
import { diskLog } from "../diagnostics/logs/diskLogger";
|
|
25
25
|
|
|
26
26
|
/*
|
|
@@ -130,7 +130,7 @@ class PathValueCommitter {
|
|
|
130
130
|
let valuesPerOtherAuthority = new Map<string, PathValue[]>();
|
|
131
131
|
for (let pathValue of values) {
|
|
132
132
|
if (isDebugLogEnabled()) {
|
|
133
|
-
|
|
133
|
+
auditLog("CREATE VALUE", { path: pathValue.path, time: pathValue.time.time });
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
let otherAuthorities = await pathValueAuthority2.getWriteNodes(pathValue.path);
|
|
@@ -233,13 +233,13 @@ class PathValueCommitter {
|
|
|
233
233
|
sourceNodeId: string;
|
|
234
234
|
initialTrigger?: "initialTrigger";
|
|
235
235
|
}[]) => {
|
|
236
|
-
let initialTriggers = { values: new Set<
|
|
236
|
+
let initialTriggers = { values: new Set<string>(), parentPaths: new Set<string>() };
|
|
237
237
|
// Check received values to make sure the authority for them is the same as the sourceNodeId
|
|
238
238
|
// (OR that WE are the authority). If not... it means we received values after changing the watcher,
|
|
239
239
|
// which requires us to ignore the other values
|
|
240
240
|
// (Clients get most of their values from the same server, so they shouldn't require this check)
|
|
241
|
+
const self = this;
|
|
241
242
|
if (!isClient()) {
|
|
242
|
-
const self = this;
|
|
243
243
|
measureBlock(function ignoreUnrequestedValues() {
|
|
244
244
|
for (let batch of batched) {
|
|
245
245
|
batch.pathValues = batch.pathValues.filter(value => {
|
|
@@ -255,8 +255,16 @@ class PathValueCommitter {
|
|
|
255
255
|
// When we start watching, those values will get clobbered.
|
|
256
256
|
if (watchingAuthorityId === undefined) return true;
|
|
257
257
|
if (watchingAuthorityId === batch.sourceNodeId) return true;
|
|
258
|
+
// epochTimes are just indicators that the value has no value, and so are safe to sync
|
|
259
|
+
// from any source. They won't cause future conflicts, because any other value overrides them.
|
|
260
|
+
// - This might not be required?
|
|
261
|
+
if (compareTime(value.time, epochTime) === 0) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
258
265
|
// Also warn, because... if we get a lot of these, there might be a bug.
|
|
259
266
|
// A few when we change watches is possible, but it should be rare.
|
|
267
|
+
auditLog("IGNORING VALUE FROM DIFFERENT AUTHORITY", { path: value.path, watchingAuthorityId, receivedFromAuthority: batch.sourceNodeId });
|
|
260
268
|
console.warn(`Ignoring a value that was received from a different authority than the one we are watching. Path: ${value.path}, Watching Authority (should be source): ${watchingAuthorityId}, Actual Source Authority: ${batch.sourceNodeId}`);
|
|
261
269
|
return false;
|
|
262
270
|
});
|
|
@@ -264,29 +272,84 @@ class PathValueCommitter {
|
|
|
264
272
|
});
|
|
265
273
|
}
|
|
266
274
|
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
let specialInitialParentCausedRejections: WriteState[] = [];
|
|
276
|
+
|
|
277
|
+
// The first time we receive value for a watch from a specific node, we need to reject future
|
|
278
|
+
// missing values (unless we are the authority). Otherwise bad rejected values can stick around,
|
|
269
279
|
// because they are newer, and our new source doesn't know we have them.
|
|
270
280
|
measureBlock(function resetOnInitialTrigger() {
|
|
271
281
|
for (let batch of batched) {
|
|
272
282
|
if (!batch.initialTrigger) continue;
|
|
273
283
|
for (let pathValue of batch.pathValues) {
|
|
274
|
-
initialTriggers.values.add(pathValue);
|
|
275
|
-
|
|
284
|
+
initialTriggers.values.add(pathValue.path);
|
|
285
|
+
auditLog("INITIAL SYNC", { path: pathValue.path, time: pathValue.time.time, sourceNodeId: batch.sourceNodeId });
|
|
286
|
+
// We shouldn't receive the initialTrigger flag if we are the authority anyways
|
|
287
|
+
if (nodePathAuthority.isSelfAuthority(pathValue.path)) continue;
|
|
288
|
+
let history = authorityStorage.getValuePlusHistory(pathValue.path);
|
|
289
|
+
for (let value of history) {
|
|
290
|
+
if (compareTime(value.time, pathValue.time) > 0) {
|
|
291
|
+
specialInitialParentCausedRejections.push({
|
|
292
|
+
path: value.path,
|
|
293
|
+
time: value.time,
|
|
294
|
+
isValid: false,
|
|
295
|
+
isTransparent: !!value.isTransparent,
|
|
296
|
+
reason: "future missing (initial trigger)",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
276
300
|
}
|
|
277
|
-
//
|
|
278
|
-
//
|
|
301
|
+
// IF the parent is synced, but we don't receive a path value, then invalidate the
|
|
302
|
+
// old PathValue (as this means it is invalid, but the remote doesn't know to
|
|
303
|
+
// tell us it is invalid, as we only watched the parent, not the specific path).
|
|
304
|
+
// - If the values become unrejected in the future, we will receive them again, which
|
|
305
|
+
// will cause their valid state to be changed to accepted.
|
|
306
|
+
// - Also, if another batch sets the value, it will override our valid state write,
|
|
307
|
+
// as we are writing this in a special array will is always handled first.
|
|
279
308
|
if (batch.parentsSynced) {
|
|
280
309
|
for (let parentPath of batch.parentsSynced) {
|
|
281
|
-
|
|
310
|
+
auditLog("INITIAL PARENT SYNC", { path: parentPath, sourceNodeId: batch.sourceNodeId });
|
|
282
311
|
let allChildPaths = authorityStorage.getPathsFromParent(parentPath) || [];
|
|
283
312
|
let childChecker = remoteWatcher.getChildPatchWatchChecker({
|
|
284
313
|
parentPath,
|
|
285
314
|
nodeId: batch.sourceNodeId
|
|
286
315
|
});
|
|
287
316
|
for (let childPath of allChildPaths) {
|
|
288
|
-
if (childChecker.isWatchedByNodeId(childPath))
|
|
289
|
-
|
|
317
|
+
if (!childChecker.isWatchedByNodeId(childPath)) continue;
|
|
318
|
+
// We shouldn't receive the initialTrigger flag if we are the authority anyways.
|
|
319
|
+
if (nodePathAuthority.isSelfAuthority(childPath)) continue;
|
|
320
|
+
initialTriggers.parentPaths.add(childPath);
|
|
321
|
+
|
|
322
|
+
// NOTE: On initial trigger, we send all child paths, so they should be
|
|
323
|
+
// in the values list, if they exist. AND if we receive them after,
|
|
324
|
+
// they will clobber this rejection
|
|
325
|
+
// - See PathWatcher.watchPath, where when !noInitialTrigger (aka, initialTrigger),
|
|
326
|
+
// we get all paths via authorityStorage.getPathsFromParent, and send their values.
|
|
327
|
+
if (initialTriggers.values.has(childPath)) continue;
|
|
328
|
+
|
|
329
|
+
let values = authorityStorage.getValuePlusHistory(childPath);
|
|
330
|
+
if (values.length === 0) continue;
|
|
331
|
+
|
|
332
|
+
values = values.filter(value => {
|
|
333
|
+
// DO NOT reject epoch values. For missing values, we still need a signal, and the parent
|
|
334
|
+
// sync won't send this, so we need to NOT reject them.
|
|
335
|
+
if (compareTime(value.time, epochTime) === 0) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
});
|
|
340
|
+
if (values.length === 0) continue;
|
|
341
|
+
|
|
342
|
+
debugbreak(2);
|
|
343
|
+
debugger;
|
|
344
|
+
console.log(self.getExistingWatchRemoteNodeId(childPath));
|
|
345
|
+
for (let value of values) {
|
|
346
|
+
specialInitialParentCausedRejections.push({
|
|
347
|
+
path: value.path,
|
|
348
|
+
time: value.time,
|
|
349
|
+
isValid: false,
|
|
350
|
+
isTransparent: !!value.isTransparent,
|
|
351
|
+
reason: "child missing (initial trigger)",
|
|
352
|
+
});
|
|
290
353
|
}
|
|
291
354
|
}
|
|
292
355
|
}
|
|
@@ -326,10 +389,12 @@ class PathValueCommitter {
|
|
|
326
389
|
pathValues = pathValues.filter(value => {
|
|
327
390
|
if (!nodePathAuthority.isSelfAuthority(value.path)) return true;
|
|
328
391
|
if (value.time.time < maxAge) {
|
|
392
|
+
auditLog("DISCARD INVALID GENESIS VALUE", { path: value.path, time: value.time.time });
|
|
329
393
|
console.error(red(`Ignoring a value that is ${formatTime(Date.now() - value.time.time)} pass change time, which would break assumptions made by archives / locks about change stability. ${debugPathValuePath(value)}`));
|
|
330
394
|
return false;
|
|
331
395
|
}
|
|
332
396
|
if (value.time.time > maxFutureAge) {
|
|
397
|
+
auditLog("DISCARD INVALID FUTURE VALUE", { path: value.path, time: value.time.time });
|
|
333
398
|
console.error(red(`Ignoring a value that is ${formatTime(value.time.time - Date.now())} in the future, which would break writes to that path until that time passes. ${debugPathValuePath(value)}`));
|
|
334
399
|
return false;
|
|
335
400
|
}
|
|
@@ -338,7 +403,7 @@ class PathValueCommitter {
|
|
|
338
403
|
});
|
|
339
404
|
|
|
340
405
|
// NOTE: Values MUST be synchronously ingested, otherwise our "initialTrigger" resetting will break things.
|
|
341
|
-
this.ingestValues(pathValues, parentsSynced, parentSyncedSources, initialTriggers);
|
|
406
|
+
this.ingestValues(pathValues, parentsSynced, parentSyncedSources, initialTriggers, specialInitialParentCausedRejections);
|
|
342
407
|
// Wait a microtick to allow triggered watchers to finish.
|
|
343
408
|
await Promise.resolve();
|
|
344
409
|
if (!isNode()) {
|
|
@@ -355,7 +420,8 @@ class PathValueCommitter {
|
|
|
355
420
|
pathValues: PathValue[],
|
|
356
421
|
parentsSynced: string[] | undefined,
|
|
357
422
|
parentSyncedSources: Map<string, string[]> | undefined,
|
|
358
|
-
initialTriggers?: { values: Set<
|
|
423
|
+
initialTriggers?: { values: Set<string>; parentPaths: Set<string> },
|
|
424
|
+
specialInitialParentCausedRejections?: WriteState[],
|
|
359
425
|
): void {
|
|
360
426
|
if (pathValues.length === 0 && !parentsSynced?.length) return;
|
|
361
427
|
|
|
@@ -370,7 +436,7 @@ class PathValueCommitter {
|
|
|
370
436
|
}
|
|
371
437
|
|
|
372
438
|
// NOTE: Also triggers rejections of watched paths
|
|
373
|
-
this.ingestValidStates([], parentsSynced, undefined, pathValues, initialTriggers);
|
|
439
|
+
this.ingestValidStates(specialInitialParentCausedRejections ?? [], parentsSynced, undefined, pathValues, initialTriggers);
|
|
374
440
|
}
|
|
375
441
|
|
|
376
442
|
// NOTE: Technically, because we trigger all remotes any time a value changes, even
|
|
@@ -385,14 +451,10 @@ class PathValueCommitter {
|
|
|
385
451
|
parentsSynced?: string[],
|
|
386
452
|
recomputeValidState?: "recomputeValidState",
|
|
387
453
|
alsoPathValues?: PathValue[],
|
|
388
|
-
initialTriggers?: { values: Set<
|
|
454
|
+
initialTriggers?: { values: Set<string>; parentPaths: Set<string> }
|
|
389
455
|
) {
|
|
390
456
|
if (remoteLocksChanged.length === 0 && !parentsSynced?.length && !alsoPathValues?.length) return;
|
|
391
457
|
|
|
392
|
-
if (alsoPathValues?.some(x => x.path.includes("#BREAK"))) {
|
|
393
|
-
debugger;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
458
|
let now = Date.now();
|
|
397
459
|
|
|
398
460
|
let valuesChanged = new Set<PathValue>();
|
|
@@ -14,7 +14,7 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
|
|
|
14
14
|
import { sha256 } from "js-sha256";
|
|
15
15
|
import debugbreak from "debugbreak";
|
|
16
16
|
import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
|
|
17
|
-
import {
|
|
17
|
+
import { auditLog, isDebugLogEnabled } from "./auditLogs";
|
|
18
18
|
import { debugNodeId } from "../-c-identity/IdentityController";
|
|
19
19
|
import { diskLog } from "../diagnostics/logs/diskLogger";
|
|
20
20
|
export { pathValueCommitter };
|
|
@@ -83,7 +83,7 @@ class PathValueControllerBase {
|
|
|
83
83
|
if (isDebugLogEnabled()) {
|
|
84
84
|
let sourceNodeId = debugNodeId(callerId);
|
|
85
85
|
for (let value of values) {
|
|
86
|
-
|
|
86
|
+
auditLog("RECEIVE VALUE", { path: value.path, time: value.time.time, sourceNodeId });
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
diskLog(`Received PathValues via forwardWrites`, { valueCount: values.length, callerId, });
|
|
@@ -178,10 +178,10 @@ class PathValueControllerBase {
|
|
|
178
178
|
if (isDebugLogEnabled()) {
|
|
179
179
|
let sourceNodeId = debugNodeId(callerId);
|
|
180
180
|
for (let value of config.paths) {
|
|
181
|
-
|
|
181
|
+
auditLog("WATCH PATH", { path: value, sourceNodeId });
|
|
182
182
|
}
|
|
183
183
|
for (let value of config.parentPaths) {
|
|
184
|
-
|
|
184
|
+
auditLog("WATCH PARENT PATH", { path: value, sourceNodeId });
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
pathWatcher.watchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId, initialTrigger: true });
|
|
@@ -191,10 +191,10 @@ class PathValueControllerBase {
|
|
|
191
191
|
if (isDebugLogEnabled()) {
|
|
192
192
|
let sourceNodeId = debugNodeId(SocketFunction.getCaller().nodeId);
|
|
193
193
|
for (let value of config.paths) {
|
|
194
|
-
|
|
194
|
+
auditLog("UNWATCH PATH", { path: value, sourceNodeId });
|
|
195
195
|
}
|
|
196
196
|
for (let value of config.parentPaths) {
|
|
197
|
-
|
|
197
|
+
auditLog("UNWATCH PARENT PATH", { path: value, sourceNodeId });
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
pathWatcher.unwatchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId });
|
|
@@ -4,18 +4,20 @@ import { isDevDebugbreak } from "../config";
|
|
|
4
4
|
import { measureWrap } from "socket-function/src/profiling/measure";
|
|
5
5
|
import { QueueLimited } from "socket-function/src/misc";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export type DebugLog = {
|
|
8
8
|
type: string;
|
|
9
9
|
time: number;
|
|
10
10
|
values: { [key: string]: unknown };
|
|
11
|
-
}
|
|
11
|
+
};
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// NOTE: For now, this is actually fairly light, so we'll just turn it on by default. It is very useful
|
|
14
|
+
// in debugging synchronization issues.
|
|
15
|
+
let ENABLED_LOGGING = true;
|
|
16
|
+
export function enableAuditLogging() {
|
|
15
17
|
ENABLED_LOGGING = true;
|
|
16
18
|
debugLogFnc = debugLogBase;
|
|
17
19
|
}
|
|
18
|
-
export function
|
|
20
|
+
export function disableAuditLogging() {
|
|
19
21
|
ENABLED_LOGGING = false;
|
|
20
22
|
debugLogFnc = NOOP;
|
|
21
23
|
}
|
|
@@ -32,20 +34,22 @@ export function getLogHistoryIncludes(includes: string) {
|
|
|
32
34
|
return Object.values(x.values).some(y => String(y).includes(includes));
|
|
33
35
|
});
|
|
34
36
|
}
|
|
37
|
+
(globalThis as any).getLogHistoryEquals = getLogHistoryEquals;
|
|
38
|
+
(globalThis as any).getPathAuditLogs = getLogHistoryEquals;
|
|
35
39
|
export function getLogHistoryEquals(value: string) {
|
|
36
40
|
return logHistory.getAllUnordered().filter(x => {
|
|
37
41
|
return Object.values(x.values).some(y => y === value);
|
|
38
42
|
});
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
export function
|
|
45
|
+
export function auditLog(type: string, values: { [key: string]: unknown }) {
|
|
42
46
|
debugLogFnc(type, values);
|
|
43
47
|
}
|
|
44
48
|
let debugLogFnc = debugLogBase;
|
|
45
49
|
let NOOP = () => { };
|
|
46
50
|
function debugLogBase(type: string, values: { [key: string]: unknown }) {
|
|
47
51
|
if (!ENABLED_LOGGING) {
|
|
48
|
-
|
|
52
|
+
disableAuditLogging();
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
51
55
|
let newEntry: DebugLog = { type, time: Date.now(), values };
|
|
@@ -28,7 +28,7 @@ import yargs from "yargs";
|
|
|
28
28
|
import { sha256 } from "js-sha256";
|
|
29
29
|
import { PromiseObj } from "../promise";
|
|
30
30
|
import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
|
|
31
|
-
import {
|
|
31
|
+
import { auditLog, isDebugLogEnabled } from "./auditLogs";
|
|
32
32
|
import { diskLog } from "../diagnostics/logs/diskLogger";
|
|
33
33
|
|
|
34
34
|
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
@@ -207,6 +207,7 @@ export type WriteState = {
|
|
|
207
207
|
// - HOWEVER, writers should just be smart, as it causes a lot of thrashing and work
|
|
208
208
|
// on all the clients per write, even if nothing changes!
|
|
209
209
|
isTransparent: boolean;
|
|
210
|
+
reason?: string;
|
|
210
211
|
};
|
|
211
212
|
|
|
212
213
|
// NOTE: Object values ARE allowed here. They are effectively immutable though, so it is recommended
|
|
@@ -679,7 +680,7 @@ class AuthorityPathValueStorage {
|
|
|
679
680
|
|
|
680
681
|
public resetForInitialTrigger(path: string) {
|
|
681
682
|
if (isDebugLogEnabled()) {
|
|
682
|
-
|
|
683
|
+
auditLog("REMOVE FOR INITIAL SYNC", { path });
|
|
683
684
|
}
|
|
684
685
|
this.removePathFromStorage(path, "reset for initial trigger");
|
|
685
686
|
}
|
|
@@ -720,12 +721,20 @@ class AuthorityPathValueStorage {
|
|
|
720
721
|
this.getMultiNodesForParent = fnc;
|
|
721
722
|
}
|
|
722
723
|
@measureFnc
|
|
723
|
-
public ingestValues(
|
|
724
|
+
public ingestValues(
|
|
725
|
+
newValues: PathValue[],
|
|
726
|
+
parentsSynced: string[] | undefined,
|
|
727
|
+
parentSyncedSources: Map<string, string[]> | undefined,
|
|
728
|
+
// Skips archiving (used if we received the values from another node, in which case
|
|
729
|
+
// not only should the values already be archives, they are likely too old to be archives
|
|
730
|
+
// again!)
|
|
731
|
+
skipArchive?: "skipArchive"
|
|
732
|
+
) {
|
|
724
733
|
let now = Date.now();
|
|
725
734
|
|
|
726
735
|
if (isDebugLogEnabled()) {
|
|
727
736
|
for (let value of newValues) {
|
|
728
|
-
|
|
737
|
+
auditLog("INGEST VALUE", { path: value.path, time: value.time.time });
|
|
729
738
|
}
|
|
730
739
|
}
|
|
731
740
|
|
|
@@ -762,7 +771,7 @@ class AuthorityPathValueStorage {
|
|
|
762
771
|
this.possiblyUndefinedPaths().value.add(value.path);
|
|
763
772
|
}
|
|
764
773
|
|
|
765
|
-
if (!value.event && !value.path.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
774
|
+
if (!skipArchive && !value.event && !value.path.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
766
775
|
let authorityPath = pathValueAuthority2.getSelfArchiveAuthority(value.path);
|
|
767
776
|
|
|
768
777
|
// Store in pending archive lookup
|
|
@@ -983,7 +992,7 @@ class AuthorityPathValueStorage {
|
|
|
983
992
|
this.values.delete(path);
|
|
984
993
|
|
|
985
994
|
if (isDebugLogEnabled()) {
|
|
986
|
-
|
|
995
|
+
auditLog("REMOVE PATH", { path, reason });
|
|
987
996
|
}
|
|
988
997
|
|
|
989
998
|
// NOTE: parentPathLookup is important because clients often use Object.keys() to get paths,
|
|
@@ -1348,7 +1357,7 @@ class PathWatcher {
|
|
|
1348
1357
|
}
|
|
1349
1358
|
}
|
|
1350
1359
|
|
|
1351
|
-
public triggerValuesChanged(valuesChanged: Set<PathValue>, parentsSynced?: string[], initialTriggers?: { values: Set<
|
|
1360
|
+
public triggerValuesChanged(valuesChanged: Set<PathValue>, parentsSynced?: string[], initialTriggers?: { values: Set<string>; parentPaths: Set<string> }) {
|
|
1352
1361
|
let changedPerCallbacks = this.getWatchers(valuesChanged, { parentsSynced, pathsAreSynced: true });
|
|
1353
1362
|
for (let [watch, changes] of changedPerCallbacks) {
|
|
1354
1363
|
if (initialTriggers) {
|
|
@@ -1357,7 +1366,7 @@ class PathWatcher {
|
|
|
1357
1366
|
let valuesOther = new Set<PathValue>();
|
|
1358
1367
|
let parentsOther = new Set<string>();
|
|
1359
1368
|
for (let value of changes) {
|
|
1360
|
-
if (initialTriggers.values.has(value)) {
|
|
1369
|
+
if (initialTriggers.values.has(value.path)) {
|
|
1361
1370
|
valuesFromInitialTrigger.add(value);
|
|
1362
1371
|
} else {
|
|
1363
1372
|
valuesOther.add(value);
|
|
@@ -1486,7 +1495,7 @@ class PathWatcher {
|
|
|
1486
1495
|
if (isDebugLogEnabled()) {
|
|
1487
1496
|
for (let pathValue of changes) {
|
|
1488
1497
|
|
|
1489
|
-
|
|
1498
|
+
auditLog("SEND VALUE", { path: pathValue.path, time: pathValue.time.time, nodeId: debugNodeId(watcher), transparent: pathValue.isTransparent, canGC: pathValue.canGCValue });
|
|
1490
1499
|
}
|
|
1491
1500
|
}
|
|
1492
1501
|
await PathValueController.nodes[watcher].forwardWrites(
|
|
@@ -1677,6 +1686,11 @@ class WriteValidStorage {
|
|
|
1677
1686
|
});
|
|
1678
1687
|
}
|
|
1679
1688
|
public setWriteValidState(write: WriteState, lockless?: boolean): boolean {
|
|
1689
|
+
if (write.isValid) {
|
|
1690
|
+
auditLog(write.reason || "ACCEPTING VALUE", { path: write.path, time: write.time.time });
|
|
1691
|
+
} else {
|
|
1692
|
+
auditLog(write.reason || "REJECTING VALUE", { path: write.path, time: write.time.time });
|
|
1693
|
+
}
|
|
1680
1694
|
this.ensureGarbageCollectOldState();
|
|
1681
1695
|
|
|
1682
1696
|
// If lockless then it starts valid, and always stays valid.
|
|
@@ -17,7 +17,66 @@ import { appendToPathStr, getParentPathStr, getPathDepth, getPathIndexAssert, ge
|
|
|
17
17
|
import { isEmpty } from "../misc";
|
|
18
18
|
import debugbreak from "debugbreak";
|
|
19
19
|
import { isDevDebugbreak } from "../config";
|
|
20
|
-
import {
|
|
20
|
+
import { auditLog } from "../0-path-value-core/auditLogs";
|
|
21
|
+
|
|
22
|
+
//todonext
|
|
23
|
+
// BOTH PathValueServers crashed due to trying to archive a value that is too far in the past.
|
|
24
|
+
// - Ah, that would happen if we did an audit, so... let's not archive auditted values.
|
|
25
|
+
|
|
26
|
+
//todonext
|
|
27
|
+
// Ugh... now all paths are failing to sync?
|
|
28
|
+
// ".,querysub._com.,PathFunctionRunner.,book3_0.,Data.,data.,sliftist@gmail._com.,books.,bb1c01e5fb3be5828._querysub._com_1712516071167._0005.,pages.,1728369494296._706_0._30634508051801834.,items.,"
|
|
29
|
+
|
|
30
|
+
//todonext
|
|
31
|
+
// OH! Right, if the value doesn't exist...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// This path was received, but ignored, because apparently t
|
|
35
|
+
|
|
36
|
+
//todonext
|
|
37
|
+
// Alright, failing to sync paths is easy
|
|
38
|
+
// - Let's first see if it's on the client
|
|
39
|
+
// - We do receive the value, so... it's a clientside issue
|
|
40
|
+
// ".,querysub._com.,PathFunctionRunner.,book3_0.,Data.,data.,sliftist@gmail._com.,books.,bb1c01e5fb3be5828._querysub._com_1712516071167._0005.,pages.,1728369494296._706_0._30634508051801834.,items.,hasOwnProperty.,"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
//todonext
|
|
44
|
+
// BUG! Every time on load (after 15 seconds), we fail to sync UndoWatch. This shouldn't be happening...
|
|
45
|
+
// - It looks like we are trying to load a lot of nested values, which should be atomic? So... why is our schema
|
|
46
|
+
// not working, and even if it isn't... why don't we receive the values eventually?
|
|
47
|
+
// - https://127-0-0-1.querysub.com:1111/?managementpage=AuditLogPage&selected=%221728769700117.415_0.13356892448729063%22&book=bb1c01e5fb3be5828.querysub.com_1712516071167.0005&path=.%2Cquerysub._com.%2CPathFunctionRunner.%2Cbook3_0.%2CData.%2Cdata.%2Csliftist%40gmail._com.%2Cbooks.%2Cbb1c01e5fb3be5828._querysub._com_1712516073485._0002.%2Cpages.%2Cb5da74489986c2f47._querysub._com_1708134597635._0002.%2Citems.%2C0.%2C
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
//todonext
|
|
51
|
+
// BUG! Our predictions seem to be rejected for a frame before we get the new value. Our predictions
|
|
52
|
+
// can't break like this for such a simple thing...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
//todonext
|
|
56
|
+
// Yep, found the bug. We were missing child values in the sync due to having them split between two authorities,
|
|
57
|
+
// but the parent sent both, so it's got lost, and then it deleted the other paths because it figured
|
|
58
|
+
// they are erroneous (as in, bad predictions).
|
|
59
|
+
// - The fix was basically just updating the code that gets the remote node id to use the parent syncer instead
|
|
60
|
+
|
|
61
|
+
//todonext
|
|
62
|
+
// Ugh... we failed to load a value, after starting on the main book page, and then drilling down. And then
|
|
63
|
+
// we loaded it on server restart. UGH... we might be ignoring values we shouldn't...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
//todonext
|
|
68
|
+
// Enable this flag, and verify it stops double sending of values
|
|
69
|
+
// Ex, of this path:
|
|
70
|
+
// https://127-0-0-1.querysub.com:1111/?managementpage=AuditLogPage&selected=%221728769700117.415_0.13356892448729063%22&path=.%2Cquerysub._com.%2CPathFunctionRunner.%2Cbook3_0.%2CData.%2Cdata.%2Csliftist%40gmail._com.%2Cbooks.%2Cbb1c01e5fb3be5828._querysub._com_1712516073485._0002.%2Cpages.%2Cb5da74489986c2f47._querysub._com_1708134597635._0002.%2Citems.%2C0.%2C
|
|
71
|
+
//todonext
|
|
72
|
+
// Add a breakpoint at "IGNORING VALUE..." when we turn stop double sends on. Presently, this happens
|
|
73
|
+
// a lot, due to double syncing. BUT, once we stop double syncing... this should mostly go away.
|
|
74
|
+
const STOP_KEYS_DOUBLE_SENDS = false;
|
|
75
|
+
|
|
76
|
+
//todonext;
|
|
77
|
+
// DEPLOY (do-update AND yarn deploy), and then test some more!
|
|
78
|
+
// - Also get archive.org to recache our site, as we changed our bootstrapper
|
|
79
|
+
|
|
21
80
|
|
|
22
81
|
export class RemoteWatcher {
|
|
23
82
|
public static DEBUG = false;
|
|
@@ -33,6 +92,9 @@ export class RemoteWatcher {
|
|
|
33
92
|
start: number;
|
|
34
93
|
end: number;
|
|
35
94
|
}>;
|
|
95
|
+
// Watches which we suppressed, due to this parent sync. If this parent sync stops,
|
|
96
|
+
// we will need to re-send the watches.
|
|
97
|
+
suppressedWatches: Set<string>;
|
|
36
98
|
fullNodeCount: number;
|
|
37
99
|
}>());
|
|
38
100
|
|
|
@@ -154,6 +216,8 @@ export class RemoteWatcher {
|
|
|
154
216
|
private internalWatchLatest(config: WatchConfig & {
|
|
155
217
|
debugName?: string;
|
|
156
218
|
tryReconnect?: boolean;
|
|
219
|
+
// Watch, even if we were already watching
|
|
220
|
+
forceWatch?: boolean;
|
|
157
221
|
}) {
|
|
158
222
|
let watchesPerAuthority = new Map<string, { paths: string[]; parentPaths: string[] }>();
|
|
159
223
|
|
|
@@ -173,6 +237,14 @@ export class RemoteWatcher {
|
|
|
173
237
|
// to come back out of order, causing watching functions to trigger out of order!
|
|
174
238
|
let remoteAuthorityId = this.remoteWatchPaths.get(path);
|
|
175
239
|
if (remoteAuthorityId !== undefined) {
|
|
240
|
+
if (config.forceWatch) {
|
|
241
|
+
let watchesPer = watchesPerAuthority.get(remoteAuthorityId);
|
|
242
|
+
if (!watchesPer) {
|
|
243
|
+
watchesPer = { paths: [], parentPaths: [] };
|
|
244
|
+
watchesPerAuthority.set(remoteAuthorityId, watchesPer);
|
|
245
|
+
}
|
|
246
|
+
watchesPer.paths.push(path);
|
|
247
|
+
}
|
|
176
248
|
this.disconnectedPaths.delete(path);
|
|
177
249
|
continue;
|
|
178
250
|
}
|
|
@@ -187,9 +259,11 @@ export class RemoteWatcher {
|
|
|
187
259
|
if (!this.disconnectedPaths.has(path)) {
|
|
188
260
|
newDisconnectPaths++;
|
|
189
261
|
this.disconnectedPaths.add(path);
|
|
262
|
+
auditLog("NO AUTHORITY FOR PATH", { path });
|
|
190
263
|
}
|
|
191
264
|
continue;
|
|
192
265
|
}
|
|
266
|
+
auditLog("FOUND AUTHORITY FOR PATH", { path, authorityId });
|
|
193
267
|
this.disconnectedPaths.delete(path);
|
|
194
268
|
foundPaths++;
|
|
195
269
|
|
|
@@ -281,7 +355,7 @@ export class RemoteWatcher {
|
|
|
281
355
|
}
|
|
282
356
|
|
|
283
357
|
if (!watchObj) {
|
|
284
|
-
watchObj = { nodesId: new Map(), fullNodeCount: 0 };
|
|
358
|
+
watchObj = { nodesId: new Map(), fullNodeCount: 0, suppressedWatches: new Set() };
|
|
285
359
|
this.remoteWatchParents.set(path, watchObj);
|
|
286
360
|
}
|
|
287
361
|
watchObj.fullNodeCount = nodes.length;
|
|
@@ -335,13 +409,23 @@ export class RemoteWatcher {
|
|
|
335
409
|
|
|
336
410
|
if (isDevDebugbreak()) {
|
|
337
411
|
for (let path of paths) {
|
|
338
|
-
|
|
412
|
+
auditLog("remoteWatcher outer WATCH", { path, remoteNodeId: authorityId });
|
|
339
413
|
}
|
|
340
414
|
for (let path of parentPaths) {
|
|
341
|
-
|
|
415
|
+
auditLog("remoteWatcher outer WATCH PARENT", { path, remoteNodeId: authorityId });
|
|
342
416
|
}
|
|
343
417
|
}
|
|
344
418
|
|
|
419
|
+
if (STOP_KEYS_DOUBLE_SENDS) {
|
|
420
|
+
paths = paths.filter(path => {
|
|
421
|
+
let parentPath = getParentPathStr(path);
|
|
422
|
+
let parentObj = this.remoteWatchParents.get(parentPath);
|
|
423
|
+
if (!parentObj) return true;
|
|
424
|
+
parentObj.suppressedWatches.add(path);
|
|
425
|
+
return false;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
345
429
|
this.reconnectCache(authorityId);
|
|
346
430
|
// Don't throw, as the reconnectCache will eventually resolve errors and watch successfully
|
|
347
431
|
logErrors(this.watchLatestRemote({
|
|
@@ -407,10 +491,10 @@ export class RemoteWatcher {
|
|
|
407
491
|
for (let [authorityId, { paths, parentPaths }] of byAuthority) {
|
|
408
492
|
if (isDevDebugbreak()) {
|
|
409
493
|
for (let path of paths) {
|
|
410
|
-
|
|
494
|
+
auditLog("remoteWatcher inner WATCH", { path, remoteNodeId: authorityId });
|
|
411
495
|
}
|
|
412
496
|
for (let path of parentPaths) {
|
|
413
|
-
|
|
497
|
+
auditLog("remoteWatcher inner WATCH PARENT", { path, remoteNodeId: authorityId });
|
|
414
498
|
}
|
|
415
499
|
}
|
|
416
500
|
// NOTE: We log watches here because we want to log batched watches
|
|
@@ -460,7 +544,6 @@ export class RemoteWatcher {
|
|
|
460
544
|
};
|
|
461
545
|
const innerParentPathRemove = (path: string, remoteAuthorityId: string | undefined) => {
|
|
462
546
|
if (remoteAuthorityId === undefined) return;
|
|
463
|
-
this.remoteWatchParents.delete(path);
|
|
464
547
|
this.disconnectedParents.delete(path);
|
|
465
548
|
let paths = unwatchesPerAuthority.get(remoteAuthorityId);
|
|
466
549
|
if (!paths) {
|
|
@@ -474,21 +557,36 @@ export class RemoteWatcher {
|
|
|
474
557
|
let remoteAuthorityId = this.remoteWatchPaths.get(path);
|
|
475
558
|
innerPathRemove(path, remoteAuthorityId);
|
|
476
559
|
}
|
|
560
|
+
let suppressedRewatches = new Set<string>();
|
|
477
561
|
for (let path of config.parentPaths) {
|
|
478
562
|
let watchObj = this.remoteWatchParents.get(path);
|
|
479
563
|
if (!watchObj) continue;
|
|
564
|
+
// Rewatch anything that we suppressed watching (which happens when STOP_KEYS_DOUBLE_SENDS is set)
|
|
565
|
+
for (let suppressedWatch of watchObj.suppressedWatches) {
|
|
566
|
+
// Only if we are still watching it (which most of the time, we won't be)
|
|
567
|
+
if (!this.remoteWatchPaths.has(suppressedWatch)) continue;
|
|
568
|
+
suppressedRewatches.add(suppressedWatch);
|
|
569
|
+
}
|
|
480
570
|
for (let authorityId of watchObj.nodesId.keys()) {
|
|
481
571
|
innerParentPathRemove(path, authorityId);
|
|
482
572
|
}
|
|
483
573
|
this.remoteWatchParents.delete(path);
|
|
484
574
|
}
|
|
575
|
+
if (suppressedRewatches.size > 0) {
|
|
576
|
+
this.internalWatchLatest({
|
|
577
|
+
paths: Array.from(suppressedRewatches),
|
|
578
|
+
parentPaths: [],
|
|
579
|
+
debugName: "watchSuppressedWatches",
|
|
580
|
+
forceWatch: true,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
485
583
|
for (let [authorityIdBase, { paths, parentPaths }] of unwatchesPerAuthority.entries()) {
|
|
486
584
|
if (isDevDebugbreak()) {
|
|
487
585
|
for (let path of paths) {
|
|
488
|
-
|
|
586
|
+
auditLog("remoteWatcher inner UNWATCH", { path, remoteNodeId: authorityIdBase });
|
|
489
587
|
}
|
|
490
588
|
for (let path of parentPaths) {
|
|
491
|
-
|
|
589
|
+
auditLog("remoteWatcher inner UNWATCH PARENT", { path, remoteNodeId: authorityIdBase });
|
|
492
590
|
}
|
|
493
591
|
}
|
|
494
592
|
if (!isOwnNodeId(authorityIdBase)) {
|
|
@@ -505,7 +603,24 @@ export class RemoteWatcher {
|
|
|
505
603
|
this.unwatchLatest(config);
|
|
506
604
|
}
|
|
507
605
|
|
|
508
|
-
|
|
606
|
+
@measureFnc
|
|
607
|
+
public getExistingWatchRemoteNodeId(path: string): string | undefined {
|
|
608
|
+
let parentPath = getParentPathStr(path);
|
|
609
|
+
let parentWatchObj = this.remoteWatchParents.get(parentPath);
|
|
610
|
+
if (parentWatchObj) {
|
|
611
|
+
for (let [nodeId, obj] of parentWatchObj.nodesId) {
|
|
612
|
+
if (matchesParentRangeFilter({
|
|
613
|
+
parentPath,
|
|
614
|
+
fullPath: path,
|
|
615
|
+
start: obj.start,
|
|
616
|
+
end: obj.end,
|
|
617
|
+
})) {
|
|
618
|
+
return nodeId;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// NOTE: I think it is a bug if we hit here. There shouldn't be a parent, but have us not match it?
|
|
622
|
+
// But... I also don't see the harm in checking remoteWatchPaths?
|
|
623
|
+
}
|
|
509
624
|
return this.remoteWatchPaths.get(path);
|
|
510
625
|
}
|
|
511
626
|
public getAllRemoteWatchedNodeIds() {
|
|
@@ -36,7 +36,7 @@ import { remoteWatcher } from "./RemoteWatcher";
|
|
|
36
36
|
import { heapTagObj } from "../diagnostics/heapTag";
|
|
37
37
|
import { Querysub } from "../4-querysub/QuerysubController";
|
|
38
38
|
import { isDevDebugbreak } from "../config";
|
|
39
|
-
import {
|
|
39
|
+
import { auditLog } from "../0-path-value-core/auditLogs";
|
|
40
40
|
|
|
41
41
|
const pathValueCtor = heapTagObj("PathValue");
|
|
42
42
|
|
|
@@ -433,10 +433,10 @@ export class ClientWatcher {
|
|
|
433
433
|
let expiryTime = Date.now() + ClientWatcher.WATCH_STICK_TIME;
|
|
434
434
|
if (isDevDebugbreak()) {
|
|
435
435
|
for (let path of fullyUnwatchedPaths) {
|
|
436
|
-
|
|
436
|
+
auditLog("clientWatcher UNWATCH", { path });
|
|
437
437
|
}
|
|
438
438
|
for (let path of fullyUnwatchedParents) {
|
|
439
|
-
|
|
439
|
+
auditLog("clientWatcher UNWATCH PARENT", { path });
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
for (let path of fullyUnwatchedPaths) {
|
|
@@ -533,10 +533,10 @@ export class ClientWatcher {
|
|
|
533
533
|
|
|
534
534
|
if (isDevDebugbreak()) {
|
|
535
535
|
for (let path of pathsArray) {
|
|
536
|
-
|
|
536
|
+
auditLog("clientWatcher WATCH", { path });
|
|
537
537
|
}
|
|
538
538
|
for (let path of parentPathsArray) {
|
|
539
|
-
|
|
539
|
+
auditLog("clientWatcher WATCH PARENT", { path });
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
542
|
|
|
@@ -422,9 +422,10 @@ export class PathValueProxyWatcher {
|
|
|
422
422
|
/** NOTE: This results in VERY slow performance, and will log A LOT of information. */
|
|
423
423
|
public static TRACE = false;
|
|
424
424
|
public static TRACE_WRITES = false;
|
|
425
|
+
public static TRACE_WATCHERS = new Set<string>();
|
|
425
426
|
|
|
426
427
|
private SHOULD_TRACE(watcher: SyncWatcher) {
|
|
427
|
-
return PathValueProxyWatcher.DEBUG || PathValueProxyWatcher.TRACE || PathValueProxyWatcher.TRACE_WRITES && watcher.options.canWrite || ClientWatcher.DEBUG_TRIGGERS === "heavy";
|
|
428
|
+
return PathValueProxyWatcher.DEBUG || PathValueProxyWatcher.TRACE || PathValueProxyWatcher.TRACE_WRITES && watcher.options.canWrite || ClientWatcher.DEBUG_TRIGGERS === "heavy" || PathValueProxyWatcher.TRACE_WATCHERS.size > 0 && Array.from(PathValueProxyWatcher.TRACE_WATCHERS).some(x => watcher.debugName.includes(x));
|
|
428
429
|
}
|
|
429
430
|
|
|
430
431
|
private isUnsyncCheckers = new Set<{ value: number }>();
|
|
@@ -917,6 +918,9 @@ export class PathValueProxyWatcher {
|
|
|
917
918
|
let result = authorityStorage.temporaryOverride(options.overrides, () =>
|
|
918
919
|
options.watchFunction()
|
|
919
920
|
);
|
|
921
|
+
// Clone, otherwise proxies get out of the watcher, which can result in accesses outside
|
|
922
|
+
// of a synced watcher.
|
|
923
|
+
result = deepCloneCborx(result);
|
|
920
924
|
options.onResultUpdated?.({ result }, undefined, outerWatcher);
|
|
921
925
|
} catch (e: any) {
|
|
922
926
|
options.onResultUpdated?.({ error: e.stack }, undefined, outerWatcher);
|
|
@@ -37,17 +37,6 @@ let yargObj = yargs(process.argv)
|
|
|
37
37
|
setIsDeploy();
|
|
38
38
|
|
|
39
39
|
export async function deployMain() {
|
|
40
|
-
if (true as boolean) {
|
|
41
|
-
// Mount, so we can write directly to the database
|
|
42
|
-
await SocketFunction.mount({ port: 0, ...await getThreadKeyCert(), public: isPublic() });
|
|
43
|
-
|
|
44
|
-
await setLiveDeployedHash({ hash: "7b94606dc1de8d03acb8b23952ff5d49e716a45e", refreshThresholdTime: 0, });
|
|
45
|
-
//await setLiveDeployedHash({ hash: "4cba43649682967bf1ff919d941820bea6719ef7", refreshThresholdTime: 0, });
|
|
46
|
-
console.log(magenta(`SHUTTING DOWN`));
|
|
47
|
-
await shutdown();
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
40
|
//SocketFunction.logMessages = true;
|
|
52
41
|
//quietCoreMode();
|
|
53
42
|
//ClientWatcher.DEBUG_READS = true;
|
|
@@ -236,6 +236,7 @@ async function edgeNodeFunction(config: {
|
|
|
236
236
|
|
|
237
237
|
let progressUI = createProgressUI();
|
|
238
238
|
progressUI.setMessage("Finding available server...");
|
|
239
|
+
let retryCount = 0;
|
|
239
240
|
while (true) {
|
|
240
241
|
try {
|
|
241
242
|
let booted = await tryToBoot();
|
|
@@ -243,7 +244,8 @@ async function edgeNodeFunction(config: {
|
|
|
243
244
|
} catch (e) {
|
|
244
245
|
console.error(e);
|
|
245
246
|
}
|
|
246
|
-
|
|
247
|
+
retryCount++;
|
|
248
|
+
progressUI.setMessage(`No available servers, retried ${retryCount++} times`);
|
|
247
249
|
let time = 1000 * 15;
|
|
248
250
|
if (Date.now() < liveHashOverrideExpiryTime) {
|
|
249
251
|
time = 1000 * 3;
|
|
@@ -8,7 +8,7 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
|
|
|
8
8
|
import { green } from "socket-function/src/formatting/logColors";
|
|
9
9
|
import debugbreak from "debugbreak";
|
|
10
10
|
import { isDevDebugbreak } from "../config";
|
|
11
|
-
import { DebugLog, getLogHistoryEquals } from "../0-path-value-core/
|
|
11
|
+
import { DebugLog, getLogHistoryEquals } from "../0-path-value-core/auditLogs";
|
|
12
12
|
import { getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
13
13
|
import { sort } from "socket-function/src/misc";
|
|
14
14
|
|
|
@@ -200,7 +200,7 @@ async function checkAuthority(authority: AuthorityPath, threshold: number) {
|
|
|
200
200
|
authorityStorage.resetForInitialTrigger(valuePath.path);
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
authorityStorage.ingestValues(Array.from(allValues), undefined, undefined);
|
|
203
|
+
authorityStorage.ingestValues(Array.from(allValues), undefined, undefined, "skipArchive");
|
|
204
204
|
|
|
205
205
|
if (allValues.size > 0) {
|
|
206
206
|
// NOTE: By triggering all of these as initial syncs... it SHOULD result in us clobbering the old
|
|
@@ -209,7 +209,7 @@ async function checkAuthority(authority: AuthorityPath, threshold: number) {
|
|
|
209
209
|
// - Except predictions might be clobbered... but that's fine, they don't really matter.
|
|
210
210
|
pathWatcher.triggerValuesChanged(allValues, undefined, {
|
|
211
211
|
parentPaths: new Set(),
|
|
212
|
-
values: allValues,
|
|
212
|
+
values: new Set(Array.from(allValues).map(x => x.path)),
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
}
|
|
@@ -14,7 +14,7 @@ import { remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
|
14
14
|
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
15
15
|
import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
16
16
|
import debugbreak from "debugbreak";
|
|
17
|
-
import { DebugLog, DebugLogController, getLogHistoryEquals, getLogHistoryIncludes } from "../0-path-value-core/
|
|
17
|
+
import { DebugLog, DebugLogController, getLogHistoryEquals, getLogHistoryIncludes } from "../0-path-value-core/auditLogs";
|
|
18
18
|
import { getParentPathStr } from "../path";
|
|
19
19
|
import { diskAuditNow } from "./diskValueAudit";
|
|
20
20
|
import { yellow, green } from "socket-function/src/formatting/logColors";
|
|
@@ -247,7 +247,7 @@ async function auditSpecificPathsBase(config: {
|
|
|
247
247
|
continue;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
if (!pathValueSerializer.compareValuePaths(value, ourValue)) continue;
|
|
250
|
+
if (!pathValueSerializer.compareValuePaths(value, ourValue) || !!value?.valid !== !!ourValue?.valid) continue;
|
|
251
251
|
|
|
252
252
|
let ourTime = ourValue ? ourValue.time.time : 0;
|
|
253
253
|
let remoteTime = value ? value.time.time : 0;
|
|
@@ -262,7 +262,7 @@ async function auditSpecificPathsBase(config: {
|
|
|
262
262
|
ourValue = authorityStorage.getValueAtTime(path);
|
|
263
263
|
value = (await ValueAuditController.nodes[authorityNodeId].getPathValue(path)).value;
|
|
264
264
|
|
|
265
|
-
if (!pathValueSerializer.compareValuePaths(value, ourValue)) continue;
|
|
265
|
+
if (!pathValueSerializer.compareValuePaths(value, ourValue) || !!value?.valid !== !!ourValue?.valid) continue;
|
|
266
266
|
let THRESHOLD2 = Date.now() - MAX_ASSUMED_PROPAGATION_DELAY;
|
|
267
267
|
let ourTime2 = authorityStorage.getLastChangedTime(path)?.time || 0;
|
|
268
268
|
let oursIsNew = ourTime2 > THRESHOLD2;
|
|
@@ -297,8 +297,8 @@ async function auditSpecificPathsBase(config: {
|
|
|
297
297
|
}
|
|
298
298
|
let atTime = Date.now();
|
|
299
299
|
console.error(`Audit mismatch for ${path} from ${authorityNodeId} detected at ${atTime}. Watching because: ${reason}`);
|
|
300
|
-
console.log(`Value Remote:`, pathValueSerializer.getPathValue(value));
|
|
301
|
-
console.log(`Value Local:`, pathValueSerializer.getPathValue(ourValue));
|
|
300
|
+
console.log(`Value Remote (${value?.valid ? "valid" : "invalid"}):`, pathValueSerializer.getPathValue(value));
|
|
301
|
+
console.log(`Value Local (${ourValue?.valid ? "valid" : "invalid"}):`, pathValueSerializer.getPathValue(ourValue));
|
|
302
302
|
console.log(`Time Remote:`, value?.time.time);
|
|
303
303
|
console.log(`Time Local:`, ourValue?.time.time);
|
|
304
304
|
console.log(`Our node id:`, ourNodeId);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { qreact } from "../4-dom/qreact";
|
|
3
|
+
import { assertIsManagementUser } from "./managementPages";
|
|
4
|
+
import { css } from "typesafecss";
|
|
5
|
+
import { getSyncedController } from "../library-components/SyncedController";
|
|
6
|
+
import { DebugLog, DebugLogController, getLogHistoryEquals, getLogHistoryIncludes } from "../0-path-value-core/auditLogs";
|
|
7
|
+
import { remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
8
|
+
import { pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
|
|
9
|
+
import { getParentPathStr } from "../path";
|
|
10
|
+
import { getBrowserUrlNode, getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
11
|
+
import { nextId, sort } from "socket-function/src/misc";
|
|
12
|
+
import { t } from "../2-proxy/schema2";
|
|
13
|
+
import { InputLabelURL } from "../library-components/InputLabel";
|
|
14
|
+
import { URLParam } from "../library-components/URLParam";
|
|
15
|
+
import { Table } from "../5-diagnostics/Table";
|
|
16
|
+
import { ObjectDisplay } from "./logs/ObjectDisplay";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
TODO:
|
|
20
|
+
1) Support watching multiple paths at once
|
|
21
|
+
2) Show extra metadata for the paths
|
|
22
|
+
- Current value (raw, as in, don't sync it)
|
|
23
|
+
- If it is synced
|
|
24
|
+
- The authority of it
|
|
25
|
+
- An expander which shows a table of the full history of values, with valid state, etc
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type DebugLogWithSource = DebugLog & {
|
|
29
|
+
sourceType: "local" | "remote";
|
|
30
|
+
sourceNodeId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let path = new URLParam("path", "");
|
|
34
|
+
let includeDescendants = new URLParam("includeDescendants", false);
|
|
35
|
+
export class AuditLogPage extends qreact.Component {
|
|
36
|
+
loadId = nextId();
|
|
37
|
+
state = t.state({
|
|
38
|
+
loadedId: t.string
|
|
39
|
+
});
|
|
40
|
+
render() {
|
|
41
|
+
let logs: DebugLogWithSource[] = [];
|
|
42
|
+
let loaded = this.loadId === this.state.loadedId;
|
|
43
|
+
if (loaded) {
|
|
44
|
+
let obj = auditLogController(getBrowserUrlNode()).getPathHistory({
|
|
45
|
+
path: path.value,
|
|
46
|
+
includeDescendants: includeDescendants.value,
|
|
47
|
+
});
|
|
48
|
+
if (obj) {
|
|
49
|
+
logs = obj.fullLogs;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<div class={css.pad2(10).vbox(10).fillWidth}>
|
|
54
|
+
<InputLabelURL
|
|
55
|
+
label="Path"
|
|
56
|
+
url={path}
|
|
57
|
+
fillWidth
|
|
58
|
+
onChange={() => this.loadId = nextId()}
|
|
59
|
+
/>
|
|
60
|
+
<InputLabelURL
|
|
61
|
+
label="Include Descendants"
|
|
62
|
+
url={includeDescendants}
|
|
63
|
+
checkbox
|
|
64
|
+
onChange={() => this.loadId = nextId()}
|
|
65
|
+
/>
|
|
66
|
+
{!loaded && <button onClick={() => this.state.loadedId = this.loadId}>Load</button>}
|
|
67
|
+
<Table
|
|
68
|
+
rows={logs}
|
|
69
|
+
columns={{
|
|
70
|
+
type: {},
|
|
71
|
+
time: {},
|
|
72
|
+
sourceType: {},
|
|
73
|
+
sourceNodeId: {},
|
|
74
|
+
values: {
|
|
75
|
+
formatter(value) {
|
|
76
|
+
return <ObjectDisplay value={value} />;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AuditLogControllerBase {
|
|
88
|
+
public async getPathHistory(config: {
|
|
89
|
+
path: string;
|
|
90
|
+
includeDescendants?: boolean;
|
|
91
|
+
}) {
|
|
92
|
+
let { path, includeDescendants } = config;
|
|
93
|
+
let authorityNodeId = remoteWatcher.getExistingWatchRemoteNodeId(path);
|
|
94
|
+
if (!authorityNodeId) {
|
|
95
|
+
authorityNodeId = await pathValueAuthority2.getSingleReadNodeSync(path);
|
|
96
|
+
}
|
|
97
|
+
let logs: DebugLog[] = [];
|
|
98
|
+
let remoteLogs: DebugLog[] = [];
|
|
99
|
+
if (includeDescendants) {
|
|
100
|
+
// Also look for the parent, so we can capture keys() syncs as well
|
|
101
|
+
// (this should give us child paths too, because that's how includes works)
|
|
102
|
+
logs = getLogHistoryIncludes(path);
|
|
103
|
+
if (authorityNodeId) {
|
|
104
|
+
remoteLogs = await DebugLogController.nodes[authorityNodeId].getLogHistoryEquals(path);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
logs = getLogHistoryEquals(path);
|
|
108
|
+
if (authorityNodeId) {
|
|
109
|
+
remoteLogs = await DebugLogController.nodes[authorityNodeId].getLogHistoryEquals(path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let fullLogs: DebugLogWithSource[] = [];
|
|
114
|
+
let ownNodeId = getOwnNodeId();
|
|
115
|
+
for (let log of logs) {
|
|
116
|
+
fullLogs.push({
|
|
117
|
+
...log,
|
|
118
|
+
sourceType: "local",
|
|
119
|
+
sourceNodeId: ownNodeId,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (authorityNodeId) {
|
|
123
|
+
for (let log of remoteLogs) {
|
|
124
|
+
fullLogs.push({
|
|
125
|
+
...log,
|
|
126
|
+
sourceType: "remote",
|
|
127
|
+
sourceNodeId: authorityNodeId,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
sort(fullLogs, x => x.time);
|
|
132
|
+
return {
|
|
133
|
+
fullLogs,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const AuditLogController = SocketFunction.register(
|
|
139
|
+
"AuditLogController-bf798faf-c803-4add-ac2c-99d0f44e753a",
|
|
140
|
+
new AuditLogControllerBase(),
|
|
141
|
+
() => ({
|
|
142
|
+
getPathHistory: {},
|
|
143
|
+
}),
|
|
144
|
+
() => ({
|
|
145
|
+
hooks: [assertIsManagementUser],
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
export const auditLogController = getSyncedController(AuditLogController);
|
|
@@ -64,7 +64,7 @@ export async function registerManagementPages2(config: {
|
|
|
64
64
|
throw new Error(`registerManagementPages2 called with a different module than previously registered. This will break isManagementUser, and so is not allowed.`);
|
|
65
65
|
}
|
|
66
66
|
registeredModule = config.module;
|
|
67
|
-
let inputPages
|
|
67
|
+
let inputPages: typeof config.pages = [];
|
|
68
68
|
|
|
69
69
|
inputPages.push({
|
|
70
70
|
title: "Nodes",
|
|
@@ -89,12 +89,19 @@ export async function registerManagementPages2(config: {
|
|
|
89
89
|
componentName: "LogClassifiers",
|
|
90
90
|
getModule: () => import("./errorLogs/LogClassifiers"),
|
|
91
91
|
});
|
|
92
|
+
inputPages.push({
|
|
93
|
+
title: "Audit Paths",
|
|
94
|
+
componentName: "AuditLogPage",
|
|
95
|
+
controllerName: "AuditLogController",
|
|
96
|
+
getModule: () => import("./AuditLogPage"),
|
|
97
|
+
});
|
|
92
98
|
inputPages.push({
|
|
93
99
|
title: "Time",
|
|
94
100
|
componentName: "TimeDebug",
|
|
95
101
|
controllerName: "",
|
|
96
102
|
getModule: () => import("./TimeDebug"),
|
|
97
103
|
});
|
|
104
|
+
inputPages.push(...config.pages);
|
|
98
105
|
|
|
99
106
|
// NOTE: We don't store the UI in the database (here, or anywhere else, at least
|
|
100
107
|
// not yet), but we do want to store the permission function call, as we may
|