sliftutils 1.7.89 → 1.7.91
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/index.d.ts +36 -81
- package/misc/https/dist/cloudflareHelpers.ts.cache +2 -2
- package/misc/https/dist/dns.ts.cache +2 -2
- package/package.json +1 -1
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
- package/storage/dist/ArchivesDisk.ts.cache +2 -2
- package/storage/dist/IArchives.ts.cache +2 -2
- package/storage/dist/archiveHelpers.ts.cache +87 -0
- package/storage/dist/backblaze.ts.cache +2 -2
- package/storage/remoteStorage/accessStats.d.ts +12 -0
- package/storage/remoteStorage/accessStats.ts +47 -0
- package/storage/remoteStorage/blobStore.d.ts +3 -1
- package/storage/remoteStorage/blobStore.ts +5 -0
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +2 -2
- package/storage/remoteStorage/dist/accessStats.ts.cache +42 -3
- package/storage/remoteStorage/dist/blobStore.ts.cache +7 -4
- package/storage/remoteStorage/dist/createArchives.ts.cache +2 -2
- package/storage/remoteStorage/dist/intermediateManagement.ts.cache +56 -50
- package/storage/remoteStorage/dist/storageController.ts.cache +47 -58
- package/storage/remoteStorage/dist/storageServerState.ts.cache +119 -248
- package/storage/remoteStorage/dist/storeConfig.ts.cache +2 -2
- package/storage/remoteStorage/dist/storePlan.ts.cache +4 -56
- package/storage/remoteStorage/dist/storeSync.ts.cache +6 -5
- package/storage/remoteStorage/intermediateManagement.d.ts +2 -3
- package/storage/remoteStorage/intermediateManagement.ts +53 -46
- package/storage/remoteStorage/storageController.ts +35 -44
- package/storage/remoteStorage/storageServerState.d.ts +17 -57
- package/storage/remoteStorage/storageServerState.ts +112 -279
- package/storage/remoteStorage/storeConfig.d.ts +2 -0
- package/storage/remoteStorage/storeConfig.ts +2 -0
- package/storage/remoteStorage/storePlan.d.ts +0 -20
- package/storage/remoteStorage/storePlan.ts +1 -69
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { lazy } from "socket-function/src/caching";
|
|
2
2
|
import { runInfinitePoll } from "socket-function/src/batching";
|
|
3
3
|
import { RemoteConfig, SourceConfig, HostedConfig } from "../IArchives";
|
|
4
|
-
import { ROUTING_FILE, getConfigVersion, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort } from "./remoteConfig";
|
|
4
|
+
import { ROUTING_FILE, getConfigVersion, serializeRemoteConfig, parseRoutingData, parseHostedUrl, replaceHostedUrlPort } from "./remoteConfig";
|
|
5
5
|
import { injectIntermediateSource, expireIntermediateSources, getIntermediateSources, nextIntermediateVersion, INTERMEDIATE_EXPIRE_GRACE } from "./intermediateSources";
|
|
6
6
|
import { getTakeoverIntermediate } from "./deployTakeover";
|
|
7
7
|
import { getStorageServerConfig } from "./serverConfig";
|
|
8
8
|
import { createApiArchives, listServerActiveBucketKeys } from "./createArchives";
|
|
9
9
|
import { findSelfIndexes, previousWindowOwners } from "./storePlan";
|
|
10
|
-
import {
|
|
10
|
+
import { getActiveBucketKeys, getStore, activateBucket, writeRoutingConfig } from "./storageServerState";
|
|
11
|
+
import { readRoutingFromDisk, readNewestRoutingFile } from "./bucketDisk";
|
|
11
12
|
import { DEFAULT_FAST_WRITE_DELAY } from "./ArchivesDelayed";
|
|
12
13
|
import { WINDOW_END_FLUSH_MARGIN } from "./blobStore";
|
|
13
14
|
|
|
@@ -26,19 +27,20 @@ const INTERMEDIATE_MAINTAIN_INTERVAL = 60 * 1000;
|
|
|
26
27
|
const scheduledBoundaryScans = new Set<string>();
|
|
27
28
|
const boundaryScansRunning = new Set<string>();
|
|
28
29
|
|
|
29
|
-
/** Called
|
|
30
|
-
export function scheduleBoundaryWork(
|
|
31
|
-
let key = `${
|
|
30
|
+
/** Called every time a store applies a routing config to itself (see BlobStore's onRoutingApplied): arms the scans the config's upcoming window boundaries need. Each scan is scheduled once - the key includes the boundary it is for - so re-arming on every config application is harmless. */
|
|
31
|
+
export function scheduleBoundaryWork(account: string, bucketName: string, routing: RemoteConfig): void {
|
|
32
|
+
let key = `${account}/${bucketName}`;
|
|
33
|
+
let selfEntries = findSelfIndexes(routing, account, bucketName).map(i => routing.sources[i] as HostedConfig);
|
|
32
34
|
let now = Date.now();
|
|
33
35
|
let starts = new Set<number>();
|
|
34
|
-
for (let self of
|
|
36
|
+
for (let self of selfEntries) {
|
|
35
37
|
let start = self.validWindow[0];
|
|
36
38
|
if (start > now && start < Number.MAX_SAFE_INTEGER) {
|
|
37
39
|
starts.add(start);
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
// A window we have only just taken over, on a server that has only just started: the boundary happened while we were not running, so its scan still has to happen
|
|
41
|
-
for (let self of
|
|
43
|
+
for (let self of selfEntries) {
|
|
42
44
|
let [start, end] = self.validWindow;
|
|
43
45
|
if (!(start <= now && now < end) || start <= 0) continue;
|
|
44
46
|
if (now - start > BOUNDARY_SCAN_LOOKBACK) continue;
|
|
@@ -46,7 +48,7 @@ export function scheduleBoundaryWork(loaded: BucketState): void {
|
|
|
46
48
|
if (scheduledBoundaryScans.has(scheduleKey)) continue;
|
|
47
49
|
scheduledBoundaryScans.add(scheduleKey);
|
|
48
50
|
let timer = setTimeout(() => {
|
|
49
|
-
void runBoundaryScan(
|
|
51
|
+
void runBoundaryScan(account, bucketName, start, STARTUP_BOUNDARY_SCAN_DELAY, "startup+30s").catch((e: Error) => console.error(`Boundary scan for bucket ${key} failed: ${e.stack ?? e}`));
|
|
50
52
|
}, STARTUP_BOUNDARY_SCAN_DELAY);
|
|
51
53
|
(timer as { unref?: () => void }).unref?.();
|
|
52
54
|
}
|
|
@@ -64,7 +66,7 @@ export function scheduleBoundaryWork(loaded: BucketState): void {
|
|
|
64
66
|
arm();
|
|
65
67
|
return;
|
|
66
68
|
}
|
|
67
|
-
void runBoundaryScan(
|
|
69
|
+
void runBoundaryScan(account, bucketName, start, offset).catch((e: Error) => console.error(`Boundary scan for bucket ${key} failed: ${e.stack ?? e}`));
|
|
68
70
|
}, Math.min(at - Date.now(), MAX_TIMER_DELAY));
|
|
69
71
|
(timer as { unref?: () => void }).unref?.();
|
|
70
72
|
};
|
|
@@ -73,9 +75,9 @@ export function scheduleBoundaryWork(loaded: BucketState): void {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
/** Collects the writes that landed on the previous window's owners just before a boundary, into the store that has just taken over. Who owned what is worked out purely from the config (see previousWindowOwners); this only does the pulling. */
|
|
77
|
-
async function runBoundaryScan(
|
|
78
|
-
let key = `${
|
|
78
|
+
/** Collects the writes that landed on the previous window's owners just before a boundary, into the store that has just taken over. Who owned what is worked out purely from the config (see previousWindowOwners); this only does the pulling. The config is read off the disk at FIRE time, not captured when the scan was armed - it may have changed in between, and the newest copy is the one the handover actually happened under. */
|
|
79
|
+
async function runBoundaryScan(account: string, bucketName: string, windowStart: number, offset: number, offsetLabel?: string): Promise<void> {
|
|
80
|
+
let key = `${account}/${bucketName}`;
|
|
79
81
|
let label = `bucket ${key}, window starting ${new Date(windowStart).toISOString()}, offset ${offsetLabel || `${offset / 1000}s`}`;
|
|
80
82
|
if (boundaryScansRunning.has(key)) {
|
|
81
83
|
console.log(`Skipping boundary scan (${label}): the previous boundary scan is still running`);
|
|
@@ -83,23 +85,22 @@ async function runBoundaryScan(loaded: BucketState, windowStart: number, offset:
|
|
|
83
85
|
}
|
|
84
86
|
boundaryScansRunning.add(key);
|
|
85
87
|
try {
|
|
86
|
-
let effective =
|
|
88
|
+
let effective = await readRoutingFromDisk(account, bucketName);
|
|
87
89
|
if (!effective) return;
|
|
88
|
-
let handovers = previousWindowOwners(effective, windowStart, findSelfIndexes(effective,
|
|
90
|
+
let handovers = previousWindowOwners(effective, windowStart, findSelfIndexes(effective, account, bucketName));
|
|
89
91
|
if (!handovers.length) return;
|
|
90
92
|
console.log(`Boundary scan (${label}): ${handovers.length} route(s) taking over: ${handovers.map(x => `${JSON.stringify(x.route)} from ${x.scanOwnDisk && "our own disk" || ""}${x.scanOwnDisk && x.remotes.size && " + " || ""}${[...x.remotes.keys()].map(i => `source ${i}`).join(", ")}`).join("; ")}`);
|
|
91
93
|
let since = windowStart - BOUNDARY_SCAN_LOOKBACK;
|
|
92
94
|
for (let handover of handovers) {
|
|
93
|
-
let selfStore =
|
|
94
|
-
if (!selfStore) continue;
|
|
95
|
+
let selfStore = getStore(account, bucketName, handover.name);
|
|
95
96
|
if (handover.scanOwnDisk) {
|
|
96
|
-
await selfStore.
|
|
97
|
+
await selfStore.rescanBase();
|
|
97
98
|
}
|
|
98
99
|
for (let [sourceIndex, route] of handover.remotes) {
|
|
99
100
|
let ownerSource = effective.sources[sourceIndex];
|
|
100
101
|
if (typeof ownerSource === "string") continue;
|
|
101
102
|
try {
|
|
102
|
-
await selfStore.
|
|
103
|
+
await selfStore.boundaryScanRemote(createApiArchives(ownerSource), { since, route });
|
|
103
104
|
} catch (e) {
|
|
104
105
|
console.error(`Boundary scan (${label}) of previous-window owner (source index ${sourceIndex}) failed: ${(e as Error).stack ?? e}`);
|
|
105
106
|
}
|
|
@@ -133,66 +134,69 @@ export function reinjectIntermediates(current: RemoteConfig | undefined, incomin
|
|
|
133
134
|
// Per bucket, not global: a switchover has to write every bucket's windows at once, and a shared throttle would let only one bucket through per interval
|
|
134
135
|
const lastOwnConfigWrite = new Map<string, number>();
|
|
135
136
|
|
|
136
|
-
async function writeOwnRoutingConfig(
|
|
137
|
-
let
|
|
137
|
+
async function writeOwnRoutingConfig(config: { account: string; bucketName: string; storeName: string; current: RemoteConfig; updated: RemoteConfig; reason: string }): Promise<void> {
|
|
138
|
+
let { account, bucketName, current, updated, reason } = config;
|
|
139
|
+
let key = `${account}/${bucketName}`;
|
|
138
140
|
let sinceLast = Date.now() - (lastOwnConfigWrite.get(key) || 0);
|
|
139
141
|
if (sinceLast < OWN_CONFIG_WRITE_THROTTLE) {
|
|
140
142
|
console.log(`Not writing our own routing config update for bucket ${key} (${reason}): the last one was ${sinceLast}ms ago, under the ${OWN_CONFIG_WRITE_THROTTLE}ms throttle. Retrying on the next maintenance pass.`);
|
|
141
143
|
return;
|
|
142
144
|
}
|
|
143
145
|
lastOwnConfigWrite.set(key, Date.now());
|
|
144
|
-
|
|
145
|
-
if (!loaded.routing) return;
|
|
146
|
-
let currentVersion = getConfigVersion(loaded.routing);
|
|
146
|
+
let currentVersion = getConfigVersion(current);
|
|
147
147
|
let version = nextIntermediateVersion(currentVersion);
|
|
148
148
|
let next: RemoteConfig = { ...updated, version };
|
|
149
149
|
console.log(`Writing ${ROUTING_FILE} for bucket ${key} (${reason}), version ${currentVersion} -> ${version}: ${JSON.stringify(next)}`);
|
|
150
150
|
let data = Buffer.from(serializeRemoteConfig(next));
|
|
151
|
-
// Into
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await writeRoutingConfig(loaded.account, loaded.bucketName, ourStore.name, data);
|
|
155
|
-
}
|
|
156
|
-
await propagateRoutingConfig(loaded, next, data);
|
|
151
|
+
// Into the store holding the newest copy - it applies it and its peers pull it - and directly to the other servers, which is what makes a switchover take effect in seconds rather than at the next poll
|
|
152
|
+
await writeRoutingConfig(account, bucketName, config.storeName, data);
|
|
153
|
+
await propagateRoutingConfig({ account, bucketName, current, next, data });
|
|
157
154
|
}
|
|
158
155
|
|
|
159
|
-
async function propagateRoutingConfig(
|
|
156
|
+
async function propagateRoutingConfig(config: { account: string; bucketName: string; current: RemoteConfig; next: RemoteConfig; data: Buffer }): Promise<void> {
|
|
160
157
|
let targets = new Map<string, SourceConfig>();
|
|
161
|
-
for (let source of [...
|
|
158
|
+
for (let source of [...config.current.sources, ...config.next.sources]) {
|
|
162
159
|
if (typeof source === "string" || source.intermediate) continue;
|
|
163
160
|
if (source.type === "remote") {
|
|
164
161
|
let parsed = parseHostedUrl(source.url);
|
|
165
|
-
if (parsed.account !==
|
|
162
|
+
if (parsed.account !== config.account || parsed.bucketName !== config.bucketName) continue;
|
|
166
163
|
}
|
|
167
164
|
if (targets.has(source.url)) continue;
|
|
168
165
|
targets.set(source.url, source);
|
|
169
166
|
}
|
|
170
167
|
for (let [url, source] of targets) {
|
|
171
168
|
try {
|
|
172
|
-
await createApiArchives(source).set(ROUTING_FILE, data);
|
|
173
|
-
console.log(`Wrote ${ROUTING_FILE} for bucket ${
|
|
169
|
+
await createApiArchives(source).set(ROUTING_FILE, config.data);
|
|
170
|
+
console.log(`Wrote ${ROUTING_FILE} for bucket ${config.account}/${config.bucketName} to ${url}`);
|
|
174
171
|
} catch (e) {
|
|
175
172
|
console.error(`Propagating our routing config update to ${url} failed: ${(e as Error).stack ?? e}`);
|
|
176
173
|
}
|
|
177
174
|
}
|
|
178
175
|
}
|
|
179
176
|
|
|
180
|
-
/** Keeps every
|
|
177
|
+
/** Keeps every active bucket's switchover windows current: gives our alternate port its window while a takeover is in flight, and takes expired ones back out. */
|
|
181
178
|
async function maintainIntermediates(): Promise<void> {
|
|
182
179
|
let { domain, port } = getStorageServerConfig();
|
|
183
180
|
let takeover = getTakeoverIntermediate();
|
|
184
181
|
let written: string[] = [];
|
|
185
182
|
let alreadyCorrect: string[] = [];
|
|
186
183
|
let notOurs: string[] = [];
|
|
187
|
-
// ONLY buckets already
|
|
188
|
-
let
|
|
189
|
-
for (let
|
|
190
|
-
let key = `${
|
|
191
|
-
let
|
|
192
|
-
if (!
|
|
184
|
+
// ONLY buckets already active. A switchover must never activate a bucket: that starts its synchronization, and buckets nothing has touched (legacy ones especially) have no writes to hand off in the first place. One that does get used mid-switchover activates on that access, and the next pass gives it its window.
|
|
185
|
+
let activeKeys = getActiveBucketKeys();
|
|
186
|
+
for (let { account, bucketName } of activeKeys) {
|
|
187
|
+
let key = `${account}/${bucketName}`;
|
|
188
|
+
let newest = await readNewestRoutingFile(account, bucketName);
|
|
189
|
+
if (!newest) continue;
|
|
190
|
+
let routing: RemoteConfig;
|
|
191
|
+
try {
|
|
192
|
+
routing = parseRoutingData(newest.data);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.error(`Skipping switchover maintenance for bucket ${key}: its routing config could not be parsed (${(e as Error).message})`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
193
197
|
let expired = expireIntermediateSources(routing, Date.now());
|
|
194
198
|
if (JSON.stringify(expired) !== JSON.stringify(routing)) {
|
|
195
|
-
await writeOwnRoutingConfig(
|
|
199
|
+
await writeOwnRoutingConfig({ account, bucketName, storeName: newest.name, current: routing, updated: expired, reason: `switchover windows expired more than ${INTERMEDIATE_EXPIRE_GRACE / 1000}s ago` });
|
|
196
200
|
written.push(key);
|
|
197
201
|
continue;
|
|
198
202
|
}
|
|
@@ -216,16 +220,16 @@ async function maintainIntermediates(): Promise<void> {
|
|
|
216
220
|
alreadyCorrect.push(key);
|
|
217
221
|
continue;
|
|
218
222
|
}
|
|
219
|
-
await writeOwnRoutingConfig(
|
|
223
|
+
await writeOwnRoutingConfig({ account, bucketName, storeName: newest.name, current: routing, updated: injected, reason: `we are a deploy successor: writes route to our alternate port ${takeover.altPort} from ${new Date(takeover.start).toISOString()} until our predecessor is killed at ${new Date(takeover.end).toISOString()}` });
|
|
220
224
|
written.push(key);
|
|
221
225
|
}
|
|
222
226
|
if (!takeover) return;
|
|
223
|
-
if (!
|
|
227
|
+
if (!activeKeys.length) {
|
|
224
228
|
console.log(`No active buckets, so no config files to write the intermediate into (a bucket is only active once something uses it)`);
|
|
225
229
|
return;
|
|
226
230
|
}
|
|
227
231
|
if (!written.length) {
|
|
228
|
-
console.log(`Wrote no config files: of ${
|
|
232
|
+
console.log(`Wrote no config files: of ${activeKeys.length} active buckets, ${alreadyCorrect.length} already contain the intermediate and ${notOurs.length} are not hosted by us`);
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
|
|
@@ -247,7 +251,10 @@ async function activatePredecessorBuckets(): Promise<void> {
|
|
|
247
251
|
console.log(`Our predecessor on ${url} has ${keys.length} active buckets, activating them here so their config files get the intermediate: ${keys.map(x => `${x.account}/${x.bucketName}`).join(", ")}`);
|
|
248
252
|
for (let { account, bucketName } of keys) {
|
|
249
253
|
try {
|
|
250
|
-
await
|
|
254
|
+
let result = await activateBucket(account, bucketName);
|
|
255
|
+
if (typeof result === "string") {
|
|
256
|
+
console.error(`Activating bucket ${account}/${bucketName} (active on our predecessor) failed: ${result}`);
|
|
257
|
+
}
|
|
251
258
|
} catch (e) {
|
|
252
259
|
console.error(`Activating bucket ${account}/${bucketName} (active on our predecessor) failed: ${(e as Error).stack ?? e}`);
|
|
253
260
|
}
|
|
@@ -9,11 +9,12 @@ import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMach
|
|
|
9
9
|
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, FindConfig, SourceConfig, IMMUTABLE_CACHE_TIME } from "../IArchives";
|
|
10
10
|
import { ROUTING_FILE, getRoute, routeContains } from "./remoteConfig";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
findBucketStore, getStore, getBucketStores, readBucketInternal, writeRoutingConfig,
|
|
13
|
+
getBucketArchivesConfig, bucketSyncStatus, debugBucketIndexTotals, ActiveBucketInfo,
|
|
14
|
+
debugListAccountBuckets, ServerBucketInfo,
|
|
15
|
+
debugGetActiveBucket, activateBucket, getActiveBucketKeys,
|
|
16
16
|
} from "./storageServerState";
|
|
17
|
+
import { debugClearAccountWriteStats } from "./accessStats";
|
|
17
18
|
import { getStorageServerConfig, getTrust, getRequests, assertWritesAllowed } from "./serverConfig";
|
|
18
19
|
import { BlobStore } from "./blobStore";
|
|
19
20
|
import { getRoutingFileResult } from "./bucketDisk";
|
|
@@ -332,13 +333,13 @@ class RemoteStorageControllerBase {
|
|
|
332
333
|
}
|
|
333
334
|
@assertValidArgs
|
|
334
335
|
async getArchivesConfig(config: { account: string; bucketName: string }): Promise<ArchivesConfig> {
|
|
335
|
-
return
|
|
336
|
+
return await getBucketArchivesConfig(config.account, config.bucketName);
|
|
336
337
|
}
|
|
337
338
|
@assertValidArgs
|
|
338
339
|
async listBuckets(config: { account: string }): Promise<ServerBucketInfo[]> {
|
|
339
340
|
let start = Date.now();
|
|
340
341
|
try {
|
|
341
|
-
return await
|
|
342
|
+
return await debugListAccountBuckets(config.account);
|
|
342
343
|
} finally {
|
|
343
344
|
// The access hook (and the storage it initializes) runs before this, so a large gap between this and the caller's timing is the hook
|
|
344
345
|
console.log(`listBuckets(${config.account}) call body took ${Date.now() - start}ms`);
|
|
@@ -347,7 +348,7 @@ class RemoteStorageControllerBase {
|
|
|
347
348
|
/** The live, in-memory state of one bucket, or a string saying why it is unavailable. Answered without touching the disk, so it is cheap - but only works while the bucket is loaded here. */
|
|
348
349
|
@assertValidArgs
|
|
349
350
|
async getActiveBucket(config: { account: string; bucketName: string }): Promise<ActiveBucketInfo | string> {
|
|
350
|
-
return await
|
|
351
|
+
return await debugGetActiveBucket(config.account, config.bucketName);
|
|
351
352
|
}
|
|
352
353
|
/** Loads a bucket that exists on this server into memory (starting its synchronization) and returns its live state, or a string saying why it could not be loaded. */
|
|
353
354
|
@assertValidArgs
|
|
@@ -358,7 +359,7 @@ class RemoteStorageControllerBase {
|
|
|
358
359
|
@assertValidArgs
|
|
359
360
|
async clearWriteStats(config: { account: string }): Promise<{ clearedBuckets: number }> {
|
|
360
361
|
clearAccountAccessStats(config.account);
|
|
361
|
-
return { clearedBuckets:
|
|
362
|
+
return { clearedBuckets: debugClearAccountWriteStats(config.account) };
|
|
362
363
|
}
|
|
363
364
|
/** In-memory totals per operation type since startup (or the last clearWriteStats). */
|
|
364
365
|
@assertValidArgs
|
|
@@ -372,22 +373,22 @@ class RemoteStorageControllerBase {
|
|
|
372
373
|
}
|
|
373
374
|
@assertValidArgs
|
|
374
375
|
async getIndexInfo(config: { account: string; bucketName: string }): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
|
|
375
|
-
return await
|
|
376
|
+
return await debugBucketIndexTotals(config.account, config.bucketName);
|
|
376
377
|
}
|
|
377
378
|
@assertValidArgs
|
|
378
379
|
async getSyncStatus(config: { account: string; bucketName: string }): Promise<ArchivesSyncStatus> {
|
|
379
|
-
return await bucketSyncStatus(
|
|
380
|
+
return await bucketSyncStatus(config.account, config.bucketName);
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
@assertValidArgs
|
|
383
384
|
async startLargeFile(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; lastModified?: number; forceSetImmutable?: boolean; noChecks?: boolean; internal?: boolean }): Promise<string> {
|
|
384
385
|
assertWritesAllowed();
|
|
385
|
-
let
|
|
386
|
+
let store = findBucketStore(config.account, config.bucketName, config.sourceConfig);
|
|
386
387
|
// The store validates the write here, before any bytes move (see BlobStore.startLargeUpload) - a large file is a set, and its SetConfig decides whether it is even allowed
|
|
387
388
|
let write = { path: config.path, lastModified: config.lastModified, forceSetImmutable: config.forceSetImmutable, noChecks: config.noChecks, internal: config.internal };
|
|
388
|
-
let id = await
|
|
389
|
+
let id = await store.startLargeUpload(write);
|
|
389
390
|
// The store name pins the upload's parts and finish to the same store the start picked (in-flight uploads survive a bucket rebuild: the part data lives in that store's folder, which a rebuilt store still opens)
|
|
390
|
-
largeUploadInfo.set(id, { ...write, account: config.account, bucketName: config.bucketName, storeName:
|
|
391
|
+
largeUploadInfo.set(id, { ...write, account: config.account, bucketName: config.bucketName, storeName: config.sourceConfig.name });
|
|
391
392
|
return id;
|
|
392
393
|
}
|
|
393
394
|
async uploadPart(config: { uploadId: string; data: Buffer }): Promise<void> {
|
|
@@ -395,28 +396,21 @@ class RemoteStorageControllerBase {
|
|
|
395
396
|
let info = largeUploadInfo.get(config.uploadId);
|
|
396
397
|
if (!info) throw new Error(`Unknown large upload ${config.uploadId}`);
|
|
397
398
|
trackAccess({ account: info.account, operation: "uploadPart", path: `${info.bucketName}/${info.path}`, size: config.data.length });
|
|
398
|
-
|
|
399
|
-
await target.store.appendLargeUpload({ id: config.uploadId, data: Buffer.from(config.data) });
|
|
399
|
+
await getStore(info.account, info.bucketName, info.storeName).appendLargeUpload({ id: config.uploadId, data: Buffer.from(config.data) });
|
|
400
400
|
}
|
|
401
401
|
async finishLargeFile(config: { uploadId: string }): Promise<void> {
|
|
402
402
|
assertWritesAllowed();
|
|
403
403
|
let info = largeUploadInfo.get(config.uploadId);
|
|
404
404
|
if (!info) throw new Error(`Unknown large upload ${config.uploadId}`);
|
|
405
405
|
largeUploadInfo.delete(config.uploadId);
|
|
406
|
-
|
|
407
|
-
await target.store.finishLargeUpload({ id: config.uploadId, path: info.path, lastModified: info.lastModified, forceSetImmutable: info.forceSetImmutable, noChecks: info.noChecks, internal: info.internal });
|
|
406
|
+
await getStore(info.account, info.bucketName, info.storeName).finishLargeUpload({ id: config.uploadId, path: info.path, lastModified: info.lastModified, forceSetImmutable: info.forceSetImmutable, noChecks: info.noChecks, internal: info.internal });
|
|
408
407
|
}
|
|
409
|
-
/** Best-effort cleanup: an upload
|
|
408
|
+
/** Best-effort cleanup: an unknown upload has nothing left to cancel. */
|
|
410
409
|
async cancelLargeFile(config: { uploadId: string }): Promise<void> {
|
|
411
410
|
let info = largeUploadInfo.get(config.uploadId);
|
|
412
411
|
if (!info) return;
|
|
413
412
|
largeUploadInfo.delete(config.uploadId);
|
|
414
|
-
|
|
415
|
-
if (!bucket) return;
|
|
416
|
-
const storeName = info.storeName;
|
|
417
|
-
let target = bucket.stores.find(x => x.name === storeName);
|
|
418
|
-
if (!target) return;
|
|
419
|
-
await target.store.cancelLargeUpload({ id: config.uploadId });
|
|
413
|
+
await getStore(info.account, info.bucketName, info.storeName).cancelLargeUpload({ id: config.uploadId });
|
|
420
414
|
}
|
|
421
415
|
|
|
422
416
|
// IMPORTANT: We can never expose enumeration (listing, prefix search, changes feeds) over this public HTTP endpoint - only exact-key reads. Enumeration would be a massive security risk (public buckets rely on unguessable keys staying unguessable), and could also crash the client by sending them too much data. Listings exist only on the authenticated API (findInfo etc, behind accountAccess).
|
|
@@ -442,20 +436,26 @@ class RemoteStorageControllerBase {
|
|
|
442
436
|
assertValidName(account, "account");
|
|
443
437
|
assertValidName(bucketName, "bucket name");
|
|
444
438
|
assertValidPath(filePath);
|
|
445
|
-
let
|
|
446
|
-
if (!
|
|
439
|
+
let bucketStores = await getBucketStores(account, bucketName);
|
|
440
|
+
if (!bucketStores.length) {
|
|
447
441
|
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
448
442
|
}
|
|
449
|
-
|
|
443
|
+
// Each store's policy comes from its own config, which it has only read once init ran
|
|
444
|
+
await Promise.all(bucketStores.map(x => x.store.init()));
|
|
445
|
+
let policies = bucketStores.map(x => ({ store: x.store, policy: x.store.storeConfig.current() }));
|
|
446
|
+
if (!policies.some(x => x.policy.public)) {
|
|
450
447
|
throw new Error(`Bucket ${account}/${bucketName} is not public, so its files cannot be read over plain URLs`);
|
|
451
448
|
}
|
|
452
449
|
// Anonymous URL reads carry no source selection - the file's route picks the store, and a route no store here covers is simply not found (the routing file itself lives outside every store)
|
|
453
|
-
let httpStore:
|
|
450
|
+
let httpStore: BlobStore | undefined;
|
|
451
|
+
let immutable = false;
|
|
454
452
|
let info: { writeTime: number; size: number } | undefined;
|
|
455
453
|
if (filePath !== ROUTING_FILE) {
|
|
456
454
|
let route = getRoute(filePath);
|
|
457
|
-
|
|
458
|
-
|
|
455
|
+
let serving = policies.find(x => x.policy.public && routeContains(x.policy.route, route));
|
|
456
|
+
httpStore = serving?.store;
|
|
457
|
+
immutable = !!serving?.policy.immutable;
|
|
458
|
+
info = httpStore && await httpStore.getInfo({ path: filePath }) || undefined;
|
|
459
459
|
} else {
|
|
460
460
|
info = await getRoutingFileResult(account, bucketName);
|
|
461
461
|
}
|
|
@@ -469,7 +469,8 @@ class RemoteStorageControllerBase {
|
|
|
469
469
|
"Last-Modified": new Date(info.writeTime).toUTCString(),
|
|
470
470
|
"Accept-Ranges": "bytes",
|
|
471
471
|
};
|
|
472
|
-
|
|
472
|
+
// The serving store's own policy - and never for the routing file, which changing is the whole point of
|
|
473
|
+
if (immutable) {
|
|
473
474
|
headers["Cache-Control"] = `max-age=${IMMUTABLE_CACHE_TIME / 1000}`;
|
|
474
475
|
}
|
|
475
476
|
let ifModifiedSince = request?.headers["if-modified-since"];
|
|
@@ -502,7 +503,7 @@ class RemoteStorageControllerBase {
|
|
|
502
503
|
}
|
|
503
504
|
let result: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
504
505
|
if (httpStore) {
|
|
505
|
-
result = await httpStore.
|
|
506
|
+
result = await httpStore.get2({ path: filePath, range });
|
|
506
507
|
} else {
|
|
507
508
|
result = await getRoutingFileResult(account, bucketName);
|
|
508
509
|
if (result && range) {
|
|
@@ -520,23 +521,13 @@ class RemoteStorageControllerBase {
|
|
|
520
521
|
}
|
|
521
522
|
}
|
|
522
523
|
|
|
523
|
-
/** The one resolution every data call does: the client's selected sourceConfig
|
|
524
|
+
/** The one resolution every data call does: the client's selected sourceConfig NAMES exactly one store (created on first use), and fn runs on it. */
|
|
524
525
|
async function withStore<T>(config: { account: string; bucketName: string; sourceConfig: SourceConfig }, fn: (store: BlobStore) => Promise<T>): Promise<T> {
|
|
525
|
-
|
|
526
|
-
return await fn(target.store);
|
|
526
|
+
return await fn(findBucketStore(config.account, config.bucketName, config.sourceConfig));
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string; lastModified?: number; forceSetImmutable?: boolean; noChecks?: boolean; internal?: boolean; storeName: string }>();
|
|
530
530
|
|
|
531
|
-
async function findUploadStore(info: { account: string; bucketName: string; storeName: string }): Promise<LoadedStore> {
|
|
532
|
-
let bucket = await requireBucket(info.account, info.bucketName);
|
|
533
|
-
let target = bucket.stores.find(x => x.name === info.storeName);
|
|
534
|
-
if (!target) {
|
|
535
|
-
throw new Error(`The store ${JSON.stringify(info.storeName)} this upload targets no longer exists on bucket ${info.account}/${info.bucketName} (available: ${JSON.stringify(bucket.stores.map(x => x.name))})`);
|
|
536
|
-
}
|
|
537
|
-
return target;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
531
|
const accountAccess: SocketFunctionHook = async (context) => {
|
|
541
532
|
let start = Date.now();
|
|
542
533
|
let config = context.call.args[0] as { account?: string } | undefined;
|
|
@@ -1,38 +1,17 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { BlobStore } from "./blobStore";
|
|
4
|
-
import { RemoteConfig,
|
|
4
|
+
import { RemoteConfig, SourceConfig, IArchives, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
|
|
5
5
|
import { BucketDiskInfo } from "./bucketDisk";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* - what exists on disk, for listings and for the pages that report it
|
|
13
|
-
*
|
|
14
|
-
* It decides nothing about routes, windows or peers - a store reads its own config and works that
|
|
15
|
-
* out itself (BlobStore.applyRoutingConfig) - and it schedules nothing: switchovers and the handovers
|
|
16
|
-
* they cause live in intermediateManagement.
|
|
17
|
-
*/
|
|
18
|
-
export type LoadedStore = {
|
|
6
|
+
import { BucketWriteStats } from "./accessStats";
|
|
7
|
+
export declare function getStore(account: string, bucketName: string, name: string): BlobStore;
|
|
8
|
+
/** The store serving a request: the one the client's selected entry NAMES. Account, name, and bucket ARE the folder, so this is a direct lookup, never a search of what exists - and a name this server has never seen is CREATED, never rejected, because asking for a name is the instruction to have that store (one name, one folder, one index; it configures itself once the routing config lands in it). Nothing else about the request is compared - not the window, not the route, not the flags - which is the whole point of naming it: a client a config version behind on some flag still reaches the right store. */
|
|
9
|
+
export declare function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): BlobStore;
|
|
10
|
+
/** The stores of a bucket as the DISK records them (a bucket is nothing more than the store folders sharing its name), opened - so the ones that weren't running yet start synchronizing. Empty when the bucket does not exist here. */
|
|
11
|
+
export declare function getBucketStores(account: string, bucketName: string): Promise<{
|
|
19
12
|
name: string;
|
|
20
|
-
entries: HostedConfig[];
|
|
21
|
-
folder: string;
|
|
22
13
|
store: BlobStore;
|
|
23
|
-
}
|
|
24
|
-
export type BucketState = {
|
|
25
|
-
account: string;
|
|
26
|
-
bucketName: string;
|
|
27
|
-
routing?: RemoteConfig;
|
|
28
|
-
selfEntries: HostedConfig[];
|
|
29
|
-
self: SelfSummary | undefined;
|
|
30
|
-
stores: LoadedStore[];
|
|
31
|
-
};
|
|
32
|
-
/** The loaded bucket, loading it (which instantiates its stores and starts their synchronization) if needed. A bucket that does not exist on this server throws - callers never see undefined buckets. */
|
|
33
|
-
export declare function requireBucket(account: string, bucketName: string): Promise<BucketState>;
|
|
34
|
-
/** The store serving a request: the one the client's selected entry NAMES. Nothing else is compared - not the window, not the route, not the flags - because the name is what says which storage this is, and everything else is policy the store itself applies to the request. A client a config version behind on some flag therefore still reaches the right store, which is the whole point of naming it. */
|
|
35
|
-
export declare function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): Promise<LoadedStore>;
|
|
14
|
+
}[]>;
|
|
36
15
|
/** Internal (store-to-store) reads skip store selection entirely: the caller is another store whose index says this MACHINE holds the bytes - the persisted holder identity is just a URL, which cannot name a store. Whichever store's folder has the newest copy answers. */
|
|
37
16
|
export declare function readBucketInternal(account: string, bucketName: string, config: {
|
|
38
17
|
path: string;
|
|
@@ -46,9 +25,9 @@ export declare function readBucketInternal(account: string, bucketName: string,
|
|
|
46
25
|
writeTime: number;
|
|
47
26
|
size: number;
|
|
48
27
|
} | undefined>;
|
|
49
|
-
export declare function
|
|
50
|
-
export declare function bucketSyncStatus(
|
|
51
|
-
export declare function
|
|
28
|
+
export declare function getBucketArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig>;
|
|
29
|
+
export declare function bucketSyncStatus(account: string, bucketName: string): Promise<ArchivesSyncStatus>;
|
|
30
|
+
export declare function debugBucketIndexTotals(account: string, bucketName: string): Promise<{
|
|
52
31
|
fileCount: number;
|
|
53
32
|
byteCount: number;
|
|
54
33
|
sources: {
|
|
@@ -57,14 +36,8 @@ export declare function bucketIndexTotals(bucket: BucketState): Promise<{
|
|
|
57
36
|
byteCount: number;
|
|
58
37
|
}[];
|
|
59
38
|
}>;
|
|
60
|
-
/** The bucket states currently in memory - what the switchover machinery walks. Only what is loaded: loading a bucket starts its synchronization, and a bucket nothing has touched has no in-flight writes to hand over anyway. */
|
|
61
|
-
export declare function getBucketStates(): BucketState[];
|
|
62
39
|
/** A cached IArchives for a persisted source identity: a routing URL (hosted/backblaze) or a disk folder path - the form BlobStore's sources list stores. Configuration (valid windows, routes) decides WHEN a source should be used; for reading bytes the index says a source holds, the URL alone is enough - even for sources no longer in any config. */
|
|
63
40
|
export declare function resolveSourceArchives(url: string): IArchives;
|
|
64
|
-
export declare function getStore(account: string, bucketName: string, name: string): BlobStore;
|
|
65
|
-
export declare function getLoadedBucket(account: string, bucketName: string): Promise<BucketState | undefined>;
|
|
66
|
-
/** Re-reads the bucket off the disk, replacing the cached snapshot: what a routing-config write calls, because that write can CREATE a store (a new name gets a new folder) and the snapshot would keep answering with the stores it saw at load time. Existing stores are reused (getStore caches by folder), so this only picks up folder changes - it never restarts a store. */
|
|
67
|
-
export declare function reloadBucket(account: string, bucketName: string): Promise<BucketState | undefined>;
|
|
68
41
|
/**
|
|
69
42
|
* Writing the routing config is a write like any other: it goes into a store, and the store applies
|
|
70
43
|
* it to itself and lets its peers pull it. The only thing that happens here is picking WHICH store,
|
|
@@ -77,7 +50,7 @@ export declare function reloadBucket(account: string, bucketName: string): Promi
|
|
|
77
50
|
export declare function writeRoutingConfig(account: string, bucketName: string, name: string, data: Buffer, config?: {
|
|
78
51
|
lastModified?: number;
|
|
79
52
|
}): Promise<void>;
|
|
80
|
-
/** Which buckets this process currently has
|
|
53
|
+
/** Which buckets this process currently has active (some store of theirs was opened) - what a deploy successor asks its predecessor for, so it activates exactly the buckets that are actually in use. */
|
|
81
54
|
export declare function getActiveBucketKeys(): {
|
|
82
55
|
account: string;
|
|
83
56
|
bucketName: string;
|
|
@@ -96,25 +69,12 @@ export type ServerBucketInfo = {
|
|
|
96
69
|
};
|
|
97
70
|
export type ActiveBucketInfo = {
|
|
98
71
|
folder: string;
|
|
99
|
-
/** The routing config the
|
|
72
|
+
/** The bucket's routing config, the newest copy among its stores. Absent when none of them has one yet. */
|
|
100
73
|
routing?: RemoteConfig;
|
|
101
|
-
/** Our own entries in that config, and their summarized current role (routes union + flags) */
|
|
102
|
-
selfEntries: HostedConfig[];
|
|
103
|
-
self?: SelfSummary;
|
|
104
74
|
config: ArchivesConfig;
|
|
105
75
|
};
|
|
106
|
-
/** The
|
|
107
|
-
export declare function
|
|
108
|
-
/** Loads a bucket that exists on this server's disk into memory, which starts
|
|
76
|
+
/** The state of ONE active bucket. Returns an error string when the bucket is not active here, which is the normal state for a bucket nothing has accessed since startup. */
|
|
77
|
+
export declare function debugGetActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
78
|
+
/** Loads every store of a bucket that exists on this server's disk into memory, which starts their synchronization and window timers, and returns the bucket's state. Nothing is written and no other server is contacted - unlike building an ArchivesChain for it, which would probe every source and could write the routing config. Already-active buckets just return their state. */
|
|
109
79
|
export declare function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
110
|
-
export declare function
|
|
111
|
-
export type BucketWriteStats = {
|
|
112
|
-
/** Every set call the bucket accepted */
|
|
113
|
-
originalWrites: number;
|
|
114
|
-
originalBytes: number;
|
|
115
|
-
/** What actually reached the sources. Fast writes coalesce repeated writes to the same key, so this is lower than the original counts (and is what the disk actually did). */
|
|
116
|
-
flushedWrites: number;
|
|
117
|
-
flushedBytes: number;
|
|
118
|
-
};
|
|
119
|
-
/** Zeroes the write statistics of every bucket in the account. */
|
|
120
|
-
export declare function clearAccountWriteStats(account: string): number;
|
|
80
|
+
export declare function debugListAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
|