querysub 0.406.0 → 0.408.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/audit-disk-values.js +7 -0
- package/bin/deploy-prefixes.js +7 -0
- package/package.json +5 -3
- package/src/-a-archives/archiveCache.ts +12 -9
- package/src/-a-auth/certs.ts +1 -1
- package/src/-c-identity/IdentityController.ts +9 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +63 -10
- package/src/0-path-value-core/AuthorityLookup.ts +14 -4
- package/src/0-path-value-core/PathRouter.ts +247 -117
- package/src/0-path-value-core/PathRouterRouteOverride.ts +1 -1
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +4 -2
- package/src/0-path-value-core/PathValueCommitter.ts +68 -31
- package/src/0-path-value-core/PathValueController.ts +77 -8
- package/src/0-path-value-core/PathWatcher.ts +46 -4
- package/src/0-path-value-core/ShardPrefixes.ts +6 -0
- package/src/0-path-value-core/ValidStateComputer.ts +20 -8
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +18 -55
- package/src/0-path-value-core/pathValueArchives.ts +19 -8
- package/src/0-path-value-core/pathValueCore.ts +75 -27
- package/src/0-path-value-core/startupAuthority.ts +9 -9
- package/src/1-path-client/RemoteWatcher.ts +217 -178
- package/src/1-path-client/pathValueClientWatcher.ts +6 -11
- package/src/2-proxy/pathValueProxy.ts +2 -3
- package/src/3-path-functions/PathFunctionRunner.ts +3 -1
- package/src/3-path-functions/syncSchema.ts +6 -2
- package/src/4-deploy/deployGetFunctionsInner.ts +1 -1
- package/src/4-deploy/deployPrefixes.ts +14 -0
- package/src/4-deploy/edgeNodes.ts +1 -1
- package/src/4-querysub/Querysub.ts +17 -5
- package/src/4-querysub/QuerysubController.ts +21 -10
- package/src/4-querysub/predictionQueue.tsx +3 -0
- package/src/4-querysub/querysubPrediction.ts +27 -20
- package/src/5-diagnostics/nodeMetadata.ts +17 -0
- package/src/diagnostics/NodeConnectionsPage.tsx +167 -0
- package/src/diagnostics/NodeViewer.tsx +11 -15
- package/src/diagnostics/PathDistributionInfo.tsx +102 -0
- package/src/diagnostics/SyncTestPage.tsx +19 -8
- package/src/diagnostics/auditDiskValues.ts +221 -0
- package/src/diagnostics/auditDiskValuesEntry.ts +43 -0
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
- package/src/diagnostics/logs/TimeRangeSelector.tsx +3 -3
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +2 -0
- package/src/diagnostics/managementPages.tsx +10 -1
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +3 -2
- package/src/diagnostics/pathAuditer.ts +21 -0
- package/src/path.ts +9 -2
- package/src/rangeMath.ts +41 -0
- package/tempnotes.txt +5 -58
- package/test.ts +13 -295
- package/src/diagnostics/benchmark.ts +0 -139
- package/src/diagnostics/runSaturationTest.ts +0 -416
- package/src/diagnostics/satSchema.ts +0 -64
- package/src/test/mongoSatTest.tsx +0 -55
- package/src/test/satTest.ts +0 -193
- package/src/test/test.tsx +0 -552
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { qreact } from "../4-dom/qreact";
|
|
2
|
+
import { css } from "../4-dom/css";
|
|
3
|
+
import { sort } from "socket-function/src/misc";
|
|
4
|
+
import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
|
|
5
|
+
import { pathWatcher } from "../0-path-value-core/PathWatcher";
|
|
6
|
+
import { PathRouter } from "../0-path-value-core/PathRouter";
|
|
7
|
+
import { QuerysubController, querysubNodeId } from "../4-querysub/QuerysubController";
|
|
8
|
+
import { getSyncedController } from "../library-components/SyncedController";
|
|
9
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
10
|
+
import { isCurrentUserSuperUser } from "../user-implementation/userData";
|
|
11
|
+
import { delay } from "socket-function/src/batching";
|
|
12
|
+
import { remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
13
|
+
|
|
14
|
+
module.hotreload = true;
|
|
15
|
+
|
|
16
|
+
let querysubController = getSyncedController(QuerysubController);
|
|
17
|
+
|
|
18
|
+
let pathNodeCache = new Map<string, string | "missing">();
|
|
19
|
+
let pendingPaths = new Set<string>();
|
|
20
|
+
let flushScheduled = false;
|
|
21
|
+
|
|
22
|
+
function scheduleFlush() {
|
|
23
|
+
if (flushScheduled) return;
|
|
24
|
+
flushScheduled = true;
|
|
25
|
+
let promise = Promise.resolve().then(async () => {
|
|
26
|
+
flushScheduled = false;
|
|
27
|
+
if (pendingPaths.size === 0) return;
|
|
28
|
+
let paths = Array.from(pendingPaths);
|
|
29
|
+
pendingPaths.clear();
|
|
30
|
+
let nodeId = await querysubNodeId();
|
|
31
|
+
await remoteWatcher.flushWatchers();
|
|
32
|
+
if (!nodeId) return;
|
|
33
|
+
let result = await QuerysubController.nodes[nodeId].debugGetPathNodeIds(paths);
|
|
34
|
+
for (let path of paths) {
|
|
35
|
+
pathNodeCache.set(path, result.get(path) ?? "missing");
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
proxyWatcher.triggerOnPromiseFinish(promise, { waitReason: "pathDistribution" });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getNodeForPath(path: string): string | "missing" | undefined {
|
|
42
|
+
let cached = pathNodeCache.get(path);
|
|
43
|
+
if (cached !== undefined) return cached;
|
|
44
|
+
pendingPaths.add(path);
|
|
45
|
+
scheduleFlush();
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class PathDistributionInfo extends qreact.Component {
|
|
50
|
+
render() {
|
|
51
|
+
if (!isCurrentUserSuperUser()) return undefined;
|
|
52
|
+
|
|
53
|
+
let nodeSpecs = querysubController(SocketFunction.browserNodeId()).debugGetNodeSpecs();
|
|
54
|
+
|
|
55
|
+
let paths = pathWatcher.getAllWatchedPaths().filter(p => !PathRouter.isLocalPath(p));
|
|
56
|
+
let distribution = new Map<string, string[]>();
|
|
57
|
+
let missingPaths: string[] = [];
|
|
58
|
+
for (let path of paths) {
|
|
59
|
+
let nodeId = getNodeForPath(path);
|
|
60
|
+
if (nodeId === undefined) continue;
|
|
61
|
+
if (nodeId === "missing") {
|
|
62
|
+
missingPaths.push(path);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
let nodePaths = distribution.get(nodeId);
|
|
66
|
+
if (!nodePaths) {
|
|
67
|
+
nodePaths = [];
|
|
68
|
+
distribution.set(nodeId, nodePaths);
|
|
69
|
+
}
|
|
70
|
+
nodePaths.push(path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let entries = Array.from(distribution.entries());
|
|
74
|
+
sort(entries, ([nodeId, p]) => nodeId);
|
|
75
|
+
sort(entries, ([nodeId, p]) => (nodeSpecs?.get(nodeId)?.routeStart ?? 0));
|
|
76
|
+
|
|
77
|
+
let titleParts = entries.map(([nodeId, p]) => {
|
|
78
|
+
let spec = nodeSpecs?.get(nodeId);
|
|
79
|
+
let range = spec ? ` (${spec.routeStart}-${spec.routeEnd})` : "";
|
|
80
|
+
return `${nodeId}${range}: ${p.length}`;
|
|
81
|
+
});
|
|
82
|
+
if (missingPaths.length > 0) titleParts.push(`missing: ${missingPaths.length}`);
|
|
83
|
+
let title = titleParts.join("\n");
|
|
84
|
+
|
|
85
|
+
let parts: preact.ComponentChild[] = [];
|
|
86
|
+
for (let i = 0; i < entries.length; i++) {
|
|
87
|
+
let [nodeId, nodePaths] = entries[i];
|
|
88
|
+
if (i > 0) parts.push(" | ");
|
|
89
|
+
let spec = nodeSpecs?.get(nodeId);
|
|
90
|
+
parts.push(<span className={css.button} onClick={() => console.log(`Paths for ${nodeId}:`, nodePaths, spec && { range: `${spec.routeStart}-${spec.routeEnd}` })}>{nodePaths.length}</span>);
|
|
91
|
+
}
|
|
92
|
+
if (missingPaths.length > 0) {
|
|
93
|
+
if (parts.length > 0) parts.push(" | ");
|
|
94
|
+
parts.push(<span className={css.button.boldStyle.hslcolor(0, 100, 60)} onClick={() => console.log("Missing paths:", missingPaths)}>{missingPaths.length}</span>);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return <div className={css.hbox(4)} title={title}>
|
|
98
|
+
<span>🌐</span>
|
|
99
|
+
{parts.length > 0 && parts || "..."}
|
|
100
|
+
</div>;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -17,11 +17,9 @@ import { PathAuditerController } from "./pathAuditer";
|
|
|
17
17
|
import { t } from "../2-proxy/schema2";
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
let { data, functions } = Querysub.createSchema
|
|
21
|
-
values:
|
|
22
|
-
|
|
23
|
-
};
|
|
24
|
-
}>()({
|
|
20
|
+
let { data, functions } = Querysub.createSchema({
|
|
21
|
+
values: t.lookup(t.number)
|
|
22
|
+
})({
|
|
25
23
|
functions: {
|
|
26
24
|
setValue(key: string, value: number) {
|
|
27
25
|
data().values[key] = value;
|
|
@@ -90,6 +88,14 @@ export class SyncTestPage extends qreact.Component {
|
|
|
90
88
|
allThreads = allThreads.slice();
|
|
91
89
|
sort(allThreads, x => x.entrypoint);
|
|
92
90
|
|
|
91
|
+
function refreshDelayed() {
|
|
92
|
+
Querysub.onCommitFinished(() => {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
syncTestController.refreshAll();
|
|
95
|
+
}, 500);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
return (
|
|
94
100
|
<div className={css.pad2(20).vbox(40)}>
|
|
95
101
|
<h1>Sync Test Page</h1>
|
|
@@ -125,8 +131,11 @@ export class SyncTestPage extends qreact.Component {
|
|
|
125
131
|
<div className={css.hbox(10)}>
|
|
126
132
|
<span class={css.boldStyle}>{key}</span>
|
|
127
133
|
<span>{value}</span>
|
|
128
|
-
<Button onClick={() => functions.incrementValue(key)}>Increment</Button>
|
|
129
|
-
<InputLabel value={value} number onChangeValue={value =>
|
|
134
|
+
<Button onClick={() => { functions.incrementValue(key); refreshDelayed(); }}>Increment</Button>
|
|
135
|
+
<InputLabel value={value} number onChangeValue={value => {
|
|
136
|
+
functions.setValue(key, +value);
|
|
137
|
+
refreshDelayed();
|
|
138
|
+
}} />
|
|
130
139
|
</div>
|
|
131
140
|
<div className={css.hbox(10, 2).wrap.marginLeft(20)}>
|
|
132
141
|
{allThreads.map(thread => {
|
|
@@ -149,6 +158,7 @@ export class SyncTestPage extends qreact.Component {
|
|
|
149
158
|
path: path,
|
|
150
159
|
nodeId: thread.nodeId
|
|
151
160
|
});
|
|
161
|
+
refreshDelayed();
|
|
152
162
|
}}>
|
|
153
163
|
Delete Value
|
|
154
164
|
</Button>
|
|
@@ -168,6 +178,7 @@ export class SyncTestPage extends qreact.Component {
|
|
|
168
178
|
label="Insert new key"
|
|
169
179
|
onChangeValue={value => {
|
|
170
180
|
functions.setValue(value, 0);
|
|
181
|
+
refreshDelayed();
|
|
171
182
|
}}
|
|
172
183
|
/>
|
|
173
184
|
<Button onClick={() => {
|
|
@@ -205,7 +216,7 @@ class SyncTestControllerBase {
|
|
|
205
216
|
}
|
|
206
217
|
|
|
207
218
|
public async getValueAndTime(path: string) {
|
|
208
|
-
let pathValue = authorityStorage.getValueAtTime(path);
|
|
219
|
+
let pathValue = authorityStorage.getValueAtTime(path, undefined, false, "noAudit");
|
|
209
220
|
let value = pathValueSerializer.getPathValue(pathValue);
|
|
210
221
|
let time = pathValue?.time.time;
|
|
211
222
|
return { value, time };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { getControllerNodeIdList } from "../-g-core-values/NodeCapabilities";
|
|
2
|
+
import { AuthoritySpec, PathRouter } from "../0-path-value-core/PathRouter";
|
|
3
|
+
import { PathValueController, PathValueControllerBase, AuditSnapshotEntry } from "../0-path-value-core/PathValueController";
|
|
4
|
+
import { MAX_CHANGE_AGE, MAX_TIME_UNTIL_DISK_FLUSH, pathValueArchives, compareTime, PathValue, Time } from "../0-path-value-core/pathValueCore";
|
|
5
|
+
import { binarySearchIndex, compare } from "socket-function/src/misc";
|
|
6
|
+
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
7
|
+
import { magenta, green, yellow, red } from "socket-function/src/formatting/logColors";
|
|
8
|
+
|
|
9
|
+
function logDiscrepancy(message: string, diskEntry: { path: string; time: Time } | undefined, serverEntry: AuditSnapshotEntry | undefined, authorityNodeId: string) {
|
|
10
|
+
let details: Record<string, unknown> = { authorityNodeId };
|
|
11
|
+
|
|
12
|
+
if (diskEntry) {
|
|
13
|
+
details.path = diskEntry.path;
|
|
14
|
+
details.diskTime = diskEntry.time.time;
|
|
15
|
+
details.diskTimeVersion = diskEntry.time.version;
|
|
16
|
+
details.diskCreatorId = diskEntry.time.creatorId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (serverEntry) {
|
|
20
|
+
details.path = serverEntry.path;
|
|
21
|
+
details.serverTime = serverEntry.time.time;
|
|
22
|
+
details.serverTimeVersion = serverEntry.time.version;
|
|
23
|
+
details.serverCreatorId = serverEntry.time.creatorId;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.error(`Disk audit: ${message}`, details);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function auditDiskValues(spec: AuthoritySpec) {
|
|
30
|
+
let startTime = Date.now();
|
|
31
|
+
console.log(magenta("=== Starting Disk Audit ==="));
|
|
32
|
+
|
|
33
|
+
let timeStartWait = Date.now();
|
|
34
|
+
await PathRouter.waitUntilReady();
|
|
35
|
+
let timeWaitReady = Date.now() - timeStartWait;
|
|
36
|
+
|
|
37
|
+
let checkCutOff = Date.now() - MAX_TIME_UNTIL_DISK_FLUSH;
|
|
38
|
+
let authoritiesToCheck = PathRouter.getAllOverlappingAuthorities(spec);
|
|
39
|
+
|
|
40
|
+
console.log(magenta(`Checking ${green(formatNumber(authoritiesToCheck.length))} authorities`));
|
|
41
|
+
|
|
42
|
+
let timeStartLoadDisk = Date.now();
|
|
43
|
+
let snapshot = await pathValueArchives.loadValues(spec);
|
|
44
|
+
let diskValues = Object.values(snapshot.values).flat();
|
|
45
|
+
let timeLoadDisk = Date.now() - timeStartLoadDisk;
|
|
46
|
+
|
|
47
|
+
console.log(green(`Loaded ${magenta(formatNumber(diskValues.length))} values from disk in ${formatTime(timeLoadDisk)}`));
|
|
48
|
+
|
|
49
|
+
let diskValuesByPath = new Map<string, PathValue>();
|
|
50
|
+
for (let value of diskValues) {
|
|
51
|
+
if (!value.valid) continue;
|
|
52
|
+
if (value.canGCValue) continue;
|
|
53
|
+
|
|
54
|
+
let existing = diskValuesByPath.get(value.path);
|
|
55
|
+
if (!existing || compareTime(value.time, existing.time) > 0) {
|
|
56
|
+
diskValuesByPath.set(value.path, value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let serverSnapshotsByAuthority = new Map<string, { spec: AuthoritySpec; serverMap: Map<string, AuditSnapshotEntry> }>();
|
|
61
|
+
|
|
62
|
+
let timeStartLoadAuthorities = Date.now();
|
|
63
|
+
let authoritiesLoaded = 0;
|
|
64
|
+
await Promise.all(authoritiesToCheck.map(async authority => {
|
|
65
|
+
try {
|
|
66
|
+
let serverSnapshot = await PathValueControllerBase.getAuditSnapshot({
|
|
67
|
+
nodeId: authority.nodeId,
|
|
68
|
+
spec,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let serverMap = new Map<string, AuditSnapshotEntry>();
|
|
72
|
+
for (let entry of serverSnapshot) {
|
|
73
|
+
serverMap.set(entry.path, entry);
|
|
74
|
+
}
|
|
75
|
+
serverSnapshotsByAuthority.set(authority.nodeId, { spec: authority, serverMap });
|
|
76
|
+
authoritiesLoaded++;
|
|
77
|
+
console.log(magenta(`Loaded authority ${magenta(String(authoritiesLoaded))}/${green(formatNumber(authoritiesToCheck.length))}: ${authority.nodeId} (${magenta(formatNumber(serverSnapshot.length))} entries)`));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(`Failed to get audit snapshot from authority ${authority.nodeId}`, error);
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
let timeLoadAuthorities = Date.now() - timeStartLoadAuthorities;
|
|
83
|
+
|
|
84
|
+
let totalServerValues = 0;
|
|
85
|
+
for (let [, { serverMap }] of serverSnapshotsByAuthority) {
|
|
86
|
+
totalServerValues += serverMap.size;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(yellow(`Starting comparison: ${green(formatNumber(diskValuesByPath.size))} disk values vs ${green(formatNumber(totalServerValues))} server values`));
|
|
90
|
+
|
|
91
|
+
let timeStartComparison = Date.now();
|
|
92
|
+
let issuesOnDiskMissingFromServer = 0;
|
|
93
|
+
let issuesTimeMismatch = 0;
|
|
94
|
+
let issuesOnServerMissingFromDisk = 0;
|
|
95
|
+
let valuesToSync: PathValue[] = [];
|
|
96
|
+
let serverEntriesToFetch = new Map<string, AuditSnapshotEntry[]>();
|
|
97
|
+
|
|
98
|
+
function compareValues(a: PathValue, b: PathValue): number {
|
|
99
|
+
let pathComparison = compare(a.path, b.path);
|
|
100
|
+
if (pathComparison !== 0) return pathComparison;
|
|
101
|
+
return compareTime(a.time, b.time);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function addValueToSync(value: PathValue) {
|
|
105
|
+
let index = binarySearchIndex(valuesToSync.length, i => compareValues(valuesToSync[i], value));
|
|
106
|
+
if (index >= 0) return;
|
|
107
|
+
|
|
108
|
+
index = ~index;
|
|
109
|
+
valuesToSync.splice(index, 0, value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (let [authorityNodeId, { spec: authoritySpec, serverMap }] of serverSnapshotsByAuthority) {
|
|
113
|
+
for (let [path, diskValue] of diskValuesByPath) {
|
|
114
|
+
if (diskValue.time.time > checkCutOff) continue;
|
|
115
|
+
if (diskValue.canGCValue) continue;
|
|
116
|
+
|
|
117
|
+
if (!PathRouter.matchesAuthoritySpec(authoritySpec, path)) continue;
|
|
118
|
+
|
|
119
|
+
let diskEntry = { path: diskValue.path, time: diskValue.time };
|
|
120
|
+
let serverEntry = serverMap.get(path);
|
|
121
|
+
if (!serverEntry) {
|
|
122
|
+
logDiscrepancy("Value on disk but missing from server", diskEntry, undefined, authorityNodeId);
|
|
123
|
+
issuesOnDiskMissingFromServer++;
|
|
124
|
+
addValueToSync(diskValue);
|
|
125
|
+
} else {
|
|
126
|
+
if (serverEntry.time.time > checkCutOff) continue;
|
|
127
|
+
|
|
128
|
+
let comparison = compareTime(diskValue.time, serverEntry.time);
|
|
129
|
+
if (comparison !== 0) {
|
|
130
|
+
logDiscrepancy("Time mismatch between disk and server", diskEntry, serverEntry, authorityNodeId);
|
|
131
|
+
issuesTimeMismatch++;
|
|
132
|
+
|
|
133
|
+
if (comparison > 0) {
|
|
134
|
+
addValueToSync(diskValue);
|
|
135
|
+
} else {
|
|
136
|
+
let entries = serverEntriesToFetch.get(authorityNodeId);
|
|
137
|
+
if (!entries) {
|
|
138
|
+
entries = [];
|
|
139
|
+
serverEntriesToFetch.set(authorityNodeId, entries);
|
|
140
|
+
}
|
|
141
|
+
entries.push(serverEntry);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (let [path, serverEntry] of serverMap) {
|
|
148
|
+
if (serverEntry.time.time > checkCutOff) continue;
|
|
149
|
+
|
|
150
|
+
if (!PathRouter.matchesAuthoritySpec(authoritySpec, path)) continue;
|
|
151
|
+
|
|
152
|
+
let diskValue = diskValuesByPath.get(path);
|
|
153
|
+
if (!diskValue) {
|
|
154
|
+
logDiscrepancy("Value on server but missing from disk", undefined, serverEntry, authorityNodeId);
|
|
155
|
+
issuesOnServerMissingFromDisk++;
|
|
156
|
+
let entries = serverEntriesToFetch.get(authorityNodeId);
|
|
157
|
+
if (!entries) {
|
|
158
|
+
entries = [];
|
|
159
|
+
serverEntriesToFetch.set(authorityNodeId, entries);
|
|
160
|
+
}
|
|
161
|
+
entries.push(serverEntry);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
let timeComparison = Date.now() - timeStartComparison;
|
|
166
|
+
|
|
167
|
+
let timeStartFetch = Date.now();
|
|
168
|
+
for (let [authorityNodeId, entries] of serverEntriesToFetch) {
|
|
169
|
+
try {
|
|
170
|
+
let values = await PathValueControllerBase.getValuesByPathAndTime({
|
|
171
|
+
nodeId: authorityNodeId,
|
|
172
|
+
entries,
|
|
173
|
+
});
|
|
174
|
+
for (let value of values) {
|
|
175
|
+
addValueToSync(value);
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(`Failed to fetch values from authority ${authorityNodeId}`, error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let timeFetch = Date.now() - timeStartFetch;
|
|
182
|
+
|
|
183
|
+
let timeStartSync = Date.now();
|
|
184
|
+
if (valuesToSync.length > 0) {
|
|
185
|
+
let valuesPerAuthority = PathRouter.getAllAuthoritiesForValues(valuesToSync);
|
|
186
|
+
for (let [authorityNodeId, values] of valuesPerAuthority) {
|
|
187
|
+
await PathValueControllerBase.sendValues({ nodeId: authorityNodeId, pathValues: values });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let timeSync = Date.now() - timeStartSync;
|
|
191
|
+
|
|
192
|
+
let totalTime = Date.now() - startTime;
|
|
193
|
+
|
|
194
|
+
let totalIssues = issuesOnDiskMissingFromServer + issuesTimeMismatch + issuesOnServerMissingFromDisk;
|
|
195
|
+
|
|
196
|
+
console.log(magenta("=== Disk Audit Summary ==="));
|
|
197
|
+
if (totalIssues === 0) {
|
|
198
|
+
console.log(green(`✓ No issues found`));
|
|
199
|
+
} else {
|
|
200
|
+
console.log(yellow(`Found ${red(formatNumber(totalIssues))} total issues:`));
|
|
201
|
+
if (issuesOnDiskMissingFromServer > 0) {
|
|
202
|
+
console.log(yellow(` - ${red(formatNumber(issuesOnDiskMissingFromServer))} values on disk but missing from server`));
|
|
203
|
+
}
|
|
204
|
+
if (issuesTimeMismatch > 0) {
|
|
205
|
+
console.log(yellow(` - ${red(formatNumber(issuesTimeMismatch))} time mismatches between disk and server`));
|
|
206
|
+
}
|
|
207
|
+
if (issuesOnServerMissingFromDisk > 0) {
|
|
208
|
+
console.log(yellow(` - ${red(formatNumber(issuesOnServerMissingFromDisk))} values on server but missing from disk`));
|
|
209
|
+
}
|
|
210
|
+
console.log(yellow(`Synced ${magenta(formatNumber(valuesToSync.length))} values`));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(magenta("=== Timing Breakdown ==="));
|
|
214
|
+
console.log(magenta(`Wait for ready: ${formatTime(timeWaitReady)}`));
|
|
215
|
+
console.log(magenta(`Load disk: ${formatTime(timeLoadDisk)}`));
|
|
216
|
+
console.log(magenta(`Load server: ${formatTime(timeLoadAuthorities)}`));
|
|
217
|
+
console.log(magenta(`Comparison: ${formatTime(timeComparison)}`));
|
|
218
|
+
console.log(magenta(`Fetch values: ${formatTime(timeFetch)}`));
|
|
219
|
+
console.log(magenta(`Sync values: ${formatTime(timeSync)}`));
|
|
220
|
+
console.log(green(`Total time: ${formatTime(totalTime)}`));
|
|
221
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isNodeTrue, timeInHour } from "socket-function/src/misc";
|
|
2
|
+
import { AuthoritySpec } from "../0-path-value-core/PathRouter";
|
|
3
|
+
import { getAllAuthoritySpec } from "../0-path-value-core/PathRouterServerAuthoritySpec";
|
|
4
|
+
import { auditDiskValues } from "./auditDiskValues";
|
|
5
|
+
import yargs from "yargs";
|
|
6
|
+
import { runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
7
|
+
import { Querysub } from "../4-querysub/Querysub";
|
|
8
|
+
|
|
9
|
+
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
10
|
+
.option("watch", { type: "boolean", desc: "Audit in a loop (otherwise just audit once)" })
|
|
11
|
+
.argv || {}
|
|
12
|
+
;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const SPLIT_COUNT = 10;
|
|
16
|
+
async function auditDiskValuesNow() {
|
|
17
|
+
let baseSpec = await getAllAuthoritySpec();
|
|
18
|
+
let splitSpecs: AuthoritySpec[] = [];
|
|
19
|
+
for (let i = 0; i < SPLIT_COUNT; i++) {
|
|
20
|
+
splitSpecs.push({
|
|
21
|
+
...baseSpec,
|
|
22
|
+
routeStart: i * baseSpec.routeEnd / SPLIT_COUNT,
|
|
23
|
+
routeEnd: (i + 1) * baseSpec.routeEnd / SPLIT_COUNT,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
for (let spec of splitSpecs) {
|
|
27
|
+
await auditDiskValues(spec);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function main() {
|
|
31
|
+
await Querysub.hostService("audit-disk-values");
|
|
32
|
+
if (yargObj.watch) {
|
|
33
|
+
await runInfinitePollCallAtStart(timeInHour, auditDiskValuesNow);
|
|
34
|
+
} else {
|
|
35
|
+
try {
|
|
36
|
+
// Force, as they are running this manually, and so they probably want to see something happen...
|
|
37
|
+
await auditDiskValuesNow();
|
|
38
|
+
} finally {
|
|
39
|
+
process.exit();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
main().catch(console.error);
|
|
@@ -304,7 +304,11 @@ export class LogViewer3 extends qreact.Component {
|
|
|
304
304
|
},
|
|
305
305
|
onResult: (match: LogDatum) => {
|
|
306
306
|
results.push(match);
|
|
307
|
-
|
|
307
|
+
if (range.searchFromStart) {
|
|
308
|
+
sort(results, x => x.time);
|
|
309
|
+
} else {
|
|
310
|
+
sort(results, x => -x.time);
|
|
311
|
+
}
|
|
308
312
|
void updateResults();
|
|
309
313
|
},
|
|
310
314
|
onResults: (loggerStats: IndexedLogResults) => {
|
|
@@ -80,9 +80,9 @@ export class TimeRangeSelector extends qreact.Component {
|
|
|
80
80
|
<InputLabel
|
|
81
81
|
label="Search from Start"
|
|
82
82
|
checkbox
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
searchFromStartParam.value =
|
|
83
|
+
checked={searchFromStartParam.value}
|
|
84
|
+
onChange={e => {
|
|
85
|
+
searchFromStartParam.value = e.currentTarget.checked;
|
|
86
86
|
}}
|
|
87
87
|
/>
|
|
88
88
|
<Button
|
|
@@ -209,6 +209,8 @@ export class LifeCycleInstanceRenderer extends qreact.Component<{
|
|
|
209
209
|
}
|
|
210
210
|
title={statusTitle}
|
|
211
211
|
onClick={(e) => {
|
|
212
|
+
// If it's already been removed from the DOM, then don't trigger the toggle
|
|
213
|
+
if (!(e.target as HTMLElement).parentElement) return;
|
|
212
214
|
if ((e.target as HTMLElement).closest(".LifeCycleRenderer-contents")) {
|
|
213
215
|
return;
|
|
214
216
|
}
|
|
@@ -28,6 +28,8 @@ import { closeAllModals } from "../5-diagnostics/Modal";
|
|
|
28
28
|
import { delay } from "socket-function/src/batching";
|
|
29
29
|
import { currentViewParam, selectedServiceIdParam } from "../deployManager/urlParams";
|
|
30
30
|
import { FunctionCallInfo } from "./FunctionCallInfo";
|
|
31
|
+
import { PathDistributionInfo } from "./PathDistributionInfo";
|
|
32
|
+
import { isCurrentUserSuperUser } from "../user-implementation/userData";
|
|
31
33
|
|
|
32
34
|
export const managementPageURL = new URLParam("managementpage", "");
|
|
33
35
|
export const showingManagementURL = new URLParam("showingmanagement", false);
|
|
@@ -114,6 +116,12 @@ export async function registerManagementPages2(config: {
|
|
|
114
116
|
controllerName: "AuditLogController",
|
|
115
117
|
getModule: () => import("./AuditLogPage"),
|
|
116
118
|
});
|
|
119
|
+
inputPages.push({
|
|
120
|
+
title: "Node Connections",
|
|
121
|
+
componentName: "NodeConnectionsPage",
|
|
122
|
+
controllerName: "NodeConnectionsController",
|
|
123
|
+
getModule: () => import("./NodeConnectionsPage"),
|
|
124
|
+
});
|
|
117
125
|
inputPages.push({
|
|
118
126
|
title: "Time",
|
|
119
127
|
componentName: "TimeDebug",
|
|
@@ -357,7 +365,8 @@ class ManagementRoot extends qreact.Component {
|
|
|
357
365
|
{pages.map(page =>
|
|
358
366
|
<ATag values={[{ param: managementPageURL, value: page.componentName }]}>{page.title}</ATag>
|
|
359
367
|
)}
|
|
360
|
-
<FunctionCallInfo />
|
|
368
|
+
{isCurrentUserSuperUser() && <FunctionCallInfo />}
|
|
369
|
+
{isCurrentUserSuperUser() && <PathDistributionInfo />}
|
|
361
370
|
</div>
|
|
362
371
|
{currentPage &&
|
|
363
372
|
<div
|
|
@@ -101,7 +101,7 @@ export class ArchiveViewer extends qreact.Component {
|
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
try {
|
|
104
|
-
let newValuePaths = await controller.getValuePaths(getAllAuthoritySpec());
|
|
104
|
+
let newValuePaths = await controller.getValuePaths(await getAllAuthoritySpec());
|
|
105
105
|
Querysub.localCommit(() => this.state.valuePaths = newValuePaths);
|
|
106
106
|
} catch (e: any) {
|
|
107
107
|
Querysub.localCommit(() => {
|
|
@@ -715,7 +715,8 @@ class ProcessProgress extends qreact.Component<{
|
|
|
715
715
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
716
716
|
class ArchiveViewerControllerBase {
|
|
717
717
|
public async getValuePaths(authority: AuthoritySpec): Promise<string[]> {
|
|
718
|
-
|
|
718
|
+
let { pickedPaths } = await pathValueArchives.getValuePaths(authority);
|
|
719
|
+
return pickedPaths;
|
|
719
720
|
}
|
|
720
721
|
public async getValuePathSizes(files: string[]): Promise<number[]> {
|
|
721
722
|
return pathValueArchives.getValuePathSizes(files);
|
|
@@ -19,6 +19,7 @@ import { isNode } from "typesafecss";
|
|
|
19
19
|
import { isClient } from "../config2";
|
|
20
20
|
import { isLocal } from "../config";
|
|
21
21
|
import { pathWatcher } from "../0-path-value-core/PathWatcher";
|
|
22
|
+
import { debugNodeId } from "../-c-identity/IdentityController";
|
|
22
23
|
|
|
23
24
|
if (!isClient()) {
|
|
24
25
|
// Comment this line out to disable our functionality
|
|
@@ -272,10 +273,12 @@ function trackSyncAge(config: {
|
|
|
272
273
|
|
|
273
274
|
async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[], now: number) {
|
|
274
275
|
let requests: PathTimeRequest[] = [];
|
|
276
|
+
let originalValues = new Map<string, PathValue | undefined>();
|
|
275
277
|
|
|
276
278
|
for (let pathObj of pathsToAudit) {
|
|
277
279
|
let path = pathObj.path;
|
|
278
280
|
let ourLatest = authorityStorage.getValueAtTime(path);
|
|
281
|
+
originalValues.set(path, ourLatest);
|
|
279
282
|
|
|
280
283
|
// Ask for their latest valid (time: undefined means latest)
|
|
281
284
|
requests.push({ path, time: undefined });
|
|
@@ -293,14 +296,32 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
293
296
|
}
|
|
294
297
|
}
|
|
295
298
|
|
|
299
|
+
// Wait. That way, we basically can't have race conditions, as there can't be anything which we have seen both before and after the request, which the remote server hasn't seen, as we're giving it three seconds to receive anything we haven't seen.
|
|
300
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
296
301
|
let responses = await PathAuditerController.nodes[nodeId].getValidStates(requests);
|
|
302
|
+
// Wait again, also for race conditions. That way, if the server saw something, if we wait three seconds, we'll probably have seen it by the time this timeout is up.
|
|
303
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
297
304
|
|
|
298
305
|
let valuesToRequest: PathTimeRequest[] = [];
|
|
299
306
|
let valuesToSend: PathValue[] = [];
|
|
300
307
|
let pathsToForceSync = new Set<string>();
|
|
301
308
|
|
|
302
309
|
for (let response of responses) {
|
|
310
|
+
let originalValue = originalValues.get(response.path);
|
|
303
311
|
let ourValue = authorityStorage.getValueAtTime(response.path) || createMissingEpochValue(response.path);
|
|
312
|
+
|
|
313
|
+
let localValueChanged = false;
|
|
314
|
+
if (originalValue === undefined && ourValue.time !== epochTime) {
|
|
315
|
+
localValueChanged = true;
|
|
316
|
+
} else if (originalValue !== undefined && (originalValue.time !== ourValue.time || originalValue.valid !== ourValue.valid)) {
|
|
317
|
+
localValueChanged = true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (localValueChanged) {
|
|
321
|
+
onPathInteracted(response.path, 2);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
304
325
|
if (ourValue.isTransparent && response.isTransparent) continue;
|
|
305
326
|
|
|
306
327
|
// it's latest valid is newer than ours
|
package/src/path.ts
CHANGED
|
@@ -171,9 +171,16 @@ export function getParentPathStr(pathStr: string) {
|
|
|
171
171
|
return pathStr.slice(0, getStartOfLastPart(pathStr));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/** === getPathFromStr(pathStr).slice(-1)[0] */
|
|
174
|
+
/** === getPathFromStr(pathStr).slice(-1)[0] || "" */
|
|
175
175
|
export function getLastPathPart(pathStr: string) {
|
|
176
|
-
|
|
176
|
+
let lastPartIndex = pathStr.lastIndexOf(pathDelimitEscaped);
|
|
177
|
+
if (lastPartIndex < 0) return "";
|
|
178
|
+
return unescapePathPart(
|
|
179
|
+
pathStr.slice(
|
|
180
|
+
getStartOfLastPart(pathStr),
|
|
181
|
+
lastPartIndex,
|
|
182
|
+
)
|
|
183
|
+
);
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
/** === getPathStr(getPathFromStr(pathStr).slice(0, -1)) */
|
package/src/rangeMath.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type Range = {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type Ranges = Range[];
|
|
7
|
+
|
|
8
|
+
export function rangesOverlap(a: Range, b: Range): boolean {
|
|
9
|
+
return a.start < b.end && a.end > b.start;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function removeRange(ranges: Ranges, range: Range): {
|
|
13
|
+
removedRanges: Range[];
|
|
14
|
+
} {
|
|
15
|
+
let s = range.start;
|
|
16
|
+
let e = range.end;
|
|
17
|
+
let removedRanges: Range[] = [];
|
|
18
|
+
for (let i = ranges.length - 1; i >= 0; i--) {
|
|
19
|
+
let rangeInRanges = ranges[i];
|
|
20
|
+
if (s >= rangeInRanges.end || e <= rangeInRanges.start) continue;
|
|
21
|
+
let startTaken = Math.max(rangeInRanges.start, s);
|
|
22
|
+
let endTaken = Math.min(rangeInRanges.end, e);
|
|
23
|
+
removedRanges.push({ start: startTaken, end: endTaken });
|
|
24
|
+
ranges.splice(i, 1);
|
|
25
|
+
// Add back the parts we didn't overlap
|
|
26
|
+
if (rangeInRanges.start < s) {
|
|
27
|
+
ranges.push({
|
|
28
|
+
start: rangeInRanges.start,
|
|
29
|
+
end: s,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (rangeInRanges.end > e) {
|
|
33
|
+
ranges.push({
|
|
34
|
+
start: e,
|
|
35
|
+
end: rangeInRanges.end,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
return { removedRanges };
|
|
41
|
+
}
|