querysub 0.592.0 → 0.594.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/.claude/settings.local.json +5 -1
- package/package.json +3 -3
- package/spec.txt +12 -0
- package/src/-a-archives/archives2.ts +22 -18
- package/src/-d-trust/NetworkTrust2.ts +1 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/0-path-value-core/PathRouter.ts +17 -16
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +116 -32
- package/src/0-path-value-core/pathValueArchives.ts +8 -5
- package/src/2-proxy/archiveMoveHarness.ts +1 -1
- package/src/archiveapps/archiveJoinEntry.ts +1 -1
- package/src/deployManager/components/StoragePage.tsx +2 -1
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +1 -1
- package/src/library-components/SyncedController.ts +3 -5
- package/src/storageSetup.ts +1 -1
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
"WebFetch(domain:developers.cloudflare.com)",
|
|
32
32
|
"PowerShell(yarn type *)",
|
|
33
33
|
"PowerShell(Remove-Item -Confirm:$false \"C:\\\\Users\\\\quent\\\\.claude\\\\projects\\\\D--repos-querysub\\\\memory\\\\no-naming-new-bins.md\")",
|
|
34
|
-
"Bash(python -)"
|
|
34
|
+
"Bash(python -)",
|
|
35
|
+
"PowerShell(yarn upreal *)",
|
|
36
|
+
"PowerShell($env:COMPUTERNAME; $env:USERNAME)",
|
|
37
|
+
"PowerShell($env:COMPUTERNAME)",
|
|
38
|
+
"PowerShell($env:USERNAME)"
|
|
35
39
|
]
|
|
36
40
|
}
|
|
37
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.594.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
75
75
|
"pako": "^2.1.0",
|
|
76
76
|
"peggy": "^5.0.6",
|
|
77
|
-
"sliftutils": "^1.7.
|
|
78
|
-
"socket-function": "^1.2.
|
|
77
|
+
"sliftutils": "^1.7.74",
|
|
78
|
+
"socket-function": "^1.2.29",
|
|
79
79
|
"terser": "^5.31.0",
|
|
80
80
|
"typenode": "^6.6.1",
|
|
81
81
|
"typesafecss": "^0.32.0",
|
package/spec.txt
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
It could be it's just taking a while to get valid. Let's check it two times to see. Although if it is, that's a bug, but it's probably an easy bug to fix.
|
|
2
|
+
|
|
3
|
+
Wait, what transaction is it applying now? If it's not a valid transaction and we don't even write it, so what?
|
|
4
|
+
|
|
5
|
+
Well, we might have to restore from a snapshot if it breaks it, but I'm pretty confident we're not going to lose all of our data.
|
|
6
|
+
|
|
7
|
+
Ugh... I mean it seems like our storage is just really out of sync. Like, we're reading files and it changes every time, etc.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
1
13
|
Trigger values
|
|
2
14
|
1) validStateComputer.ingestValuesAndValidStates
|
|
3
15
|
1.5) authorityStorage.ingestValues
|
|
@@ -4,6 +4,7 @@ import { cache, cacheJSONArgsEqual } from "socket-function/src/caching";
|
|
|
4
4
|
import { formatDateTime } from "socket-function/src/formatting/format";
|
|
5
5
|
import { getDomain } from "../config";
|
|
6
6
|
import { timeInMinute } from "socket-function/src/misc";
|
|
7
|
+
import { FindConfig } from "sliftutils/storage/IArchives";
|
|
7
8
|
|
|
8
9
|
export const STORAGE_ACCOUNT = "root";
|
|
9
10
|
|
|
@@ -44,31 +45,34 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
|
|
|
44
45
|
const ONTARIO = `https://99-250-124-91.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
|
|
45
46
|
const HETZNER = `https://65-109-93-113.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
|
|
46
47
|
const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
|
|
48
|
+
|
|
49
|
+
function stripeSources(urls: string[]) {
|
|
50
|
+
let sources: Omit<Exclude<RemoteConfigBase, string>, "validWindow">[] = [];
|
|
51
|
+
for (let j = 0; j < urls.length; j++) {
|
|
52
|
+
// Shift by j
|
|
53
|
+
let currentUrls = urls.slice(j).concat(urls.slice(0, j));
|
|
54
|
+
for (let i = 0; i < urls.length; i++) {
|
|
55
|
+
sources.push({ type: "remote", url: currentUrls[i], route: [i / urls.length, (i + 1) / urls.length], });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return sources;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
47
62
|
let sources = createSourceWindows(overrides, [
|
|
48
63
|
{
|
|
49
|
-
startTime: +new Date("2026-07-20
|
|
50
|
-
sources: [
|
|
51
|
-
{ type: "remote", url: ONTARIO, route: [0.5, 1], },
|
|
52
|
-
{ type: "remote", url: HETZNER, route: [0, 0.5], },
|
|
53
|
-
{ type: "remote", url: ONTARIO, },
|
|
54
|
-
{ type: "remote", url: HETZNER, },
|
|
55
|
-
{ type: "backblaze", url: BACKBLAZE, },
|
|
56
|
-
]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
startTime: +new Date("2026-07-21 02:59:00-04:00"),
|
|
64
|
+
startTime: +new Date("2026-07-20 00:00:00-04:00"),
|
|
60
65
|
sources: [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{ type: "remote", url: HETZNER, },
|
|
66
|
+
// NOTE: Once we add new sources, we need to add something at a new start time in the future so we have time to move the files.
|
|
67
|
+
// TODO: EVENTUALLY, Once we have enough data, instead of restriping, we need to bisect the data. That way we can slowly move over the data instead of trying to rewrite all of our data all at once. However, for the moment, moving all the data is probably fine. On the data center, we should be able to move terabytes of data every hour, so we should be able to handle quite a bit, especially if we have a few days to run the restriping.
|
|
68
|
+
...stripeSources([ONTARIO, HETZNER]),
|
|
65
69
|
{ type: "backblaze", url: BACKBLAZE, },
|
|
66
70
|
]
|
|
67
71
|
},
|
|
68
72
|
]);
|
|
69
73
|
|
|
70
74
|
return createArchives({
|
|
71
|
-
version:
|
|
75
|
+
version: 18,
|
|
72
76
|
sources,
|
|
73
77
|
});
|
|
74
78
|
}
|
|
@@ -89,8 +93,8 @@ function nestBucket(folder: string, archives: ReturnType<typeof createArchives>)
|
|
|
89
93
|
get: (path: string, config?: GetConfig) => archives.get(folder + path, config),
|
|
90
94
|
set: (path: string, data: Buffer, config?: SetConfig) => archives.set(folder + path, data, config),
|
|
91
95
|
del: (path: string) => archives.del(folder + path),
|
|
92
|
-
find: async (prefix: string, config?:
|
|
93
|
-
findInfo: async (prefix: string, config?:
|
|
96
|
+
find: async (prefix: string, config?: FindConfig) => (await archives.find(folder + prefix, config)).map(x => x.slice(folder.length)),
|
|
97
|
+
findInfo: async (prefix: string, config?: FindConfig) => (await archives.findInfo(folder + prefix, config)).map(x => ({ ...x, path: x.path.slice(folder.length) })),
|
|
94
98
|
get2: (path: string, config?: GetConfig) => archives.get2(folder + path, config),
|
|
95
99
|
setLargeFile: (config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined>; }) => archives.setLargeFile({ ...config, path: folder + config.path }),
|
|
96
100
|
getInfo: (path: string) => archives.getInfo(folder + path),
|
|
@@ -87,7 +87,7 @@ export const isTrusted = measureWrap(async function isTrusted(machineId: string)
|
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
let populateTrustedCache = lazy(async () => {
|
|
90
|
-
let trustedMachineIds = await archives().find("");
|
|
90
|
+
let trustedMachineIds = await archives().find("", { fallbacks: true });
|
|
91
91
|
lastArchivesTrusted = trustedMachineIds.slice();
|
|
92
92
|
for (let trustedMachineId of trustedMachineIds) {
|
|
93
93
|
trustedCache.add(trustedMachineId);
|
|
@@ -342,7 +342,7 @@ async function syncArchives() {
|
|
|
342
342
|
if (isServer()) {
|
|
343
343
|
// Make sure we are present
|
|
344
344
|
await writeHeartbeat();
|
|
345
|
-
let nodeIds = await archives().find("");
|
|
345
|
+
let nodeIds = await archives().find("", { fallbacks: true });
|
|
346
346
|
console.info(`Synced node ids from archives`, { nodeIds });
|
|
347
347
|
await setNodeIds(nodeIds);
|
|
348
348
|
} else {
|
|
@@ -13,6 +13,7 @@ import { sha256 } from "js-sha256";
|
|
|
13
13
|
import { rangesOverlap, removeRange } from "../rangeMath";
|
|
14
14
|
import { decodeParentFilter } from "./hackedPackedPathParentFiltering";
|
|
15
15
|
import { getBufferInt } from "socket-function/src/bits";
|
|
16
|
+
import { safeLoop } from "socket-function/src/batching";
|
|
16
17
|
|
|
17
18
|
import { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH } from "./PathRouterConstants";
|
|
18
19
|
export { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH };
|
|
@@ -320,7 +321,7 @@ export class PathRouter {
|
|
|
320
321
|
// NOTE: Encodes all the data, even if it matches our spec or not. If you don't want this, you have to filter first
|
|
321
322
|
// - If this becomes an issue we COULD filter, as we can do it quickly, but I don't think it is required, as all the present usecases prefilter anyways.
|
|
322
323
|
@measureFnc
|
|
323
|
-
public static getPathIdentifierTargets(values: PathValue[], ourSpec: AuthoritySpec): Map<string, PathValue[]
|
|
324
|
+
public static async getPathIdentifierTargets(values: PathValue[], ourSpec: AuthoritySpec): Promise<Map<string, PathValue[]>> {
|
|
324
325
|
// NOTE: The file size limit is 1024 bytes. But we also have our folder, etc, so we want to add enough buffer
|
|
325
326
|
// - Shorter hashes means we can store more, but there's a point when the collisions make it less useful.
|
|
326
327
|
const MAX_PREFIXES_PER_FILE = 50;
|
|
@@ -333,17 +334,17 @@ export class PathRouter {
|
|
|
333
334
|
let prefixes = ourSpec.prefixes.slice();
|
|
334
335
|
sort(prefixes, x => x.prefixPartLength);
|
|
335
336
|
|
|
336
|
-
// NOTE: If there are few enough path values for a prefix, we don't even need to calculate the routing hash.
|
|
337
|
+
// NOTE: If there are few enough path values for a prefix, we don't even need to calculate the routing hash.
|
|
337
338
|
let byPrefix = new Map<PrefixMatcher | undefined, PathValue[]>();
|
|
338
|
-
|
|
339
|
+
await safeLoop({ data: values, name: "PathRouter.getPathIdentifierTargets|groupByPrefix" }, value => {
|
|
339
340
|
let prefix = getMatchingPrefix(ourSpec, value.path);
|
|
340
|
-
let
|
|
341
|
-
if (!
|
|
342
|
-
|
|
343
|
-
byPrefix.set(prefix,
|
|
341
|
+
let prefixValues = byPrefix.get(prefix);
|
|
342
|
+
if (!prefixValues) {
|
|
343
|
+
prefixValues = [];
|
|
344
|
+
byPrefix.set(prefix, prefixValues);
|
|
344
345
|
}
|
|
345
|
-
|
|
346
|
-
}
|
|
346
|
+
prefixValues.push(value);
|
|
347
|
+
});
|
|
347
348
|
|
|
348
349
|
let prefixGroups = Array.from(byPrefix.entries()).map(([prefix, values]) => ({
|
|
349
350
|
prefix,
|
|
@@ -391,17 +392,17 @@ export class PathRouter {
|
|
|
391
392
|
}
|
|
392
393
|
|
|
393
394
|
let finalFiles = new Map<string, PathValue[]>();
|
|
394
|
-
|
|
395
|
+
await safeLoop({ data: groups, name: "PathRouter.getPathIdentifierTargets|shardGroups" }, async group => {
|
|
395
396
|
if (group.count < SHARD_THRESHOLD) {
|
|
396
397
|
let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart: 0, rangeEnd: 1 });
|
|
397
398
|
finalFiles.set(identifier, group.values.flat());
|
|
398
|
-
|
|
399
|
+
return;
|
|
399
400
|
}
|
|
400
401
|
// Split by routing hash
|
|
401
|
-
let
|
|
402
|
-
let splitCount = Math.max(MIN_SHARD_FILE_COUNT, Math.ceil(
|
|
402
|
+
let groupValues = group.values.flat();
|
|
403
|
+
let splitCount = Math.max(MIN_SHARD_FILE_COUNT, Math.ceil(groupValues.length / TARGET_SHARD_SIZE));
|
|
403
404
|
let byRouteGroup = new Map<number, PathValue[]>();
|
|
404
|
-
|
|
405
|
+
await safeLoop({ data: groupValues, name: "PathRouter.getPathIdentifierTargets|routeValues" }, value => {
|
|
405
406
|
let route = this.getRouteFull({
|
|
406
407
|
path: value.path,
|
|
407
408
|
spec: {
|
|
@@ -418,14 +419,14 @@ export class PathRouter {
|
|
|
418
419
|
byRouteGroup.set(routeIndex, routeValues);
|
|
419
420
|
}
|
|
420
421
|
routeValues.push(value);
|
|
421
|
-
}
|
|
422
|
+
});
|
|
422
423
|
for (let [routeIndex, routeValues] of byRouteGroup) {
|
|
423
424
|
let rangeStart = routeIndex / splitCount;
|
|
424
425
|
let rangeEnd = (routeIndex + 1) / splitCount;
|
|
425
426
|
let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart, rangeEnd });
|
|
426
427
|
finalFiles.set(identifier, routeValues);
|
|
427
428
|
}
|
|
428
|
-
}
|
|
429
|
+
});
|
|
429
430
|
|
|
430
431
|
// NOTE: There could be a huge number of prefixes and we can't pack them all into one file because of the prefix limit, so this will write any remaining values.
|
|
431
432
|
if (remainingValues.length > 0) {
|
|
@@ -5,7 +5,6 @@ import { getOwnNodeId, getOwnNodeIdAssert } from "../../-f-node-discovery/NodeDi
|
|
|
5
5
|
import { pathValueArchives } from "../pathValueArchives";
|
|
6
6
|
import { ArchiveLocker, ArchiveTransaction } from "./ArchiveLocks";
|
|
7
7
|
import { copyArchiveFile, IArchives } from "sliftutils/storage/IArchives";
|
|
8
|
-
import debugbreak from "debugbreak";
|
|
9
8
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
10
9
|
import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
|
|
11
10
|
import { devDebugbreak, getDomain } from "../../config";
|
|
@@ -15,7 +14,7 @@ import { getNodeId } from "socket-function/src/nodeCache";
|
|
|
15
14
|
import { logNodeStateStats, logNodeStats } from "../../-0-hooks/hooks";
|
|
16
15
|
import { parsePath, toFileNameKVP, parseFileNameKVP } from "../../misc";
|
|
17
16
|
import { logDisk } from "../../diagnostics/logs/diskLogger";
|
|
18
|
-
import { retryFunctional, runInParallel } from "socket-function/src/batching";
|
|
17
|
+
import { delay, retryFunctional, runInParallel } from "socket-function/src/batching";
|
|
19
18
|
|
|
20
19
|
/*
|
|
21
20
|
Data starts a .data files, from servers creating files.
|
|
@@ -30,13 +29,16 @@ const ARCHIVE_PROPAGATION_TIME = timeInSecond * 5;
|
|
|
30
29
|
// a few times though, just in case the storage system was having some issues. After enough
|
|
31
30
|
// tries though... we HAVE to just skip it, otherwise we will be stuck forever.
|
|
32
31
|
const MAX_APPLY_TRIES = 10;
|
|
33
|
-
|
|
34
|
-
const
|
|
32
|
+
// Confirms used to be zero-byte files, which the storage layer now treats as deletes, wiping every historical confirmation. Locked files created before this date are assumed confirmed, as anything invalid that old would have already been cleaned up.
|
|
33
|
+
const ASSUME_CONFIRMED_BEFORE = Date.parse("2026-07-23T00:00:00.000Z");
|
|
34
|
+
// NOTE: These values have been increased as our storage servers can handle way more throughput than before because it's using WebSockets.
|
|
35
|
+
const CONCURRENT_READ_COUNT = 128;
|
|
36
|
+
const CONCURRENT_WRITE_COUNT = 128;
|
|
35
37
|
const CONCURRENT_TRANSACTION_READS = 8;
|
|
36
38
|
|
|
37
39
|
/** Moves by copy-then-delete (IArchives has no move). copyArchiveFile stamps the target with the source's write time, so ordering-by-writeTime survives the move. A missing source means another process already moved it, which is fine. */
|
|
38
40
|
async function moveArchiveFile(from: IArchives, to: IArchives, path: string): Promise<void> {
|
|
39
|
-
let copied = await copyArchiveFile({ from, to, path });
|
|
41
|
+
let copied = await copyArchiveFile({ from, to, path, noFallbacks: true });
|
|
40
42
|
if (!copied) return;
|
|
41
43
|
await from.del(path);
|
|
42
44
|
}
|
|
@@ -61,7 +63,8 @@ export function createArchiveLocker2(config: {
|
|
|
61
63
|
async getKeys() {
|
|
62
64
|
let filesRawValues = await archiveValues.findInfo("", { type: "files" });
|
|
63
65
|
let filesRawLocks = await archiveLocks.findInfo("", { type: "files" });
|
|
64
|
-
|
|
66
|
+
// Zero-byte files are deletes on new storage servers. Old storage servers still list them, so we filter here to make them behave the same.
|
|
67
|
+
let files: FileInfo[] = [...filesRawValues, ...filesRawLocks].filter(x => x.size > 0).map(x => ({
|
|
65
68
|
file: x.path,
|
|
66
69
|
createTime: x.createTime,
|
|
67
70
|
size: x.size,
|
|
@@ -74,16 +77,11 @@ export function createArchiveLocker2(config: {
|
|
|
74
77
|
logNodeStats(`archives|Created TΔ`, formatNumber, 1);
|
|
75
78
|
},
|
|
76
79
|
async getValue(key) {
|
|
77
|
-
|
|
80
|
+
// NOTE: I think we wanna not use fallbacks, Because we really want this to be atomic and safe.
|
|
81
|
+
return getArchives(key).get(key, { noFallbacks: true });
|
|
78
82
|
},
|
|
79
83
|
async deleteKey(key) {
|
|
80
84
|
let archives = await getArchives(key);
|
|
81
|
-
if (
|
|
82
|
-
!await archives.getInfo(key)
|
|
83
|
-
&& !await archives.get(key)
|
|
84
|
-
) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
85
|
// ACTUALLY delete confirms, as they don't contain that much information, and just clutter up the recycle bin.
|
|
88
86
|
// - DO archive transactions, as it might be useful to view the old transaction history.
|
|
89
87
|
if (archives === archiveLocks && key.endsWith(".confirm")) {
|
|
@@ -217,7 +215,8 @@ export function createArchiveLocker2(config: {
|
|
|
217
215
|
while (pendingFiles.length > 0) {
|
|
218
216
|
let file = pendingFiles.pop()!;
|
|
219
217
|
let value = await storage.getValue(file.file);
|
|
220
|
-
|
|
218
|
+
// NOTE: The way we've updated our archives is it's going to return a result unless it throws. And it shouldn't throw very often. The only reason it would return undefined is if the file was empty. After I do the deploy, get keys won't return empty files, but I need to fix this to do the deploy. So we're just going to default any empty returns to have a value. So we stop saying that files are missing and rereading infinitely.
|
|
219
|
+
readResults.set(file.file, value ?? Buffer.alloc(0));
|
|
221
220
|
}
|
|
222
221
|
}
|
|
223
222
|
await Promise.all(list(CONCURRENT_READ_COUNT).map(runReadThread));
|
|
@@ -420,6 +419,19 @@ class TransactionLocker {
|
|
|
420
419
|
}
|
|
421
420
|
};
|
|
422
421
|
await Promise.all(list(CONCURRENT_WRITE_COUNT).map(runThread));
|
|
422
|
+
|
|
423
|
+
// Verify the files still exist. There shouldn't be any valid case where they disappear. If they do disappear, it's our storage's fault.
|
|
424
|
+
let keys = await this.storage.getKeys();
|
|
425
|
+
let keysSet = new Set(keys.map(a => a.file));
|
|
426
|
+
let creates = transaction.ops.filter(a => a.type === "create") as { type: "create"; key: string; value: Buffer | undefined }[];
|
|
427
|
+
let missingCreates = creates.filter(a => !keysSet.has(a.key));
|
|
428
|
+
if (missingCreates.length > 0) {
|
|
429
|
+
console.error(`We are missing files that we just wrote to storage. This shouldn't be possible because the files can't be cleaned up until they get old enough, so this means our storage is broken`, {
|
|
430
|
+
missingCount: missingCreates.length,
|
|
431
|
+
firstMissing: missingCreates[0]?.key,
|
|
432
|
+
firstMissingSize: missingCreates[0]?.value?.length,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
423
435
|
}
|
|
424
436
|
// Just writes the transaction (in a format readDataState can read)
|
|
425
437
|
private async writeTransaction(transaction: Transaction & { seqNum: number; }): Promise<void> {
|
|
@@ -592,7 +604,7 @@ class TransactionLocker {
|
|
|
592
604
|
if (!file.file.endsWith(".locked")) continue;
|
|
593
605
|
let confirmKey = this.getConfirmKey(file.file);
|
|
594
606
|
let confirmFile = existingFiles.get(confirmKey);
|
|
595
|
-
if (confirmFile) {
|
|
607
|
+
if (confirmFile || file.createTime < ASSUME_CONFIRMED_BEFORE) {
|
|
596
608
|
currentDataFiles.set(file.file, file);
|
|
597
609
|
} else {
|
|
598
610
|
uncomfirmedCount++;
|
|
@@ -627,25 +639,67 @@ class TransactionLocker {
|
|
|
627
639
|
}
|
|
628
640
|
// #endregion
|
|
629
641
|
|
|
630
|
-
private isTransactionValid(transaction: Transaction, dataFiles: FileInfo[], rawDataFiles: FileInfo[]): boolean {
|
|
642
|
+
private async isTransactionValid(transaction: Transaction, dataFiles: FileInfo[], rawDataFiles: FileInfo[]): Promise<boolean> {
|
|
631
643
|
let override = this.isTransactionValidOverride;
|
|
632
644
|
if (override) return override(transaction, dataFiles, rawDataFiles);
|
|
633
645
|
if (transaction.lockedFilesMustEqual) {
|
|
634
646
|
let newLockedFiles = new Set(dataFiles.map(a => a.file).filter(a => a.endsWith(".locked")));
|
|
635
647
|
let mustEqualSet = new Set(transaction.lockedFilesMustEqual);
|
|
636
|
-
|
|
637
|
-
|
|
648
|
+
let missingFiles = Array.from(mustEqualSet).filter(a => !newLockedFiles.has(a));
|
|
649
|
+
let extraFiles = Array.from(newLockedFiles).filter(a => !mustEqualSet.has(a));
|
|
650
|
+
let missingFile = missingFiles[0];
|
|
651
|
+
if (missingFile) {
|
|
652
|
+
console.error(`Missing file, cannot apply transaction`, {
|
|
653
|
+
missing: missingFile,
|
|
654
|
+
totalMissing: missingFiles.length,
|
|
655
|
+
totalExtra: extraFiles.length,
|
|
656
|
+
});
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
let extraFile = extraFiles[0];
|
|
660
|
+
if (extraFile) {
|
|
661
|
+
let extra = await this.storage.getValue(extraFile);
|
|
662
|
+
console.error(`Extra file, cannot apply transaction`, {
|
|
663
|
+
extra: extraFile,
|
|
664
|
+
extraLength: extra?.length,
|
|
665
|
+
extraFirst1024: extra?.slice(0, 1024).toString("hex"),
|
|
666
|
+
totalMissing: missingFiles.length,
|
|
667
|
+
totalExtra: extraFiles.length,
|
|
668
|
+
});
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
if (mustEqualSet.size !== newLockedFiles.size) {
|
|
672
|
+
console.error(`Transaction not applied (locked files must equal but size mismatch)`, {
|
|
673
|
+
mustEqualSetSize: mustEqualSet.size,
|
|
674
|
+
newLockedFilesSize: newLockedFiles.size,
|
|
675
|
+
});
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
638
678
|
}
|
|
639
679
|
let existingFiles = new Set(dataFiles.map(a => a.file));
|
|
640
680
|
let existingRawFiles = new Set(rawDataFiles.map(a => a.file));
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
681
|
+
let missingCreates = transaction.ops.filter(a => a.type === "create" && !existingRawFiles.has(a.key)) as { type: "create"; key: string; value: Buffer | undefined }[];
|
|
682
|
+
let missingDeletes = transaction.ops.filter(a => a.type === "delete" && !existingFiles.has(a.key)) as { type: "delete"; key: string }[];
|
|
683
|
+
if (missingCreates.length > 0 || missingDeletes.length > 0) {
|
|
684
|
+
console.error(`Transaction not applied (missing creates or deletes)`, {
|
|
685
|
+
missingCreateFirst: missingCreates[0]?.key,
|
|
686
|
+
missingDeleteFirst: missingDeletes[0]?.key,
|
|
687
|
+
missingCreateFirstSize: missingCreates[0]?.value?.length,
|
|
688
|
+
totalMissingCreates: missingCreates.length,
|
|
689
|
+
totalMissingDeletes: missingDeletes.length,
|
|
690
|
+
});
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
for (let op of transaction.ops) {
|
|
694
|
+
const type = op.type;
|
|
695
|
+
if (type === "create") {
|
|
696
|
+
} else if (type === "delete") {
|
|
697
|
+
} else {
|
|
698
|
+
let unhandled: never = type;
|
|
699
|
+
throw new Error(`Unhandled op type: ${unhandled}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return true;
|
|
649
703
|
}
|
|
650
704
|
|
|
651
705
|
// NOTE: If the transaction was applied long in the past, this will return false. HOWEVER, this is fine,
|
|
@@ -681,7 +735,7 @@ class TransactionLocker {
|
|
|
681
735
|
let createCount = transaction.ops.filter(a => a.type === "create").length;
|
|
682
736
|
let deleteCount = transaction.ops.filter(a => a.type === "delete").length;
|
|
683
737
|
let lockedFiles = transaction.lockedFilesMustEqual?.length;
|
|
684
|
-
console.
|
|
738
|
+
console.log(`Applying transaction with ${createCount} file creates and ${deleteCount} file deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
|
|
685
739
|
transactions: transaction.ops.map(x => JSON.stringify(x)),
|
|
686
740
|
});
|
|
687
741
|
logNodeStats(`archives|TΔ Apply`, formatNumber, 1);
|
|
@@ -707,9 +761,29 @@ class TransactionLocker {
|
|
|
707
761
|
};
|
|
708
762
|
await Promise.all(list(CONCURRENT_WRITE_COUNT).map(runThread));
|
|
709
763
|
|
|
710
|
-
console.
|
|
764
|
+
console.log(`Applied transaction with ${createCount} file creates and file ${deleteCount} deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
|
|
711
765
|
transactions: transaction.ops.map(x => JSON.stringify(x)),
|
|
712
766
|
});
|
|
767
|
+
|
|
768
|
+
let newKeys = await this.storage.getKeys();
|
|
769
|
+
let newKeysSet = new Set(newKeys.map(a => a.file));
|
|
770
|
+
let createFiles = transaction.ops.filter(a => a.type === "create") as { type: "create"; key: string; value: Buffer | undefined }[];
|
|
771
|
+
let deleteFiles = transaction.ops.filter(a => a.type === "delete") as { type: "delete"; key: string }[];
|
|
772
|
+
let deleteSet = new Set(deleteFiles.map(a => a.key));
|
|
773
|
+
|
|
774
|
+
let missingCreates = createFiles.filter(a => !newKeysSet.has(a.key));
|
|
775
|
+
let extraFiles = newKeys.filter(a => deleteSet.has(a.file));
|
|
776
|
+
|
|
777
|
+
if (missingCreates.length > 0 || extraFiles.length > 0) {
|
|
778
|
+
console.error(`The storage system is broken and did not actually apply our transaction changes (transaction ${transaction.source.file})`, {
|
|
779
|
+
missingCreateFirst: missingCreates[0]?.key,
|
|
780
|
+
missingDeleteFirst: extraFiles[0]?.file,
|
|
781
|
+
missingCreateFirstSize: missingCreates[0]?.value?.length,
|
|
782
|
+
missingDeleteFirstSize: extraFiles[0]?.size,
|
|
783
|
+
totalMissingCreates: missingCreates.length,
|
|
784
|
+
totalExtraFiles: extraFiles.length,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
713
787
|
}
|
|
714
788
|
|
|
715
789
|
/** Only returns data files (no transaction files, or confirmations).
|
|
@@ -871,6 +945,13 @@ class TransactionLocker {
|
|
|
871
945
|
this.perf.checkpoint("apply transaction");
|
|
872
946
|
this.transactionAppliedCount.set(activeT.seqNum, applyCount + 1);
|
|
873
947
|
|
|
948
|
+
{
|
|
949
|
+
let dataState = await this.readDataState();
|
|
950
|
+
if (!this.wasTransactionApplied(activeT, dataState.confirmedDataFiles, dataState.rawDataFiles)) {
|
|
951
|
+
console.error(`Transaction ${activeT.seqNum} was not applied, even though we just applied it.`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
874
955
|
// Run again, until we can be reasonable sure activeT isn't changing. We can be wrong,
|
|
875
956
|
// though, which will just result in any transaction depending on activeT being
|
|
876
957
|
// rejecting, and having to be inserted again.
|
|
@@ -882,7 +963,7 @@ class TransactionLocker {
|
|
|
882
963
|
let dels = transaction.ops.filter(a => a.type === "delete").length;
|
|
883
964
|
let creates = transaction.ops.filter(a => a.type === "create").length;
|
|
884
965
|
let createBytes = transaction.ops.map(a => a.type === "create" && a.value?.length || 0).reduce((a, b) => a + b, 0);
|
|
885
|
-
console.
|
|
966
|
+
console.log(`Starting transaction with ${creates} file creates and ${dels} file deletes, ${formatNumber(createBytes)}B`, {
|
|
886
967
|
createFilesNames: transaction.ops.filter(a => a.type === "create").map(a => a.key),
|
|
887
968
|
deleteFilesNames: transaction.ops.filter(a => a.type === "delete").map(a => a.key),
|
|
888
969
|
});
|
|
@@ -909,9 +990,12 @@ class TransactionLocker {
|
|
|
909
990
|
|
|
910
991
|
while (true) {
|
|
911
992
|
let beforeData = await this.getFilesBase();
|
|
912
|
-
if (!this.isTransactionValid(transaction, beforeData.dataFiles, beforeData.rawDataFiles)) {
|
|
993
|
+
if (!await this.isTransactionValid(transaction, beforeData.dataFiles, beforeData.rawDataFiles)) {
|
|
913
994
|
logNodeStats(`archives|TΔ Rejected`, formatNumber, 1);
|
|
914
|
-
console.
|
|
995
|
+
console.log(`Finished transaction with rejection, ${transaction.ops.length} ops`);
|
|
996
|
+
await delay(30 * 1000);
|
|
997
|
+
let valid2 = await this.isTransactionValid(transaction, beforeData.dataFiles, beforeData.rawDataFiles);
|
|
998
|
+
console.log(`Valid2: ${valid2}`);
|
|
915
999
|
return "rejected";
|
|
916
1000
|
}
|
|
917
1001
|
|
|
@@ -920,7 +1004,7 @@ class TransactionLocker {
|
|
|
920
1004
|
let afterData = await this.getFilesBase();
|
|
921
1005
|
if (this.wasTransactionApplied(transaction, afterData.dataFiles, afterData.rawDataFiles)) {
|
|
922
1006
|
logNodeStats(`archives|TΔ Accepted`, formatNumber, 1);
|
|
923
|
-
console.
|
|
1007
|
+
console.log(`Finished transaction with ${transaction.ops.length} ops`);
|
|
924
1008
|
return "accepted";
|
|
925
1009
|
}
|
|
926
1010
|
}
|
|
@@ -181,7 +181,7 @@ export class PathValueArchives {
|
|
|
181
181
|
|
|
182
182
|
@measureFnc
|
|
183
183
|
public async archiveValues(values: PathValue[]) {
|
|
184
|
-
let parts = PathRouter.getPathIdentifierTargets(values, authorityLookup.getOurSpec());
|
|
184
|
+
let parts = await PathRouter.getPathIdentifierTargets(values, authorityLookup.getOurSpec());
|
|
185
185
|
|
|
186
186
|
for (let [pathIdentifier, values] of parts) {
|
|
187
187
|
let encodedObj = await this.encodeValuePaths(values);
|
|
@@ -316,8 +316,9 @@ export class PathValueArchives {
|
|
|
316
316
|
let dataPath = pendingDataPaths.pop()!;
|
|
317
317
|
if (readCache.has(dataPath)) continue;
|
|
318
318
|
let data = await archives().get(dataPath);
|
|
319
|
-
if
|
|
320
|
-
|
|
319
|
+
// NOTE: The way we've updated our archives is it's going to return a result unless it throws. And it shouldn't throw very often. The only reason it would return undefined is if the file was empty. After I do the deploy, get keys won't return empty files, but I need to fix this to do the deploy. So we're just going to default any empty returns to have a value. So we stop saying that files are missing and rereading infinitely.
|
|
320
|
+
//if (!data) continue;
|
|
321
|
+
readCache.set(dataPath, data || Buffer.alloc(0));
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
await Promise.all(list(32).map(runReadThread));
|
|
@@ -359,6 +360,7 @@ export class PathValueArchives {
|
|
|
359
360
|
|
|
360
361
|
await safeLoop({ data: dataPaths, name: blue("Load PathValues From Buffer") }, async dataFile => {
|
|
361
362
|
let data = readCache.get(dataFile);
|
|
363
|
+
if (!data?.length) return;
|
|
362
364
|
if (!data) throw new Error(`Data file ${dataFile} not in lookup which we just checked?`);
|
|
363
365
|
totalSize += data.byteLength;
|
|
364
366
|
fileCount++;
|
|
@@ -410,6 +412,7 @@ export class PathValueArchives {
|
|
|
410
412
|
tooOldValues: number;
|
|
411
413
|
}> {
|
|
412
414
|
const { path, data } = config;
|
|
415
|
+
if (data.length === 0) return { values: [], rawCount: 0, tooOldValues: 0 };
|
|
413
416
|
let tooOldValues = 0;
|
|
414
417
|
|
|
415
418
|
let dataValues: PathValue[] = [];
|
|
@@ -423,10 +426,10 @@ export class PathValueArchives {
|
|
|
423
426
|
skipValues: config.skipValues,
|
|
424
427
|
});
|
|
425
428
|
if (dataValues.length !== decodedObj.valueCount) {
|
|
426
|
-
console.warn(`Bad archive data file at ${config.path}, Decoded count ${formatNumber(decodedObj.valueCount)} !== count in file name ${formatNumber(dataValues.length)} (${decodedObj.valueCount} !== ${dataValues.length})`);
|
|
429
|
+
console.warn(`Bad archive data file (size ${data.length}) at ${config.path}, Decoded count ${formatNumber(decodedObj.valueCount)} !== count in file name ${formatNumber(dataValues.length)} (${decodedObj.valueCount} !== ${dataValues.length})`);
|
|
427
430
|
}
|
|
428
431
|
} catch (e: any) {
|
|
429
|
-
console.warn(`Bad archive data file at ${config.path}, error: ${e.stack}`);
|
|
432
|
+
console.warn(`Bad archive data file (size ${data.length}) at ${config.path}, error: ${e.stack}`);
|
|
430
433
|
}
|
|
431
434
|
let rawCount = dataValues.length;
|
|
432
435
|
|
|
@@ -250,7 +250,7 @@ export async function runArchiveMover(config: {
|
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
for (let [key, values] of Object.entries(result.newValues)) {
|
|
253
|
-
let targets = PathRouter.getPathIdentifierTargets(values, authority);
|
|
253
|
+
let targets = await PathRouter.getPathIdentifierTargets(values, authority);
|
|
254
254
|
for (let [target, values] of targets) {
|
|
255
255
|
await addValues(target + "/", values, key);
|
|
256
256
|
}
|
|
@@ -99,7 +99,7 @@ async function runGenesisJoinIteration(config?: { force?: boolean }) {
|
|
|
99
99
|
transaction.deleteFiles.push(file);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
let targets = PathRouter.getPathIdentifierTargets(allCombinedValues, authoritySpec);
|
|
102
|
+
let targets = await PathRouter.getPathIdentifierTargets(allCombinedValues, authoritySpec);
|
|
103
103
|
for (let [target, values] of targets) {
|
|
104
104
|
|
|
105
105
|
let dataObj = await pathValueArchives.encodeValuePaths(values, {
|
|
@@ -6,7 +6,7 @@ import { sort, timeInSecond } from "socket-function/src/misc";
|
|
|
6
6
|
import preact from "preact";
|
|
7
7
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
8
8
|
import { listServerBuckets, clearServerWriteStats, activateServerBucket, getServerActiveBucket } from "sliftutils/storage/remoteStorage/createArchives";
|
|
9
|
-
import type { ServerBucketInfo,
|
|
9
|
+
import type { ServerBucketInfo, BucketWriteStats, ActiveBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
|
|
10
10
|
import type { ArchivesConfig, SyncActivity, RemoteConfig, HostedConfig } from "sliftutils/storage/IArchives";
|
|
11
11
|
import { parseHostedUrl } from "sliftutils/storage/remoteStorage/remoteConfig";
|
|
12
12
|
import { UsageBar, getUsageThresholds } from "../../library-components/UsageBar";
|
|
@@ -19,6 +19,7 @@ import { Table } from "../../5-diagnostics/Table";
|
|
|
19
19
|
import { RouteConfigView } from "./RouteConfigView";
|
|
20
20
|
import { Tag, SourceTag, getSourceTags } from "./Tag";
|
|
21
21
|
import { STORAGE_COMMAND_PREFIX } from "../serviceCategories";
|
|
22
|
+
import { BucketDiskInfo } from "sliftutils/storage/remoteStorage/bucketDisk";
|
|
22
23
|
|
|
23
24
|
module.hotreload = true;
|
|
24
25
|
|
|
@@ -48,7 +48,7 @@ let pendingDiscordNotifications = new Map<string, {
|
|
|
48
48
|
errorCount: number;
|
|
49
49
|
isNew: boolean;
|
|
50
50
|
}>();
|
|
51
|
-
async function getSuppressionEntries(): Promise<SuppressionEntry[]> {
|
|
51
|
+
export async function getSuppressionEntries(): Promise<SuppressionEntry[]> {
|
|
52
52
|
return await suppression.values();
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -295,14 +295,12 @@ export function getSyncedController<T extends {
|
|
|
295
295
|
});
|
|
296
296
|
// We have to wait until we actually commit before making the call. Otherwise, if this commit is rejected because we need to do synchronized values, we'll call the function twice.
|
|
297
297
|
let fnc = controller.nodes[nodeId][fncName] as any;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
//if (Querysub.isAllSynced()) {
|
|
298
|
+
// NOTE: We can't do on commit finish because we later have a trigger on promise finish for our own promise. And if we don't start the promise call until we finish the commit, And we can't finish the commit until we start the promise call, it'll never finish. So... The best we can do is just check for if everything's synced at this current point.
|
|
299
|
+
if (Querysub.isAllSynced()) {
|
|
301
300
|
void Promise.resolve().then(() => {
|
|
302
301
|
doPromiseCall();
|
|
303
302
|
});
|
|
304
|
-
|
|
305
|
-
});
|
|
303
|
+
}
|
|
306
304
|
function doPromiseCall() {
|
|
307
305
|
let promise = fnc(...args) as Promise<unknown>;
|
|
308
306
|
promiseObjBase.resolve(promise);
|
package/src/storageSetup.ts
CHANGED
|
@@ -15,7 +15,7 @@ async function main() {
|
|
|
15
15
|
|
|
16
16
|
let trustedMachineIds = await getTrustedMachineIds();
|
|
17
17
|
|
|
18
|
-
let { setTrustedMachines } = await import("sliftutils/storage/remoteStorage/
|
|
18
|
+
let { setTrustedMachines } = await import("sliftutils/storage/remoteStorage/serverConfig");
|
|
19
19
|
await setTrustedMachines({ account: "root", machineIds: trustedMachineIds });
|
|
20
20
|
console.log(`Synchronized ${trustedMachineIds.length} trusted machines`);
|
|
21
21
|
|