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,303 @@
|
|
|
1
|
+
import { keyByArray, binarySearchIndex } from "socket-function/src/misc";
|
|
2
|
+
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
3
|
+
import { isNode } from "typesafecss";
|
|
4
|
+
import { isDiskAudit } from "../config";
|
|
5
|
+
import { getPathFromStr } from "../path";
|
|
6
|
+
import { auditLog } from "./auditLogs";
|
|
7
|
+
import { PathRouter } from "./PathRouter";
|
|
8
|
+
import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue } from "./pathValueCore";
|
|
9
|
+
import { pathWatcher } from "./PathWatcher";
|
|
10
|
+
import { lockWatcher2 } from "./LockWatcher2";
|
|
11
|
+
import { onPathInteracted } from "../diagnostics/pathAuditerCallback";
|
|
12
|
+
|
|
13
|
+
class ValidStateComputer {
|
|
14
|
+
|
|
15
|
+
private capturePrevValidStates(valuePaths: PathValue[]): Map<PathValue, boolean | undefined> {
|
|
16
|
+
let prevValidStates = new Map<PathValue, boolean | undefined>();
|
|
17
|
+
for (let value of valuePaths) {
|
|
18
|
+
let prevValidState = authorityStorage.getValueAtTime(value.path, value.time)?.valid;
|
|
19
|
+
prevValidStates.set(value, prevValidState);
|
|
20
|
+
}
|
|
21
|
+
return prevValidStates;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@measureFnc
|
|
25
|
+
public ingestValuesAndValidStates(config: {
|
|
26
|
+
pathValues: PathValue[];
|
|
27
|
+
parentSyncs: { parentPath: string; sourceNodeId: string }[];
|
|
28
|
+
initialTriggers: { values: Set<string>; parentPaths: Set<string> };
|
|
29
|
+
doNotArchive?: boolean;
|
|
30
|
+
}) {
|
|
31
|
+
let { pathValues, parentSyncs, initialTriggers, doNotArchive } = config;
|
|
32
|
+
|
|
33
|
+
// TODO: We might want to add back optimizations for "no watches and no locks"?
|
|
34
|
+
// TODO: If we find it is a serious performance problem, we could just send the valid state instead of sending the full path value and values are rejected (this is what we used to do).
|
|
35
|
+
|
|
36
|
+
let now = Date.now();
|
|
37
|
+
|
|
38
|
+
authorityStorage.addParentSyncs(parentSyncs);
|
|
39
|
+
|
|
40
|
+
let ourValues: PathValue[] = [];
|
|
41
|
+
let notOurValues: PathValue[] = [];
|
|
42
|
+
for (let value of pathValues) {
|
|
43
|
+
if (!PathRouter.isSelfAuthority(value.path)) {
|
|
44
|
+
notOurValues.push(value);
|
|
45
|
+
} else {
|
|
46
|
+
ourValues.push(value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
authorityStorage.ingestValues(notOurValues, { doNotArchive });
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// Use initial triggers, at least for all the values that we aren't the authority of, to clobber the old values, creating new shallow copies of the old values with a valid state false if we don't have the corresponding value in our new values, including for all parent paths.
|
|
53
|
+
{
|
|
54
|
+
// TODO: This code is similar to the code in trigger values change, but it's also dealing with watchers and other stuff, so I think it makes sense to have it also here.
|
|
55
|
+
let fullInitialPaths = new Set<string>();
|
|
56
|
+
for (let path of initialTriggers?.values ?? []) {
|
|
57
|
+
fullInitialPaths.add(path);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// initialTriggers.parentPaths => triggerPaths
|
|
61
|
+
for (let parentPath of initialTriggers.parentPaths) {
|
|
62
|
+
let valuePaths = authorityStorage.getPathsFromParent(parentPath);
|
|
63
|
+
for (let valuePath of valuePaths || []) {
|
|
64
|
+
fullInitialPaths.add(valuePath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let fullInitialPathsArr = Array.from(fullInitialPaths);
|
|
69
|
+
fullInitialPathsArr = fullInitialPathsArr.filter(x => !PathRouter.isSelfAuthority(x));
|
|
70
|
+
|
|
71
|
+
let valueByPath = keyByArray(notOurValues, x => x.path);
|
|
72
|
+
for (let path of fullInitialPathsArr) {
|
|
73
|
+
let newValues = valueByPath.get(path) ?? [];
|
|
74
|
+
let prevValues = authorityStorage.getValuePlusHistory(path);
|
|
75
|
+
|
|
76
|
+
newValues.sort((a, b) => compareTime(a.time, b.time));
|
|
77
|
+
|
|
78
|
+
for (let value of prevValues) {
|
|
79
|
+
let newIndex = binarySearchIndex(newValues.length, i => compareTime(newValues[i].time, value.time));
|
|
80
|
+
if (newIndex < 0) {
|
|
81
|
+
let newValue = newValues.at(-1);
|
|
82
|
+
console.info(`Rejecting past value due to initial sync: ${debugPathValuePath(value)}`, {
|
|
83
|
+
path: value.path,
|
|
84
|
+
timeId: value.time.time,
|
|
85
|
+
newTimeId: newValue?.time.time,
|
|
86
|
+
});
|
|
87
|
+
ourValues.push({ ...value, valid: false, });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
let initialPrevValidStates: Map<PathValue, boolean | undefined> | undefined = this.capturePrevValidStates(ourValues);
|
|
96
|
+
authorityStorage.ingestValues(ourValues, { doNotArchive });
|
|
97
|
+
|
|
98
|
+
// NOTE: Watch value locks, will internally watch remote locks if necessary.
|
|
99
|
+
lockWatcher2.watchValueLocks(ourValues, now);
|
|
100
|
+
|
|
101
|
+
// NOTE: Initial values always get put into computeValidStates, which then ingests, but does not compute values we are not the authority for.
|
|
102
|
+
let valuesChanged = new Set<PathValue>(pathValues);
|
|
103
|
+
let dependenciesChanged = new Set<PathValue>(ourValues);
|
|
104
|
+
let alreadyTriggered = new Set<PathValue>(dependenciesChanged);
|
|
105
|
+
|
|
106
|
+
let loopCount = 0;
|
|
107
|
+
|
|
108
|
+
while (dependenciesChanged.size > 0) {
|
|
109
|
+
loopCount++;
|
|
110
|
+
if (loopCount > 3000) {
|
|
111
|
+
console.error("Too many loops in valid state loop. aborting.", {
|
|
112
|
+
loopCount,
|
|
113
|
+
changedCount: dependenciesChanged.size,
|
|
114
|
+
totalTriggered: alreadyTriggered.size,
|
|
115
|
+
exampleChanged: Array.from(dependenciesChanged)[0].path,
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
// Just because the dependencies change doesn't mean the valid state has changed. We actually have to compute to see if it has changed. Of course, if we're not the authority, we won't compute the valid site at all. It'll just ignore it, which is good. If we're not the authority, we don't want to be doing recursive computation. Because we prepopulate it, the values change with all of the input values, we'll still get a trigger. We just won't get a nested trigger.
|
|
120
|
+
let validStateChanged = validStateComputer.computeValidStates(
|
|
121
|
+
Array.from(dependenciesChanged),
|
|
122
|
+
now,
|
|
123
|
+
initialPrevValidStates,
|
|
124
|
+
).changed;
|
|
125
|
+
initialPrevValidStates = undefined;
|
|
126
|
+
dependenciesChanged.clear();
|
|
127
|
+
|
|
128
|
+
for (let validStateChange of validStateChanged) {
|
|
129
|
+
valuesChanged.add(validStateChange);
|
|
130
|
+
// IMPORTANT! The consequence of this is that if someone predicts a value, sends it to us, it's not valid, we will see it's not valid, and we will tell all of the nodes that are watching that path about all of the values on that path. This is inefficient. However, rejected values should be quite rare, so it shouldn't be too inefficient.
|
|
131
|
+
if (!validStateChange.valid) {
|
|
132
|
+
// If it's not valid, we need to send the next available valid value. But we haven't done all the fullness computation yet, so the current next available valid value might be recomputed, it might be invalid. So we'll just do the initial trigger. It's not going to be that expensive as the GCing should mean that most paths have a small history, and rejections should be quite rare.
|
|
133
|
+
initialTriggers.values.add(validStateChange.path);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// And then for everything that had a valid state change, go check if it has a dependency that was watching it.
|
|
137
|
+
for (let value of validStateChanged) {
|
|
138
|
+
let watchers = lockWatcher2.getValuePathWatchers(value);
|
|
139
|
+
for (let watcher of watchers) {
|
|
140
|
+
if (alreadyTriggered.has(watcher)) continue;
|
|
141
|
+
dependenciesChanged.add(watcher);
|
|
142
|
+
alreadyTriggered.add(watcher);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pathWatcher.triggerValuesChanged({
|
|
148
|
+
valuesChanged,
|
|
149
|
+
initialTriggers
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private isLockValid(lock: ReadLock): boolean {
|
|
154
|
+
let value = authorityStorage.getValueAtTime(lock.path, lock.endTime);
|
|
155
|
+
if (!value) {
|
|
156
|
+
// If not synced, we don't want to reject it yet
|
|
157
|
+
return !authorityStorage.isSynced(lock.path);
|
|
158
|
+
}
|
|
159
|
+
return !!value.valid;
|
|
160
|
+
}
|
|
161
|
+
private isLockContentionFree(lock: ReadLock): PathValue | undefined {
|
|
162
|
+
// sorted by -time (newest are first)
|
|
163
|
+
let history = authorityStorage.getValuePlusHistory(lock.path);
|
|
164
|
+
|
|
165
|
+
let index = binarySearchIndex(history.length, i => compareTime(lock.endTime, history[i].time));
|
|
166
|
+
if (index < 0) index = ~index;
|
|
167
|
+
else index++;
|
|
168
|
+
for (let i = index; i < history.length; i++) {
|
|
169
|
+
let value = history[i];
|
|
170
|
+
if (!value.valid) continue;
|
|
171
|
+
// It's fine, they have equivalent values. It was probably gced by the writer, which is fine.
|
|
172
|
+
if (value.isTransparent && lock.readIsTransparent) continue;
|
|
173
|
+
// If value.time is within our range, there's contention!
|
|
174
|
+
if (compareTime(lock.startTime, value.time) < 0 && compareTime(value.time, lock.endTime) < 0) {
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@measureFnc
|
|
182
|
+
public computeValidStates(
|
|
183
|
+
valuePaths: PathValue[],
|
|
184
|
+
now: number,
|
|
185
|
+
prevValidStates = this.capturePrevValidStates(valuePaths)
|
|
186
|
+
): { changed: PathValue[] } {
|
|
187
|
+
let changed: PathValue[] = [];
|
|
188
|
+
|
|
189
|
+
let lockGroups = byLockGroup(valuePaths);
|
|
190
|
+
|
|
191
|
+
for (let [locks, valueGroup] of lockGroups) {
|
|
192
|
+
if (valueGroup.length === 0) continue;
|
|
193
|
+
|
|
194
|
+
let valid = true;
|
|
195
|
+
for (let lock of locks) {
|
|
196
|
+
if (!this.isLockValid(lock)) {
|
|
197
|
+
onPathInteracted(lock.path, 2);
|
|
198
|
+
valid = false;
|
|
199
|
+
this.logLockInvalid(valueGroup, lock, now);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
let contention = this.isLockContentionFree(lock);
|
|
203
|
+
if (contention) {
|
|
204
|
+
onPathInteracted(lock.path, 2);
|
|
205
|
+
valid = false;
|
|
206
|
+
this.logLockContention(valueGroup, lock, contention, now);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (let pathValue of valueGroup) {
|
|
211
|
+
let prevValidState = prevValidStates.get(pathValue);
|
|
212
|
+
// IMPORTANT! This means if it didn't previously exist and it's presently rejected, we count that as a change, as it this is going from undefined to false. This is actually very useful, as a lot of places want to know if a write is rejected, so they can display it in the UI for the developer.
|
|
213
|
+
if (valid === prevValidState) continue;
|
|
214
|
+
|
|
215
|
+
changed.push(pathValue);
|
|
216
|
+
|
|
217
|
+
// NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
|
|
218
|
+
(pathValue as any).valid = valid;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { changed };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private logLockInvalid(valueGroup: PathValue[], lock: ReadLock, now: number) {
|
|
226
|
+
// This is good... unless it happens A LOT. Then the app needs to change (unless it
|
|
227
|
+
// is a game, or something with realtime competition, in which case this is probably unavoidable).
|
|
228
|
+
if (valueGroup.length > 0 && lock.endTime.version !== Number.MAX_SAFE_INTEGER && lock.startTime.version !== -2) {
|
|
229
|
+
if ((!isCoreQuiet || !isNode()) && debugRejections) {
|
|
230
|
+
let timeToReject = now - valueGroup[0].time.time;
|
|
231
|
+
let message = `!!! (VALUE REJECTED) VALUE REJECTED DUE TO USING MISSING / REJECTED READ!!!(rejected after ${timeToReject}ms at ${Date.now()})`;
|
|
232
|
+
message += `\n${debugTime(valueGroup[0].time)} (write)`;
|
|
233
|
+
message += `\n rejected as the server could not find: `;
|
|
234
|
+
if (lock.readIsTransparent) {
|
|
235
|
+
message += `\n${debugTime(lock.startTime)} to ${debugTime(lock.endTime)} ${getPathFromStr(lock.path).join(".")} `;
|
|
236
|
+
} else {
|
|
237
|
+
message += `\n${debugTime(lock.startTime)} ${getPathFromStr(lock.path).join(".")} `;
|
|
238
|
+
}
|
|
239
|
+
if (lock.readIsTransparent) {
|
|
240
|
+
message += `\n (read was undefined, so presumably a value exists which the writer missed)`;
|
|
241
|
+
}
|
|
242
|
+
message += `\nFull list of writes rejected: `;
|
|
243
|
+
for (let pathValue of valueGroup) {
|
|
244
|
+
message += `\n${debugPathValue(pathValue)}`;
|
|
245
|
+
}
|
|
246
|
+
console.error(message);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (isDiskAudit()) {
|
|
250
|
+
for (let pathValue of valueGroup) {
|
|
251
|
+
auditLog("VALUE REJECTED, we did not see a required value (we are missing their value, or their value was rejected)", {
|
|
252
|
+
path: pathValue.path,
|
|
253
|
+
timeId: pathValue.time.time,
|
|
254
|
+
missingPath: lock.path,
|
|
255
|
+
missingPathTimeId: lock.startTime.time,
|
|
256
|
+
transparent: lock.readIsTransparent,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
private logLockContention(valueGroup: PathValue[], lock: ReadLock, conflictingValue: PathValue, now: number) {
|
|
263
|
+
|
|
264
|
+
// This special version indicates clientside prediction (which SHOULD be rejected).
|
|
265
|
+
if (lock.endTime.version !== Number.MAX_SAFE_INTEGER) {
|
|
266
|
+
|
|
267
|
+
let changed = valueGroup.filter(x => x.valid);
|
|
268
|
+
if (changed.length > 0) {
|
|
269
|
+
if ((!isCoreQuiet || !isNode())
|
|
270
|
+
|| debugRejections
|
|
271
|
+
) {
|
|
272
|
+
let timeToReject = now - changed[0].time.time;
|
|
273
|
+
let redMessage = `!!! (VALUE REJECTED) LOCK CONTENTION FIXED VIA REJECTION OF VALUE!!!(rejected after ${timeToReject}ms)`;
|
|
274
|
+
for (let pathValue of changed) {
|
|
275
|
+
redMessage += `\n${debugPathValue(pathValue)}`;
|
|
276
|
+
}
|
|
277
|
+
redMessage += `\n (write rejected due to original read not noticing value at: ${lock.path})`;
|
|
278
|
+
redMessage += `\n (original read from: ${debugTime(lock.startTime)}`;
|
|
279
|
+
redMessage += `\n (at time: ${debugTime(lock.endTime)}`;
|
|
280
|
+
redMessage += `\n (current time: ${Date.now()})`;
|
|
281
|
+
redMessage += `\n (conflict write at: ${debugTime(conflictingValue.time)}`;
|
|
282
|
+
console.error(redMessage);
|
|
283
|
+
}
|
|
284
|
+
if (isDiskAudit()) {
|
|
285
|
+
for (let pathValue of changed) {
|
|
286
|
+
auditLog("VALUE REJECTED, remote write did not see a conflicting value (remote is missing a value)", {
|
|
287
|
+
lock,
|
|
288
|
+
path: pathValue.path,
|
|
289
|
+
timeId: pathValue.time.time,
|
|
290
|
+
lockedPath: lock.path,
|
|
291
|
+
transparent: lock.readIsTransparent,
|
|
292
|
+
missingPath: lock.path,
|
|
293
|
+
conflictTimeId0: conflictingValue.time.time,
|
|
294
|
+
conflictingWrites: conflictingValue,
|
|
295
|
+
hadTimeId: lock.startTime.time,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export const validStateComputer = new ValidStateComputer();
|
|
@@ -149,6 +149,6 @@ export interface ArchiveLocker {
|
|
|
149
149
|
* this will probably work. AND if you are reverting to an old snapshot, something that will
|
|
150
150
|
* probably work is better than a data state which is definitely broken.
|
|
151
151
|
*/
|
|
152
|
-
unsafeSetFiles(files: string[]): Promise<void>;
|
|
152
|
+
unsafeSetFiles(files: string[], config?: { hackNoDelete?: boolean }): Promise<void>;
|
|
153
153
|
unsafeGetFileLocation(file: string): Promise<"live" | "zombie" | "recycled" | "missing">;
|
|
154
154
|
}
|
|
@@ -15,6 +15,12 @@ import { getNodeId } from "socket-function/src/nodeCache";
|
|
|
15
15
|
import { logNodeStateStats, logNodeStats } from "../../-0-hooks/hooks";
|
|
16
16
|
import { parsePath, toFileNameKVP, parseFileNameKVP } from "../../misc";
|
|
17
17
|
import { logDisk } from "../../diagnostics/logs/diskLogger";
|
|
18
|
+
import { retryFunctional, runInParallel } from "socket-function/src/batching";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
Data starts a .data files, from servers creating files.
|
|
22
|
+
Then we create .locked files, and .transactions, And when we apply these transactions, it will create .confirm files which make the .locked files count.
|
|
23
|
+
*/
|
|
18
24
|
|
|
19
25
|
/** Clean up old files after a while */
|
|
20
26
|
const DEAD_CREATE_THRESHOLD = timeInHour * 12;
|
|
@@ -90,60 +96,83 @@ export function createArchiveLocker2(config: {
|
|
|
90
96
|
}
|
|
91
97
|
},
|
|
92
98
|
};
|
|
93
|
-
async function unsafeSetFiles(files: string[]): Promise<void> {
|
|
99
|
+
async function unsafeSetFiles(files: string[], config?: { hackNoDelete?: boolean }): Promise<void> {
|
|
94
100
|
let valuePaths = new Set((await archiveValues.findInfo("")).map(x => x.path));
|
|
95
101
|
let correctConfirms = new Set<string>();
|
|
96
|
-
let correctFiles = new Set<string>();
|
|
102
|
+
let correctFiles = new Set<string>(files);
|
|
97
103
|
|
|
98
104
|
async function deleteAllOtherFiles() {
|
|
105
|
+
if (config?.hackNoDelete) return;
|
|
99
106
|
// Delete all old confirms / transactions / etc
|
|
100
107
|
let allConfirms = (await archiveLocks.findInfo("")).map(x => x.path);
|
|
101
|
-
|
|
108
|
+
console.log(`Found ${allConfirms.length} confirms to delete`);
|
|
109
|
+
let deleteCount = 0;
|
|
110
|
+
async function deleteConfirm(confirm: string) {
|
|
111
|
+
deleteCount++;
|
|
102
112
|
if (correctConfirms.has(confirm)) return;
|
|
103
113
|
await storage.deleteKey(confirm);
|
|
104
|
-
|
|
114
|
+
console.log(`Deleted confirm ${confirm}, ${deleteCount}/${allConfirms.length}`);
|
|
115
|
+
}
|
|
116
|
+
let deleteConfirmParallel = runInParallel({ parallelCount: 8 }, deleteConfirm);
|
|
117
|
+
await Promise.all(allConfirms.map(deleteConfirmParallel));
|
|
118
|
+
console.log(`Deleted ${allConfirms.length} confirms`);
|
|
105
119
|
|
|
106
120
|
valuePaths = new Set((await archiveValues.findInfo("")).map(x => x.path));
|
|
121
|
+
console.log(`Found ${valuePaths.size} files to delete`);
|
|
107
122
|
// Delete all files we didn't just set (move to recycle bin)
|
|
108
|
-
|
|
123
|
+
let moveCount = 0;
|
|
124
|
+
async function moveFile(file: string) {
|
|
125
|
+
moveCount++;
|
|
109
126
|
if (correctFiles.has(file)) return;
|
|
127
|
+
let info = await archiveValues.getInfo(file);
|
|
128
|
+
console.log(`Moving file ${file}, ${moveCount}/${valuePaths.size}, ${formatNumber(info?.size || 0)} bytes`);
|
|
110
129
|
await archiveValues.move({
|
|
111
130
|
path: file,
|
|
112
131
|
targetPath: file,
|
|
113
132
|
target: archiveRecycleBin,
|
|
114
133
|
});
|
|
115
|
-
|
|
134
|
+
console.log(`Moved file ${file}, ${moveCount}/${valuePaths.size}`);
|
|
135
|
+
}
|
|
136
|
+
let moveFileParallel = runInParallel({ parallelCount: 8 }, moveFile);
|
|
137
|
+
await Promise.all(Array.from(valuePaths).map(moveFileParallel));
|
|
138
|
+
console.log(`Deleted ${valuePaths.size} files`);
|
|
116
139
|
}
|
|
117
140
|
|
|
118
|
-
console.log(magenta(`Deleting all other files`));
|
|
141
|
+
console.log(magenta(`Deleting all other files (this may take a while)`));
|
|
119
142
|
// Deleting early deletes any transactions, which might be reapplied due to our non-transaction changes
|
|
120
143
|
await deleteAllOtherFiles();
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
let restoreCount = 0;
|
|
146
|
+
async function restoreFile(file: string): Promise<void> {
|
|
147
|
+
if (await archiveValues.get(file)) {
|
|
148
|
+
console.log(`File ${file} is already in the archive values, skipping, ${restoreCount++}/${files.length}`);
|
|
149
|
+
return;
|
|
127
150
|
}
|
|
128
151
|
let isInRecycleBin = !!(await archiveRecycleBin.getInfo(file));
|
|
129
152
|
if (isInRecycleBin) {
|
|
130
|
-
console.log(`Restoring ${file}`);
|
|
131
|
-
|
|
153
|
+
console.log(`Restoring ${file}, ${restoreCount++}/${files.length}`);
|
|
154
|
+
await archiveRecycleBin.move({
|
|
132
155
|
path: file,
|
|
133
156
|
targetPath: file,
|
|
134
157
|
target: archiveValues,
|
|
135
|
-
})
|
|
136
|
-
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`File ${file} is not in the recycle bin, skipping, ${restoreCount++}/${files.length}`);
|
|
137
161
|
}
|
|
138
162
|
}
|
|
139
|
-
|
|
163
|
+
const restoreFileParallel = runInParallel({ parallelCount: 16 }, restoreFile);
|
|
164
|
+
await Promise.all(files.map(restoreFileParallel));
|
|
140
165
|
|
|
141
166
|
console.log(magenta(`Creating confirms`));
|
|
167
|
+
let createCount = 0;
|
|
142
168
|
// Confirm all files
|
|
143
|
-
|
|
144
|
-
let confirm = await locker.createConfirm(file);
|
|
169
|
+
async function createConfirm(file: string): Promise<void> {
|
|
170
|
+
let confirm = await locker.createConfirm(file, correctFiles.size - createCount);
|
|
145
171
|
correctConfirms.add(confirm);
|
|
146
|
-
|
|
172
|
+
console.log(`Created confirm ${confirm}, ${createCount++}/${correctFiles.size}`);
|
|
173
|
+
}
|
|
174
|
+
let createConfirmParallel = runInParallel({ parallelCount: 4 }, retryFunctional(createConfirm));
|
|
175
|
+
await Promise.all(Array.from(correctFiles).map(createConfirmParallel));
|
|
147
176
|
|
|
148
177
|
console.log(magenta(`Deleting all other files again`));
|
|
149
178
|
// Delete again, in case anything else was created while we were restoring. Not great, but... should
|
|
@@ -184,6 +213,7 @@ export function createArchiveLocker2(config: {
|
|
|
184
213
|
) => Promise<ArchiveTransaction[]>
|
|
185
214
|
): Promise<"accepted" | "rejected"> {
|
|
186
215
|
let files = await locker.getFiles();
|
|
216
|
+
await saveSnapshot({ files: files.map(a => a.file) });
|
|
187
217
|
let readFiles = async (files: FileInfo[]) => {
|
|
188
218
|
let pendingFiles = files.slice();
|
|
189
219
|
let readResults = new Map<string, Buffer | undefined>();
|
|
@@ -231,7 +261,6 @@ export function createArchiveLocker2(config: {
|
|
|
231
261
|
let unhandled: never = type;
|
|
232
262
|
}
|
|
233
263
|
}
|
|
234
|
-
logErrors(saveSnapshot({ files: Array.from(newFiles) }));
|
|
235
264
|
}
|
|
236
265
|
return status;
|
|
237
266
|
}
|
|
@@ -259,7 +288,7 @@ type StorageType = {
|
|
|
259
288
|
* all writes should now be readable.
|
|
260
289
|
*/
|
|
261
290
|
};
|
|
262
|
-
type Transaction = {
|
|
291
|
+
export type Transaction = {
|
|
263
292
|
ops: ({
|
|
264
293
|
type: "create";
|
|
265
294
|
key: string;
|
|
@@ -307,15 +336,16 @@ class TransactionLocker {
|
|
|
307
336
|
let { dir, name } = parsePath(key);
|
|
308
337
|
return `${dir}confirm_${name}.confirm`;
|
|
309
338
|
}
|
|
310
|
-
public async createConfirm(key: string) {
|
|
339
|
+
public async createConfirm(key: string, countLeft: number) {
|
|
311
340
|
let path = this.getConfirmKey(key);
|
|
312
|
-
console.info(
|
|
313
|
-
await this.storage.
|
|
341
|
+
console.info(`Creating confirmation for ${key}, ${countLeft} left`);
|
|
342
|
+
if (!await this.storage.getValue(path)) {
|
|
343
|
+
await this.storage.setValue(path, Buffer.from(""));
|
|
344
|
+
}
|
|
314
345
|
return path;
|
|
315
346
|
}
|
|
316
|
-
private async deleteDataFile(key: string, reason: string): Promise<void> {
|
|
317
|
-
console.log(red(`Deleting data file ${key}, because ${reason}`));
|
|
318
|
-
//await this.storage.setValue(key + ".reason", Buffer.from(reason));
|
|
347
|
+
private async deleteDataFile(key: string, reason: string, countLeft: number): Promise<void> {
|
|
348
|
+
console.log(red(`Deleting data file ${key}, because ${reason}, ${countLeft} left`));
|
|
319
349
|
// Delete file, and confirmation as well
|
|
320
350
|
await this.storage.deleteKey(key);
|
|
321
351
|
await this.storage.deleteKey(this.getConfirmKey(key));
|
|
@@ -472,13 +502,22 @@ class TransactionLocker {
|
|
|
472
502
|
|
|
473
503
|
let existingFiles = new Map(files.map(a => [a.file, a]));
|
|
474
504
|
|
|
505
|
+
let uncomfirmedCount = 0;
|
|
506
|
+
|
|
475
507
|
let currentDataFiles = new Map<string, FileInfo>();
|
|
508
|
+
let countByExtension = new Map<string, { value: number }>();
|
|
476
509
|
for (let file of files) {
|
|
510
|
+
let extension = file.file.split(".").pop() || "";
|
|
511
|
+
let existing = countByExtension.get(extension);
|
|
512
|
+
if (!existing) {
|
|
513
|
+
existing = { value: 0 };
|
|
514
|
+
countByExtension.set(extension, existing);
|
|
515
|
+
}
|
|
516
|
+
existing.value++;
|
|
477
517
|
if (!(
|
|
478
518
|
file.file.endsWith(".locked")
|
|
479
519
|
|| file.file.endsWith(".confirm")
|
|
480
520
|
|| file.file.endsWith(".transaction")
|
|
481
|
-
|| file.file.endsWith(".reason")
|
|
482
521
|
)) {
|
|
483
522
|
currentDataFiles.set(file.file, file);
|
|
484
523
|
continue;
|
|
@@ -488,14 +527,17 @@ class TransactionLocker {
|
|
|
488
527
|
let confirmFile = existingFiles.get(confirmKey);
|
|
489
528
|
if (confirmFile) {
|
|
490
529
|
currentDataFiles.set(file.file, file);
|
|
530
|
+
} else {
|
|
531
|
+
uncomfirmedCount++;
|
|
491
532
|
}
|
|
492
533
|
}
|
|
493
|
-
|
|
494
|
-
console.info("Read archive state", {
|
|
534
|
+
console.log("Read archive state", {
|
|
495
535
|
rawFilesCount: files.length,
|
|
496
536
|
confirmedCount: currentDataFiles.size,
|
|
497
|
-
|
|
498
|
-
|
|
537
|
+
uncomfirmedCount,
|
|
538
|
+
countByExtention: Array.from(countByExtension.entries()).map(([extension, value]) => `${extension}: ${value.value}`).join(" | "),
|
|
539
|
+
//rawFiles: files.map(a => a.file),
|
|
540
|
+
//confirmedFiles: Array.from(currentDataFiles.values()).map(a => a.file),
|
|
499
541
|
});
|
|
500
542
|
|
|
501
543
|
return {
|
|
@@ -586,9 +628,9 @@ class TransactionLocker {
|
|
|
586
628
|
let op = opsRemaining.pop();
|
|
587
629
|
if (!op) return;
|
|
588
630
|
if (op.type === "create") {
|
|
589
|
-
await this.createConfirm(op.key);
|
|
631
|
+
await this.createConfirm(op.key, opsRemaining.length);
|
|
590
632
|
} else if (op.type === "delete") {
|
|
591
|
-
await this.deleteDataFile(op.key, `transaction (${getOwnNodeId()})
|
|
633
|
+
await this.deleteDataFile(op.key, `transaction (${getOwnNodeId()})`, opsRemaining.length);
|
|
592
634
|
} else {
|
|
593
635
|
let unhandled: never = op;
|
|
594
636
|
throw new Error(`Unhandled type: ${unhandled}`);
|
|
@@ -706,8 +748,9 @@ class TransactionLocker {
|
|
|
706
748
|
console.warn(red(`Deleted ${unconfirmedOldFiles2.length} very old unconfirmed files`), { files: unconfirmedOldFiles2.map(x => x.file) });
|
|
707
749
|
logNodeStats(`archives|TΔ Delete Old Rejected File`, formatNumber, unconfirmedOldFiles2.length);
|
|
708
750
|
// At the point the file was very old when we started reading, not part of the active transaction.
|
|
709
|
-
for (let
|
|
710
|
-
|
|
751
|
+
for (let i = 0; i < unconfirmedOldFiles2.length; i++) {
|
|
752
|
+
let file = unconfirmedOldFiles2[i];
|
|
753
|
+
await this.deleteDataFile(file.file, `old unconfirmed file`, unconfirmedOldFiles2.length - i);
|
|
711
754
|
}
|
|
712
755
|
} else {
|
|
713
756
|
console.warn(`Almost deleted ${unconfirmedOldFiles.length} very old unconfirmed files. This is bad, did we miss their confirmations that first time? If we missed them twice in a row, we might literally delete the database, and need to enter recovery mode to fix it...`, { files: unconfirmedOldFiles });
|
|
@@ -740,13 +783,6 @@ class TransactionLocker {
|
|
|
740
783
|
}
|
|
741
784
|
}
|
|
742
785
|
|
|
743
|
-
// Delete some debug files
|
|
744
|
-
{
|
|
745
|
-
for (let file of dataState.rawDataFiles.filter(x => x.file.endsWith(".reason"))) {
|
|
746
|
-
await this.storage.deleteKey(file.file);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
786
|
return {
|
|
751
787
|
dataFiles: Array.from(dataState.confirmedDataFiles),
|
|
752
788
|
rawDataFiles: dataState.rawDataFiles,
|
|
@@ -4,7 +4,7 @@ import { getAllNodeIds, getOwnThreadId } from "../../-f-node-discovery/NodeDisco
|
|
|
4
4
|
import { pathValueArchives } from "../pathValueArchives";
|
|
5
5
|
import { ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../../errors";
|
|
6
6
|
import { green, magenta } from "socket-function/src/formatting/logColors";
|
|
7
|
-
import { devDebugbreak } from "../../config";
|
|
7
|
+
import { devDebugbreak, isPublic } from "../../config";
|
|
8
8
|
import { sort } from "socket-function/src/misc";
|
|
9
9
|
import { lazy } from "socket-function/src/caching";
|
|
10
10
|
|
|
@@ -56,6 +56,7 @@ export async function saveSnapshot(config: {
|
|
|
56
56
|
let { files } = config;
|
|
57
57
|
let overview = getSnapshotOverview(files);
|
|
58
58
|
await snapshots().set(overview.file, Buffer.from(files.join("\n")));
|
|
59
|
+
return overview;
|
|
59
60
|
}
|
|
60
61
|
function overviewToFileName(overview: Omit<ArchiveSnapshotOverview, "file">): string {
|
|
61
62
|
return Object.entries({
|
|
@@ -152,10 +153,11 @@ async function getSnapshotBase(snapshotFile: string | "live") {
|
|
|
152
153
|
// although you will still have to take most of the servers down to get back
|
|
153
154
|
// to a consistent server state.
|
|
154
155
|
export async function loadSnapshot(config: {
|
|
155
|
-
overview:
|
|
156
|
-
|
|
156
|
+
overview: { file: string; };
|
|
157
|
+
noExit?: boolean;
|
|
158
|
+
hackNoDelete?: boolean;
|
|
157
159
|
}): Promise<void> {
|
|
158
|
-
let { overview
|
|
160
|
+
let { overview } = config;
|
|
159
161
|
console.log(magenta(`Loading snapshot: ${overview.file}`));
|
|
160
162
|
let locker = await pathValueArchives.getArchiveLocker();
|
|
161
163
|
|
|
@@ -166,20 +168,15 @@ export async function loadSnapshot(config: {
|
|
|
166
168
|
let files = snapshotData.toString().split("\n");
|
|
167
169
|
|
|
168
170
|
console.log(magenta(`Unsafe setting snapshot files: ${overview.file}`));
|
|
169
|
-
await locker.unsafeSetFiles(files);
|
|
171
|
+
await locker.unsafeSetFiles(files, { hackNoDelete: config.hackNoDelete });
|
|
170
172
|
console.log(magenta(`Finished unsafe setting snapshot files: ${overview.file}`));
|
|
171
173
|
|
|
172
|
-
let allNodes = await getAllNodeIds();
|
|
173
|
-
|
|
174
|
-
// Logging the errors, not ignoring them
|
|
175
|
-
if (audit) {
|
|
176
|
-
await timeoutToUndefinedSilent(10_000, Promise.all(allNodes.map(async nodeId => {
|
|
177
|
-
if (!audit) return;
|
|
178
|
-
await audit(nodeId);
|
|
179
|
-
})));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
174
|
for (let i = 0; i < 10; i++) {
|
|
183
175
|
console.log(green(`!!!Finished loading snapshot!!!`));
|
|
184
176
|
}
|
|
185
|
-
|
|
177
|
+
|
|
178
|
+
if (!config.noExit && !isPublic()) {
|
|
179
|
+
// Exit so we know when this is done. Really, all the servers need to be restarted after we load the snapshot, anyway.
|
|
180
|
+
process.exit();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -47,7 +47,9 @@ export function getLogHistoryEquals(value: string) {
|
|
|
47
47
|
(globalThis as any).getLogHistoryEquals = getLogHistoryEquals;
|
|
48
48
|
(globalThis as any).getPathAuditLogs = getLogHistoryEquals;
|
|
49
49
|
|
|
50
|
+
// NOTE: It's good practice to check isDebugLogEnabled before calling this, as it's expensive to create an object if we're just going to throw it away.
|
|
50
51
|
export function auditLog(type: string, values: { [key: string]: unknown }) {
|
|
52
|
+
if (!ENABLED_LOGGING) return;
|
|
51
53
|
debugLogFnc(type, values);
|
|
52
54
|
}
|
|
53
55
|
let debugLogFnc = debugLogBase;
|