sliftutils 1.7.90 → 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 +34 -79
- 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/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/accessStats.ts.cache +42 -3
- package/storage/remoteStorage/dist/blobStore.ts.cache +6 -3
- package/storage/remoteStorage/dist/intermediateManagement.ts.cache +56 -50
- package/storage/remoteStorage/dist/storageController.ts.cache +36 -29
- package/storage/remoteStorage/dist/storageServerState.ts.cache +110 -233
- package/storage/remoteStorage/dist/storeConfig.ts.cache +2 -2
- package/storage/remoteStorage/dist/storePlan.ts.cache +4 -56
- package/storage/remoteStorage/intermediateManagement.d.ts +2 -3
- package/storage/remoteStorage/intermediateManagement.ts +53 -46
- package/storage/remoteStorage/storageController.ts +22 -14
- package/storage/remoteStorage/storageServerState.d.ts +15 -55
- package/storage/remoteStorage/storageServerState.ts +106 -268
- 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,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
|
-
* What this server HAS, as opposed to what any of it does. Four things, and nothing else:
|
|
9
|
-
*
|
|
10
|
-
* - the stores, by folder (getStore) - made once, and self-configuring from there
|
|
11
|
-
* - which of them a request means (findBucketStore), which is only ever a name
|
|
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 = {
|
|
19
|
-
name: string;
|
|
20
|
-
entries: HostedConfig[];
|
|
21
|
-
folder: string;
|
|
22
|
-
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>;
|
|
6
|
+
import { BucketWriteStats } from "./accessStats";
|
|
7
|
+
export declare function getStore(account: string, bucketName: string, name: string): BlobStore;
|
|
34
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. */
|
|
35
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<{
|
|
12
|
+
name: string;
|
|
13
|
+
store: BlobStore;
|
|
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 debugBucketIndexTotals(
|
|
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 debugBucketIndexTotals(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
|
|
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. */
|
|
107
77
|
export declare function debugGetActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
108
|
-
/** Loads a bucket that exists on this server's disk into memory, which starts
|
|
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
80
|
export declare function debugListAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
|
|
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 debugClearAccountWriteStats(account: string): number;
|
|
@@ -1,57 +1,61 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
1
|
import { BlobStore } from "./blobStore";
|
|
4
2
|
import {
|
|
5
|
-
RemoteConfig,
|
|
3
|
+
RemoteConfig, SourceConfig, IArchives, ArchivesConfig, ArchivesSyncStatus, SyncActivity,
|
|
6
4
|
} from "../IArchives";
|
|
7
5
|
import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, normalizeSource } from "./remoteConfig";
|
|
8
6
|
import { createStoreSource, applySourceConfig } from "./storeSources";
|
|
9
7
|
import { broadcastRoutingChanged } from "./storageController";
|
|
10
|
-
import {
|
|
8
|
+
import { assertWritesAllowed } from "./serverConfig";
|
|
11
9
|
import { assertValidSourceName } from "./validation";
|
|
12
10
|
import { getBucketFolder, listAccountStoreFolders, listBucketStoreFolders, readRoutingFromDisk, getDiskInfo, BucketDiskInfo } from "./bucketDisk";
|
|
13
11
|
import { scheduleBoundaryWork, reinjectIntermediates } from "./intermediateManagement";
|
|
14
|
-
import {
|
|
12
|
+
import { isSelfSource } from "./storePlan";
|
|
13
|
+
import { countBucketWrite, getBucketWriteStats, BucketWriteStats } from "./accessStats";
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
* What this server HAS
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* - what exists on disk, for listings and for the pages that report it
|
|
22
|
-
*
|
|
23
|
-
* It decides nothing about routes, windows or peers - a store reads its own config and works that
|
|
24
|
-
* out itself (BlobStore.applyRoutingConfig) - and it schedules nothing: switchovers and the handovers
|
|
25
|
-
* they cause live in intermediateManagement.
|
|
16
|
+
* What this server HAS: the stores, by name (getStore) - made once, self-configuring from there -
|
|
17
|
+
* and the three entry points a request resolves through (findBucketStore, readBucketInternal,
|
|
18
|
+
* writeRoutingConfig). There is no cached bucket state: a bucket is nothing but the stores whose
|
|
19
|
+
* folders share its name, listed off the disk when something genuinely needs all of them.
|
|
26
20
|
*/
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// A bucket loaded on this server: the stores it has here, and what the newest of their configs says about us. Pure data - the stores themselves do the work, including configuring themselves.
|
|
38
|
-
export type BucketState = {
|
|
39
|
-
account: string;
|
|
40
|
-
bucketName: string;
|
|
41
|
-
// The newest routing config among its stores, or absent when none of them has one yet
|
|
42
|
-
routing?: RemoteConfig;
|
|
43
|
-
selfEntries: HostedConfig[];
|
|
44
|
-
self: SelfSummary | undefined;
|
|
45
|
-
stores: LoadedStore[];
|
|
46
|
-
};
|
|
22
|
+
/**
|
|
23
|
+
* Every store this process runs, by folder - which IS the store's identity: account, name, bucket.
|
|
24
|
+
* A store is made once, configures itself from the routing config it holds, and stays; nothing here
|
|
25
|
+
* decides what it is, only that it exists.
|
|
26
|
+
*/
|
|
27
|
+
const stores = new Map<string, BlobStore>();
|
|
28
|
+
// The opened stores of each `${account}/${bucketName}`, by name - which buckets are ACTIVE (something touched them), for deploy handoffs and the debug pages. Purely a registry of what getStore already made.
|
|
29
|
+
const activeStores = new Map<string, Map<string, BlobStore>>();
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
export function getStore(account: string, bucketName: string, name: string): BlobStore {
|
|
32
|
+
// The one place a name becomes a folder, so the one place it is validated (it can come straight off the wire)
|
|
33
|
+
assertValidSourceName(name);
|
|
34
|
+
let folder = getBucketFolder(name, account, bucketName);
|
|
35
|
+
let existing = stores.get(folder);
|
|
36
|
+
if (existing) return existing;
|
|
37
|
+
console.log(`Opening store ${JSON.stringify(name)} of bucket ${account}/${bucketName} (folder ${folder}) - created on this first use if it never existed`);
|
|
38
|
+
let store = new BlobStore(folder, name, {
|
|
39
|
+
// Which entries in ITS routing config are this server's copy of it
|
|
40
|
+
isSelf: source => isSelfSource(source, account, bucketName),
|
|
41
|
+
createSource: config => createStoreSource({ sourceConfig: config.sourceConfig, folder, writeDelay: config.writeDelay }),
|
|
42
|
+
applySource: (source, sourceConfig, writeDelay) => applySourceConfig(source, sourceConfig, writeDelay),
|
|
43
|
+
onWriteCounted: (kind, bytes) => countBucketWrite(`${account}/${bucketName}`, kind, bytes),
|
|
44
|
+
resolveSourceUrl: resolveSourceArchives,
|
|
45
|
+
// The store is the one that knows when a config landed, and a config with upcoming windows is what boundary scans are armed from
|
|
46
|
+
onRoutingApplied: routing => scheduleBoundaryWork(account, bucketName, routing),
|
|
47
|
+
});
|
|
48
|
+
stores.set(folder, store);
|
|
49
|
+
let key = `${account}/${bucketName}`;
|
|
50
|
+
let byName = activeStores.get(key);
|
|
51
|
+
if (!byName) {
|
|
52
|
+
byName = new Map();
|
|
53
|
+
activeStores.set(key, byName);
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
byName.set(name, store);
|
|
56
|
+
// Lazy init would leave a store nothing has touched sitting inert - no index loaded, no config applied, nothing synchronizing - while its folder is full of files
|
|
57
|
+
void store.init().catch((e: Error) => console.error(`Initializing store ${JSON.stringify(name)} of bucket ${account}/${bucketName} failed: ${e.stack ?? e}`));
|
|
58
|
+
return store;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/** 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. */
|
|
@@ -59,15 +63,27 @@ export function findBucketStore(account: string, bucketName: string, sourceConfi
|
|
|
59
63
|
if (!sourceConfig) {
|
|
60
64
|
throw new Error(`No remote source configuration was provided for bucket ${account}/${bucketName}: every request must say which configured source it selected`);
|
|
61
65
|
}
|
|
62
|
-
// Not part of resolving the request (the name already says everything): loading the bucket is what schedules its switchover work and marks it active for deploy handoffs, and a bucket serving requests must be loaded
|
|
63
|
-
void getLoadedBucket(account, bucketName).catch((e: Error) => console.error(`Loading bucket ${account}/${bucketName} failed: ${e.stack ?? e}`));
|
|
64
66
|
return getStore(account, bucketName, sourceConfig.name);
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/** 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. */
|
|
70
|
+
export async function getBucketStores(account: string, bucketName: string): Promise<{ name: string; store: BlobStore }[]> {
|
|
71
|
+
let folders = await listBucketStoreFolders(account, bucketName);
|
|
72
|
+
return folders.map(x => ({ name: x.name, store: getStore(account, bucketName, x.name) }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function requireBucketStores(account: string, bucketName: string): Promise<{ name: string; store: BlobStore }[]> {
|
|
76
|
+
let list = await getBucketStores(account, bucketName);
|
|
77
|
+
if (!list.length) {
|
|
78
|
+
throw new Error(`Bucket ${account}/${bucketName} does not exist on this server. Write its routing config to ${JSON.stringify(ROUTING_FILE)} to create it.`);
|
|
79
|
+
}
|
|
80
|
+
return list;
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
/** 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. */
|
|
68
84
|
export async function readBucketInternal(account: string, bucketName: string, config: { path: string; range?: { start: number; end: number }; includeTombstones?: boolean }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
69
|
-
let
|
|
70
|
-
let results = await Promise.all(
|
|
85
|
+
let bucketStores = await requireBucketStores(account, bucketName);
|
|
86
|
+
let results = await Promise.all(bucketStores.map(s => s.store.get2({ path: config.path, range: config.range, includeTombstones: config.includeTombstones, internal: true })));
|
|
71
87
|
let best: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
72
88
|
for (let result of results) {
|
|
73
89
|
if (result && (!best || result.writeTime > best.writeTime)) {
|
|
@@ -77,13 +93,13 @@ export async function readBucketInternal(account: string, bucketName: string, co
|
|
|
77
93
|
return best;
|
|
78
94
|
}
|
|
79
95
|
|
|
80
|
-
|
|
96
|
+
function aggregateArchivesConfig(bucketStores: BlobStore[], routing: RemoteConfig | undefined): ArchivesConfig {
|
|
81
97
|
let index = { fileCount: 0, byteCount: 0 };
|
|
82
98
|
let indexSources: { debugName: string; fileCount: number; byteCount: number }[] = [];
|
|
83
99
|
let syncing: SyncActivity[] = [];
|
|
84
100
|
let readerDiskLimit: number | undefined;
|
|
85
|
-
for (let
|
|
86
|
-
let progress =
|
|
101
|
+
for (let store of bucketStores) {
|
|
102
|
+
let progress = store.getSyncProgress();
|
|
87
103
|
index.fileCount += progress.index.fileCount;
|
|
88
104
|
index.byteCount += progress.index.byteCount;
|
|
89
105
|
indexSources.push(...progress.sources);
|
|
@@ -93,7 +109,7 @@ export function getBucketConfig(bucket: BucketState): ArchivesConfig {
|
|
|
93
109
|
return {
|
|
94
110
|
// Every store is index-backed, so the change feed is always native
|
|
95
111
|
supportsChangesAfter: true,
|
|
96
|
-
remoteConfig:
|
|
112
|
+
remoteConfig: routing,
|
|
97
113
|
index,
|
|
98
114
|
indexSources,
|
|
99
115
|
readerDiskLimit,
|
|
@@ -101,8 +117,15 @@ export function getBucketConfig(bucket: BucketState): ArchivesConfig {
|
|
|
101
117
|
};
|
|
102
118
|
}
|
|
103
119
|
|
|
104
|
-
export async function
|
|
105
|
-
let
|
|
120
|
+
export async function getBucketArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
|
|
121
|
+
let bucketStores = await requireBucketStores(account, bucketName);
|
|
122
|
+
let routing = await readRoutingFromDisk(account, bucketName);
|
|
123
|
+
return aggregateArchivesConfig(bucketStores.map(x => x.store), routing);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function bucketSyncStatus(account: string, bucketName: string): Promise<ArchivesSyncStatus> {
|
|
127
|
+
let bucketStores = await requireBucketStores(account, bucketName);
|
|
128
|
+
let statuses = await Promise.all(bucketStores.map(s => s.store.getSyncStatus()));
|
|
106
129
|
return {
|
|
107
130
|
allScansComplete: statuses.every(x => x.allScansComplete),
|
|
108
131
|
indexSize: statuses.reduce((sum, x) => sum + x.indexSize, 0),
|
|
@@ -110,8 +133,9 @@ export async function bucketSyncStatus(bucket: BucketState): Promise<ArchivesSyn
|
|
|
110
133
|
};
|
|
111
134
|
}
|
|
112
135
|
|
|
113
|
-
export async function debugBucketIndexTotals(
|
|
114
|
-
let
|
|
136
|
+
export async function debugBucketIndexTotals(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] }> {
|
|
137
|
+
let bucketStores = await requireBucketStores(account, bucketName);
|
|
138
|
+
let totals = await Promise.all(bucketStores.map(s => s.store.computeIndexTotals()));
|
|
115
139
|
return {
|
|
116
140
|
fileCount: totals.reduce((sum, x) => sum + x.fileCount, 0),
|
|
117
141
|
byteCount: totals.reduce((sum, x) => sum + x.byteCount, 0),
|
|
@@ -119,15 +143,6 @@ export async function debugBucketIndexTotals(bucket: BucketState): Promise<{ fil
|
|
|
119
143
|
};
|
|
120
144
|
}
|
|
121
145
|
|
|
122
|
-
const buckets = new Map<string, Promise<BucketState | undefined>>();
|
|
123
|
-
// The resolved ones, for the code that walks buckets rather than asking for one (see getBucketStates)
|
|
124
|
-
const loadedBuckets = new Map<string, BucketState>();
|
|
125
|
-
|
|
126
|
-
/** 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. */
|
|
127
|
-
export function getBucketStates(): BucketState[] {
|
|
128
|
-
return [...loadedBuckets.values()];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
146
|
const resolvedSourceArchives = new Map<string, IArchives>();
|
|
132
147
|
/** 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. */
|
|
133
148
|
export function resolveSourceArchives(url: string): IArchives {
|
|
@@ -143,91 +158,6 @@ export function resolveSourceArchives(url: string): IArchives {
|
|
|
143
158
|
return archives;
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
/** What an index entry records as the holder of its bytes (see ArchivesSource.url), so it must name the endpoint FOREVER. An intermediate is a switchover's temporary alternate port onto another source, and that port is gone for good once its window passes - so it is recorded as the source it was split out of, which holds the same bucket and outlives it. */
|
|
147
|
-
function sourcePersistentUrl(sourceConfig: SourceConfig | undefined, folder: string): string {
|
|
148
|
-
if (!sourceConfig) return folder;
|
|
149
|
-
return sourceConfig.intermediate || sourceConfig.url;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** The fast-write policy this store writes under: its own configuration decides it (fast/writeDelay are OUR settings, not the peer's), and each source then holds writes for as long as sourceWriteDelay says. */
|
|
153
|
-
/**
|
|
154
|
-
* Every store this process runs, by folder - which IS the store's identity: account, name, bucket.
|
|
155
|
-
* A store is made once, configures itself from the routing config it holds, and stays; nothing here
|
|
156
|
-
* decides what it is, only that it exists.
|
|
157
|
-
*/
|
|
158
|
-
const stores = new Map<string, BlobStore>();
|
|
159
|
-
|
|
160
|
-
export function getStore(account: string, bucketName: string, name: string): BlobStore {
|
|
161
|
-
// The one place a name becomes a folder, so the one place it is validated (it can come straight off the wire)
|
|
162
|
-
assertValidSourceName(name);
|
|
163
|
-
let folder = getBucketFolder(name, account, bucketName);
|
|
164
|
-
let existing = stores.get(folder);
|
|
165
|
-
if (existing) return existing;
|
|
166
|
-
console.log(`Opening store ${JSON.stringify(name)} of bucket ${account}/${bucketName} (folder ${folder}) - created on this first use if it never existed`);
|
|
167
|
-
let store = new BlobStore(folder, name, {
|
|
168
|
-
// Which entries in ITS routing config are this server's copy of it
|
|
169
|
-
isSelf: source => isSelfSource(source, account, bucketName),
|
|
170
|
-
createSource: config => createStoreSource({ sourceConfig: config.sourceConfig, folder, writeDelay: config.writeDelay }),
|
|
171
|
-
applySource: (source, sourceConfig, writeDelay) => applySourceConfig(source, sourceConfig, writeDelay),
|
|
172
|
-
onWriteCounted: (kind, bytes) => countBucketWrite(`${account}/${bucketName}`, kind, bytes),
|
|
173
|
-
resolveSourceUrl: resolveSourceArchives,
|
|
174
|
-
});
|
|
175
|
-
stores.set(folder, store);
|
|
176
|
-
// Lazy init would leave a store nothing has touched sitting inert - no index loaded, no config applied, nothing synchronizing - while its folder is full of files
|
|
177
|
-
void store.init().catch((e: Error) => console.error(`Initializing store ${JSON.stringify(name)} of bucket ${account}/${bucketName} failed: ${e.stack ?? e}`));
|
|
178
|
-
return store;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** The bucket as this server has it: the stores whose folders exist, plus whatever the newest of their configs says about us. Names come from the DISK, not from the config - a store we hold keeps being served whether or not the current config still mentions it. */
|
|
182
|
-
function buildBucket(account: string, bucketName: string, names: string[], routing: RemoteConfig | undefined): BucketState {
|
|
183
|
-
let plan = routing && planSelfStores(account, bucketName, routing) || undefined;
|
|
184
|
-
let entriesByName = new Map<string, HostedConfig[]>();
|
|
185
|
-
for (let selfStore of plan?.stores || []) {
|
|
186
|
-
entriesByName.set(selfStore.name, selfStore.entries);
|
|
187
|
-
}
|
|
188
|
-
// Each store reads its own copy of the config and decides what it is - see BlobStore.applyRoutingConfig. Nothing here configures anything.
|
|
189
|
-
let stores: LoadedStore[] = names.map(name => ({
|
|
190
|
-
name,
|
|
191
|
-
entries: entriesByName.get(name) || [],
|
|
192
|
-
folder: getBucketFolder(name, account, bucketName),
|
|
193
|
-
store: getStore(account, bucketName, name),
|
|
194
|
-
}));
|
|
195
|
-
let loaded: BucketState = { account, bucketName, routing, selfEntries: plan?.selfEntries || [], self: plan?.self, stores };
|
|
196
|
-
loadedBuckets.set(`${account}/${bucketName}`, loaded);
|
|
197
|
-
scheduleBoundaryWork(loaded);
|
|
198
|
-
return loaded;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// A bucket exists here when it has at least one store folder - the disk IS the record. Its config is the newest copy among those stores; with none at all it has no config yet, which is a perfectly good state for a store nobody has configured.
|
|
202
|
-
async function loadBucket(account: string, bucketName: string): Promise<BucketState | undefined> {
|
|
203
|
-
let folders = await listBucketStoreFolders(account, bucketName);
|
|
204
|
-
if (!folders.length) return undefined;
|
|
205
|
-
let routing = await readRoutingFromDisk(account, bucketName);
|
|
206
|
-
return buildBucket(account, bucketName, folders.map(x => x.name), routing);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function getLoadedBucket(account: string, bucketName: string): Promise<BucketState | undefined> {
|
|
210
|
-
let key = `${account}/${bucketName}`;
|
|
211
|
-
return buckets.get(key) || reloadBucket(account, bucketName);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** 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. */
|
|
215
|
-
export async function reloadBucket(account: string, bucketName: string): Promise<BucketState | undefined> {
|
|
216
|
-
let key = `${account}/${bucketName}`;
|
|
217
|
-
let loaded = loadBucket(account, bucketName);
|
|
218
|
-
buckets.set(key, loaded);
|
|
219
|
-
void loaded.then(bucket => {
|
|
220
|
-
if (!bucket && buckets.get(key) === loaded) {
|
|
221
|
-
buckets.delete(key);
|
|
222
|
-
}
|
|
223
|
-
}, () => {
|
|
224
|
-
if (buckets.get(key) === loaded) {
|
|
225
|
-
buckets.delete(key);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
return await loaded;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
161
|
/**
|
|
232
162
|
* Writing the routing config is a write like any other: it goes into a store, and the store applies
|
|
233
163
|
* it to itself and lets its peers pull it. The only thing that happens here is picking WHICH store,
|
|
@@ -241,17 +171,15 @@ export async function writeRoutingConfig(account: string, bucketName: string, na
|
|
|
241
171
|
assertWritesAllowed();
|
|
242
172
|
let store = getStore(account, bucketName, name);
|
|
243
173
|
let incoming = parseRoutingData(data);
|
|
244
|
-
let current =
|
|
174
|
+
let current = await readRoutingFromDisk(account, bucketName);
|
|
245
175
|
let stored = reinjectIntermediates(current, incoming);
|
|
246
176
|
await store.set({ path: ROUTING_FILE, data: Buffer.from(serializeRemoteConfig(stored)), lastModified: config?.lastModified });
|
|
247
|
-
// The write may have CREATED the store (a new name gets a new folder), and the loaded bucket is a snapshot of the disk - so it is re-read, or every data write naming the new store is rejected until a restart
|
|
248
|
-
await reloadBucket(account, bucketName);
|
|
249
177
|
broadcastRoutingChanged();
|
|
250
178
|
}
|
|
251
179
|
|
|
252
|
-
/** Which buckets this process currently has
|
|
180
|
+
/** 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. */
|
|
253
181
|
export function getActiveBucketKeys(): { account: string; bucketName: string }[] {
|
|
254
|
-
return [...
|
|
182
|
+
return [...activeStores.keys()].map(key => {
|
|
255
183
|
let slash = key.indexOf("/");
|
|
256
184
|
return { account: key.slice(0, slash), bucketName: key.slice(slash + 1) };
|
|
257
185
|
});
|
|
@@ -272,80 +200,46 @@ export type ServerBucketInfo = {
|
|
|
272
200
|
|
|
273
201
|
export type ActiveBucketInfo = {
|
|
274
202
|
folder: string;
|
|
275
|
-
/** The routing config the
|
|
203
|
+
/** The bucket's routing config, the newest copy among its stores. Absent when none of them has one yet. */
|
|
276
204
|
routing?: RemoteConfig;
|
|
277
|
-
/** Our own entries in that config, and their summarized current role (routes union + flags) */
|
|
278
|
-
selfEntries: HostedConfig[];
|
|
279
|
-
self?: SelfSummary;
|
|
280
205
|
config: ArchivesConfig;
|
|
281
206
|
};
|
|
282
207
|
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
// One of its stores' folders - a bucket no longer has a folder of its own, it has one per store
|
|
286
|
-
folder: loaded.stores[0]?.folder || "",
|
|
287
|
-
routing: loaded.routing,
|
|
288
|
-
selfEntries: loaded.selfEntries,
|
|
289
|
-
self: loaded.self,
|
|
290
|
-
config: getBucketConfig(loaded),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** The live in-memory state of ONE bucket, answered without touching the disk (no routing file read, no statfs, no stored write stats). Returns an error string when the bucket is not loaded here, which is the normal state for a bucket nothing has accessed since startup. */
|
|
208
|
+
/** 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. */
|
|
295
209
|
export async function debugGetActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
296
210
|
let key = `${account}/${bucketName}`;
|
|
297
|
-
let
|
|
298
|
-
if (!
|
|
299
|
-
return `Bucket ${key} is not
|
|
300
|
-
}
|
|
301
|
-
let loaded: BucketState | undefined;
|
|
302
|
-
try {
|
|
303
|
-
loaded = await loadedPromise;
|
|
304
|
-
} catch (e) {
|
|
305
|
-
return `Bucket ${key} failed to load on this server: ${String((e as Error).stack ?? e).slice(0, 500)}`;
|
|
306
|
-
}
|
|
307
|
-
if (!loaded) {
|
|
308
|
-
return `Bucket ${key} does not exist on this server`;
|
|
211
|
+
let opened = activeStores.get(key);
|
|
212
|
+
if (!opened || !opened.size) {
|
|
213
|
+
return `Bucket ${key} is not active on this server (a bucket activates on first access)`;
|
|
309
214
|
}
|
|
310
|
-
|
|
215
|
+
let bucketStores = [...opened.values()];
|
|
216
|
+
let routing = await readRoutingFromDisk(account, bucketName);
|
|
217
|
+
return { folder: bucketStores[0].folder, routing, config: aggregateArchivesConfig(bucketStores, routing) };
|
|
311
218
|
}
|
|
312
219
|
|
|
313
|
-
/** Loads a bucket that exists on this server's disk into memory, which starts
|
|
220
|
+
/** 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. */
|
|
314
221
|
export async function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
315
222
|
let key = `${account}/${bucketName}`;
|
|
316
|
-
let
|
|
317
|
-
let
|
|
318
|
-
|
|
319
|
-
loaded = await getLoadedBucket(account, bucketName);
|
|
320
|
-
} catch (e) {
|
|
321
|
-
return `Bucket ${key} failed to load on this server: ${String((e as Error).stack ?? e).slice(0, 500)}`;
|
|
322
|
-
}
|
|
323
|
-
if (!loaded) {
|
|
223
|
+
let wasActive = activeStores.has(key);
|
|
224
|
+
let bucketStores = await getBucketStores(account, bucketName);
|
|
225
|
+
if (!bucketStores.length) {
|
|
324
226
|
return `Bucket ${key} does not exist on this server (it has no store folders under ${getBucketFolder("<name>", account, bucketName)})`;
|
|
325
227
|
}
|
|
326
228
|
// Wait for the indexes to load, so the totals we return are the real ones rather than zeroes from stores that have not read their index yet. The source scans keep running in the background.
|
|
327
|
-
for (let s of
|
|
229
|
+
for (let s of bucketStores) {
|
|
328
230
|
await s.store.init();
|
|
329
231
|
}
|
|
330
|
-
if (!
|
|
331
|
-
console.log(`Activated bucket ${key} on request:
|
|
232
|
+
if (!wasActive) {
|
|
233
|
+
console.log(`Activated bucket ${key} on request: its ${bucketStores.length} store(s) are now loaded and synchronizing`);
|
|
332
234
|
}
|
|
333
|
-
|
|
235
|
+
let routing = await readRoutingFromDisk(account, bucketName);
|
|
236
|
+
return { folder: bucketStores[0].store.folder, routing, config: aggregateArchivesConfig(bucketStores.map(x => x.store), routing) };
|
|
334
237
|
}
|
|
335
238
|
|
|
336
239
|
export async function debugListAccountBuckets(account: string): Promise<ServerBucketInfo[]> {
|
|
337
240
|
let start = Date.now();
|
|
338
|
-
let timings = new Map<string, number>();
|
|
339
|
-
async function timed<T>(name: string, run: () => Promise<T>): Promise<T> {
|
|
340
|
-
let began = Date.now();
|
|
341
|
-
try {
|
|
342
|
-
return await run();
|
|
343
|
-
} finally {
|
|
344
|
-
timings.set(name, (timings.get(name) || 0) + (Date.now() - began));
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
241
|
// The disk is the record: a bucket exists here when it has at least one store folder, and several names for one bucket are still one bucket
|
|
348
|
-
let folders = await
|
|
242
|
+
let folders = await listAccountStoreFolders(account);
|
|
349
243
|
let byBucket = new Map<string, string>();
|
|
350
244
|
for (let store of folders) {
|
|
351
245
|
if (byBucket.has(store.bucketName)) continue;
|
|
@@ -356,83 +250,27 @@ export async function debugListAccountBuckets(account: string): Promise<ServerBu
|
|
|
356
250
|
return await Promise.all(names.map(async bucketName => {
|
|
357
251
|
let key = `${account}/${bucketName}`;
|
|
358
252
|
let folder = byBucket.get(bucketName) || "";
|
|
359
|
-
let base: ServerBucketInfo = { bucketName, active:
|
|
360
|
-
base.disk = await
|
|
253
|
+
let base: ServerBucketInfo = { bucketName, active: activeStores.has(key), folder };
|
|
254
|
+
base.disk = await getDiskInfo(folder).catch((e: Error) => {
|
|
361
255
|
base.diskError = String(e.stack ?? e).slice(0, 500);
|
|
362
256
|
return undefined;
|
|
363
257
|
});
|
|
364
258
|
base.writeStats = getBucketWriteStats(key);
|
|
365
|
-
const loadedPromise = buckets.get(key);
|
|
366
|
-
if (loadedPromise) {
|
|
367
|
-
try {
|
|
368
|
-
let loaded = await timed("awaitLoaded", () => loadedPromise);
|
|
369
|
-
if (loaded) {
|
|
370
|
-
return { ...base, active: true, config: getBucketConfig(loaded) };
|
|
371
|
-
}
|
|
372
|
-
} catch (e) {
|
|
373
|
-
return { ...base, active: true, error: String((e as Error).stack ?? e).slice(0, 500) };
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
259
|
try {
|
|
377
|
-
|
|
378
|
-
if (
|
|
260
|
+
let routing = await readRoutingFromDisk(account, bucketName);
|
|
261
|
+
if (base.active) {
|
|
262
|
+
let opened = [...(activeStores.get(key)?.values() || [])];
|
|
263
|
+
return { ...base, config: aggregateArchivesConfig(opened, routing) };
|
|
264
|
+
}
|
|
265
|
+
if (!routing) {
|
|
379
266
|
return { ...base, error: `No routing file (${ROUTING_FILE}) in any of its stores` };
|
|
380
267
|
}
|
|
381
|
-
return { ...base, config: { remoteConfig:
|
|
268
|
+
return { ...base, config: { remoteConfig: routing } };
|
|
382
269
|
} catch (e) {
|
|
383
270
|
return { ...base, error: String((e as Error).stack ?? e).slice(0, 500) };
|
|
384
271
|
}
|
|
385
272
|
}));
|
|
386
273
|
} finally {
|
|
387
|
-
|
|
388
|
-
let parts = [...timings].map(([name, ms]) => `${name} ${ms}ms`).join(", ");
|
|
389
|
-
console.log(`debugListAccountBuckets(${account}) took ${Date.now() - start}ms for ${names.length} buckets: ${parts}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export type BucketWriteStats = {
|
|
394
|
-
/** Every set call the bucket accepted */
|
|
395
|
-
originalWrites: number;
|
|
396
|
-
originalBytes: number;
|
|
397
|
-
/** 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). */
|
|
398
|
-
flushedWrites: number;
|
|
399
|
-
flushedBytes: number;
|
|
400
|
-
};
|
|
401
|
-
function emptyWriteStats(): BucketWriteStats {
|
|
402
|
-
return { originalWrites: 0, originalBytes: 0, flushedWrites: 0, flushedBytes: 0 };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// In memory only: totals since this process started (or the last clearWriteStats). Persisting them to disk was more machinery than the numbers were worth.
|
|
406
|
-
const writeStats = new Map<string, BucketWriteStats>();
|
|
407
|
-
|
|
408
|
-
function countBucketWrite(key: string, kind: "original" | "flushed", bytes: number): void {
|
|
409
|
-
let stats = writeStats.get(key);
|
|
410
|
-
if (!stats) {
|
|
411
|
-
stats = emptyWriteStats();
|
|
412
|
-
writeStats.set(key, stats);
|
|
413
|
-
}
|
|
414
|
-
if (kind === "original") {
|
|
415
|
-
stats.originalWrites++;
|
|
416
|
-
stats.originalBytes += bytes;
|
|
417
|
-
} else {
|
|
418
|
-
stats.flushedWrites++;
|
|
419
|
-
stats.flushedBytes += bytes;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function getBucketWriteStats(key: string): BucketWriteStats {
|
|
424
|
-
return writeStats.get(key) || emptyWriteStats();
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/** Zeroes the write statistics of every bucket in the account. */
|
|
428
|
-
export function debugClearAccountWriteStats(account: string): number {
|
|
429
|
-
let prefix = `${account}/`;
|
|
430
|
-
let cleared = 0;
|
|
431
|
-
for (let key of [...writeStats.keys()]) {
|
|
432
|
-
if (!key.startsWith(prefix)) continue;
|
|
433
|
-
writeStats.delete(key);
|
|
434
|
-
cleared++;
|
|
274
|
+
console.log(`debugListAccountBuckets(${account}) took ${Date.now() - start}ms for ${names.length} buckets`);
|
|
435
275
|
}
|
|
436
|
-
console.log(`Cleared the write statistics of ${cleared} buckets in account ${account}`);
|
|
437
|
-
return cleared;
|
|
438
276
|
}
|