sliftutils 1.7.67 → 1.7.68
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/dist/test.ts.cache +3 -3
- package/index.d.ts +39 -2
- package/package.json +1 -1
- package/storage/remoteStorage/accessStats.d.ts +25 -0
- package/storage/remoteStorage/accessStats.ts +86 -0
- package/storage/remoteStorage/dist/storageController.ts.cache +50 -13
- package/storage/remoteStorage/dist/storageServerState.ts.cache +24 -73
- package/storage/remoteStorage/storageController.d.ts +8 -0
- package/storage/remoteStorage/storageController.ts +47 -9
- package/storage/remoteStorage/storageServerState.d.ts +2 -2
- package/storage/remoteStorage/storageServerState.ts +21 -68
- package/test.ts +1 -1
- package/treeSummary.d.ts +31 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig } from "../IArchives";
|
|
4
4
|
import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
|
|
5
|
+
import { AccessTotals, AccessSummaryState } from "./accessStats";
|
|
6
|
+
import type { SummaryEntry } from "../../treeSummary";
|
|
5
7
|
export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
|
|
6
8
|
export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
|
|
7
9
|
export declare const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
|
|
@@ -88,6 +90,12 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
88
90
|
clearWriteStats: (account: string) => Promise<{
|
|
89
91
|
clearedBuckets: number;
|
|
90
92
|
}>;
|
|
93
|
+
getAccessStats: (account: string) => Promise<AccessTotals>;
|
|
94
|
+
getAccessSummaries: (account: string, config: {
|
|
95
|
+
operation: string;
|
|
96
|
+
maxCount: number;
|
|
97
|
+
weightBySize?: boolean;
|
|
98
|
+
}) => Promise<SummaryEntry<AccessSummaryState>[]>;
|
|
91
99
|
getIndexInfo: (account: string, bucketName: string) => Promise<{
|
|
92
100
|
fileCount: number;
|
|
93
101
|
byteCount: number;
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
getActiveBucket, activateBucket, ActiveBucketInfo, getActiveBucketKeys,
|
|
16
16
|
} from "./storageServerState";
|
|
17
17
|
import { StorageClientController } from "./storageClientController";
|
|
18
|
+
import { trackAccess, getAccessTotals, readAccessSummaries, clearAccountAccessStats, AccessTotals, AccessSummaryState } from "./accessStats";
|
|
19
|
+
import type { SummaryEntry } from "../../treeSummary";
|
|
18
20
|
|
|
19
21
|
export const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
|
|
20
22
|
export const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
|
|
@@ -292,37 +294,57 @@ class RemoteStorageControllerBase {
|
|
|
292
294
|
async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }, internal?: boolean): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
293
295
|
assertValidPath(path);
|
|
294
296
|
let bucket = await getBucket(account, bucketName);
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
297
|
+
let result: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
298
|
+
if (bucket) {
|
|
299
|
+
if (internal && bucket.store.getInternal2) {
|
|
300
|
+
result = await bucket.store.getInternal2(path, { range });
|
|
301
|
+
} else {
|
|
302
|
+
result = await bucket.store.get2(path, { range });
|
|
303
|
+
}
|
|
298
304
|
}
|
|
299
|
-
|
|
305
|
+
trackAccess({ account, operation: "get", path: `${bucketName}/${path}`, size: result && result.data.length || 0 });
|
|
306
|
+
return result;
|
|
300
307
|
}
|
|
301
308
|
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean): Promise<void> {
|
|
302
309
|
assertValidName(bucketName, "bucket name");
|
|
303
310
|
assertValidPath(path);
|
|
311
|
+
trackAccess({ account, operation: "set", path: `${bucketName}/${path}`, size: data.length });
|
|
304
312
|
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable, internal });
|
|
305
313
|
}
|
|
306
314
|
async del(account: string, bucketName: string, path: string): Promise<void> {
|
|
307
315
|
assertValidName(bucketName, "bucket name");
|
|
308
316
|
assertValidPath(path);
|
|
317
|
+
trackAccess({ account, operation: "del", path: `${bucketName}/${path}` });
|
|
309
318
|
await deleteBucketFile(account, bucketName, path);
|
|
310
319
|
}
|
|
311
320
|
async getInfo(account: string, bucketName: string, path: string, includeTombstones?: boolean): Promise<{ writeTime: number; size: number } | undefined> {
|
|
312
321
|
assertValidPath(path);
|
|
322
|
+
trackAccess({ account, operation: "getInfo", path: `${bucketName}/${path}` });
|
|
313
323
|
let bucket = await getBucket(account, bucketName);
|
|
314
324
|
if (!bucket) return undefined;
|
|
315
325
|
return await bucket.store.getInfo(path, { includeTombstones });
|
|
316
326
|
}
|
|
317
327
|
async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
|
|
318
328
|
let bucket = await getBucket(account, bucketName);
|
|
319
|
-
|
|
320
|
-
|
|
329
|
+
let results = bucket && await bucket.store.findInfo(prefix, config) || [];
|
|
330
|
+
// The paths are only known once the results are in; an empty result still counts as one access at the prefix.
|
|
331
|
+
if (results.length) {
|
|
332
|
+
for (let info of results) {
|
|
333
|
+
trackAccess({ account, operation: "findInfo", path: `${bucketName}/${info.path}` });
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
trackAccess({ account, operation: "findInfo", path: `${bucketName}/${prefix}` });
|
|
337
|
+
}
|
|
338
|
+
return results;
|
|
321
339
|
}
|
|
322
340
|
async getChangesAfter2(account: string, bucketName: string, config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
|
|
323
341
|
let bucket = await getBucket(account, bucketName);
|
|
324
342
|
if (!bucket) return [];
|
|
325
|
-
|
|
343
|
+
let results = await bucket.store.getChangesAfter2(config);
|
|
344
|
+
for (let info of results) {
|
|
345
|
+
trackAccess({ account, operation: "getChangesAfter", path: `${bucketName}/${info.path}` });
|
|
346
|
+
}
|
|
347
|
+
return results;
|
|
326
348
|
}
|
|
327
349
|
async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
|
|
328
350
|
let bucket = await getBucket(account, bucketName);
|
|
@@ -348,10 +370,21 @@ class RemoteStorageControllerBase {
|
|
|
348
370
|
assertValidName(bucketName, "bucket name");
|
|
349
371
|
return await activateBucket(account, bucketName);
|
|
350
372
|
}
|
|
351
|
-
/** Zeroes the write statistics listBuckets reports, for every bucket in the account. */
|
|
373
|
+
/** Zeroes the write statistics listBuckets reports and the in-memory access statistics, for every bucket in the account. */
|
|
352
374
|
async clearWriteStats(account: string): Promise<{ clearedBuckets: number }> {
|
|
353
375
|
assertValidName(account, "account");
|
|
354
|
-
|
|
376
|
+
clearAccountAccessStats(account);
|
|
377
|
+
return { clearedBuckets: clearAccountWriteStats(account) };
|
|
378
|
+
}
|
|
379
|
+
/** In-memory totals per operation type since startup (or the last clearWriteStats). */
|
|
380
|
+
async getAccessStats(account: string): Promise<AccessTotals> {
|
|
381
|
+
assertValidName(account, "account");
|
|
382
|
+
return getAccessTotals(account);
|
|
383
|
+
}
|
|
384
|
+
/** A path breakdown of one operation's accesses (operation names come from getAccessStats). maxCount is passed straight to TreeSummary.getSummaries. weightBySize is ignored for count-only operations, which return their count breakdown. */
|
|
385
|
+
async getAccessSummaries(account: string, config: { operation: string; maxCount: number; weightBySize?: boolean }): Promise<SummaryEntry<AccessSummaryState>[]> {
|
|
386
|
+
assertValidName(account, "account");
|
|
387
|
+
return readAccessSummaries({ account, ...config });
|
|
355
388
|
}
|
|
356
389
|
async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
|
|
357
390
|
let bucket = await getBucket(account, bucketName);
|
|
@@ -383,6 +416,7 @@ class RemoteStorageControllerBase {
|
|
|
383
416
|
assertWritesAllowed();
|
|
384
417
|
let info = largeUploadInfo.get(uploadId);
|
|
385
418
|
if (!info) throw new Error(`Unknown large upload ${uploadId}`);
|
|
419
|
+
trackAccess({ account: info.account, operation: "uploadPart", path: `${info.bucketName}/${info.path}`, size: data.length });
|
|
386
420
|
let bucket = await getBucket(info.account, info.bucketName);
|
|
387
421
|
if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
|
|
388
422
|
await bucket.store.appendLargeUpload(uploadId, Buffer.from(data));
|
|
@@ -437,6 +471,7 @@ class RemoteStorageControllerBase {
|
|
|
437
471
|
}
|
|
438
472
|
let info = await bucket.store.getInfo(filePath);
|
|
439
473
|
if (!info || !info.size) {
|
|
474
|
+
trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: 0 });
|
|
440
475
|
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
441
476
|
}
|
|
442
477
|
let ext = filePath.split(".").pop() || "";
|
|
@@ -477,6 +512,7 @@ class RemoteStorageControllerBase {
|
|
|
477
512
|
}
|
|
478
513
|
}
|
|
479
514
|
let result = await bucket.store.get2(filePath, { range });
|
|
515
|
+
trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: result && result.data.length || 0 });
|
|
480
516
|
if (!result) {
|
|
481
517
|
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
482
518
|
}
|
|
@@ -532,6 +568,8 @@ export const RemoteStorageController = SocketFunction.register(
|
|
|
532
568
|
getActiveBucket: { hooks: [accountAccess] },
|
|
533
569
|
activateBucket: { hooks: [accountAccess] },
|
|
534
570
|
clearWriteStats: { hooks: [accountAccess] },
|
|
571
|
+
getAccessStats: { hooks: [accountAccess] },
|
|
572
|
+
getAccessSummaries: { hooks: [accountAccess] },
|
|
535
573
|
getSyncStatus: { hooks: [accountAccess] },
|
|
536
574
|
startLargeFile: { hooks: [accountAccess] },
|
|
537
575
|
uploadPart: { hooks: [uploadAccess] },
|
|
@@ -28,8 +28,8 @@ export type BucketWriteStats = {
|
|
|
28
28
|
flushedWrites: number;
|
|
29
29
|
flushedBytes: number;
|
|
30
30
|
};
|
|
31
|
-
/** Zeroes the write statistics of every bucket in the account
|
|
32
|
-
export declare function clearAccountWriteStats(account: string):
|
|
31
|
+
/** Zeroes the write statistics of every bucket in the account. */
|
|
32
|
+
export declare function clearAccountWriteStats(account: string): number;
|
|
33
33
|
export declare function setTrustedMachines(config: {
|
|
34
34
|
account: string;
|
|
35
35
|
machineIds: string[];
|
|
@@ -98,78 +98,36 @@ export type BucketWriteStats = {
|
|
|
98
98
|
function emptyWriteStats(): BucketWriteStats {
|
|
99
99
|
return { originalWrites: 0, originalBytes: 0, flushedWrites: 0, flushedBytes: 0 };
|
|
100
100
|
}
|
|
101
|
-
function addWriteStats(a: BucketWriteStats, b: BucketWriteStats): BucketWriteStats {
|
|
102
|
-
return {
|
|
103
|
-
originalWrites: a.originalWrites + b.originalWrites,
|
|
104
|
-
originalBytes: a.originalBytes + b.originalBytes,
|
|
105
|
-
flushedWrites: a.flushedWrites + b.flushedWrites,
|
|
106
|
-
flushedBytes: a.flushedBytes + b.flushedBytes,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function getWriteStatsStorage(): Promise<IStorage<BucketWriteStats>> {
|
|
110
|
-
return getSystemStorage<BucketWriteStats>("writeStats");
|
|
111
|
-
}
|
|
112
101
|
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
async function flushWriteStats(): Promise<void> {
|
|
117
|
-
if (!writeStatDeltas.size) return;
|
|
118
|
-
let pending = [...writeStatDeltas];
|
|
119
|
-
writeStatDeltas.clear();
|
|
120
|
-
let storage = await getWriteStatsStorage();
|
|
121
|
-
for (let [key, delta] of pending) {
|
|
122
|
-
try {
|
|
123
|
-
await storage.set(key, addWriteStats(await storage.get(key) || emptyWriteStats(), delta));
|
|
124
|
-
} catch (e) {
|
|
125
|
-
// Put the delta back, so a failed flush loses nothing
|
|
126
|
-
writeStatDeltas.set(key, addWriteStats(writeStatDeltas.get(key) || emptyWriteStats(), delta));
|
|
127
|
-
console.error(`Flushing write stats for bucket ${key} failed: ${(e as Error).stack ?? e}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const startWriteStatsFlushing = lazy(() => {
|
|
133
|
-
runInfinitePoll(WRITE_STATS_FLUSH_INTERVAL, flushWriteStats);
|
|
134
|
-
});
|
|
102
|
+
// In memory only: totals since this process started (or the last clearWriteStats). Persisting them to disk was more machinery than the numbers were worth.
|
|
103
|
+
const writeStats = new Map<string, BucketWriteStats>();
|
|
135
104
|
|
|
136
105
|
function countBucketWrite(key: string, kind: "original" | "flushed", bytes: number): void {
|
|
137
|
-
let
|
|
138
|
-
if (!
|
|
139
|
-
|
|
140
|
-
|
|
106
|
+
let stats = writeStats.get(key);
|
|
107
|
+
if (!stats) {
|
|
108
|
+
stats = emptyWriteStats();
|
|
109
|
+
writeStats.set(key, stats);
|
|
141
110
|
}
|
|
142
111
|
if (kind === "original") {
|
|
143
|
-
|
|
144
|
-
|
|
112
|
+
stats.originalWrites++;
|
|
113
|
+
stats.originalBytes += bytes;
|
|
145
114
|
} else {
|
|
146
|
-
|
|
147
|
-
|
|
115
|
+
stats.flushedWrites++;
|
|
116
|
+
stats.flushedBytes += bytes;
|
|
148
117
|
}
|
|
149
|
-
startWriteStatsFlushing();
|
|
150
118
|
}
|
|
151
119
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let stored = await storage.get(key) || emptyWriteStats();
|
|
155
|
-
let delta = writeStatDeltas.get(key);
|
|
156
|
-
return delta && addWriteStats(stored, delta) || stored;
|
|
120
|
+
function getBucketWriteStats(key: string): BucketWriteStats {
|
|
121
|
+
return writeStats.get(key) || emptyWriteStats();
|
|
157
122
|
}
|
|
158
123
|
|
|
159
|
-
/** Zeroes the write statistics of every bucket in the account
|
|
160
|
-
export
|
|
161
|
-
let storage = await getWriteStatsStorage();
|
|
124
|
+
/** Zeroes the write statistics of every bucket in the account. */
|
|
125
|
+
export function clearAccountWriteStats(account: string): number {
|
|
162
126
|
let prefix = `${account}/`;
|
|
163
127
|
let cleared = 0;
|
|
164
|
-
for (let key of
|
|
165
|
-
if (!key.startsWith(prefix)) continue;
|
|
166
|
-
writeStatDeltas.delete(key);
|
|
167
|
-
await storage.remove(key);
|
|
168
|
-
cleared++;
|
|
169
|
-
}
|
|
170
|
-
for (let key of [...writeStatDeltas.keys()]) {
|
|
128
|
+
for (let key of [...writeStats.keys()]) {
|
|
171
129
|
if (!key.startsWith(prefix)) continue;
|
|
172
|
-
|
|
130
|
+
writeStats.delete(key);
|
|
173
131
|
cleared++;
|
|
174
132
|
}
|
|
175
133
|
console.log(`Cleared the write statistics of ${cleared} buckets in account ${account}`);
|
|
@@ -211,7 +169,6 @@ const buckets = new Map<string, Promise<LoadedBucket | undefined>>();
|
|
|
211
169
|
|
|
212
170
|
const MAX_REBUILD_TIMER_DELAY = 2 ** 31 - 1;
|
|
213
171
|
const REBUILD_BOUNDARY_BUFFER = 1000;
|
|
214
|
-
const WRITE_STATS_FLUSH_INTERVAL = 5 * 60 * 1000;
|
|
215
172
|
|
|
216
173
|
function getBucketFolder(account: string, bucketName: string): string {
|
|
217
174
|
return path.join(getStorageServerConfig().folder, "buckets2", account, bucketName);
|
|
@@ -1070,15 +1027,11 @@ export async function listAccountBuckets(account: string): Promise<ServerBucketI
|
|
|
1070
1027
|
let key = `${account}/${bucketName}`;
|
|
1071
1028
|
let folder = getBucketFolder(account, bucketName);
|
|
1072
1029
|
let base: ServerBucketInfo = { bucketName, active: false, folder };
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
timed("writeStats", () => getBucketWriteStats(key)),
|
|
1079
|
-
]);
|
|
1080
|
-
base.disk = disk;
|
|
1081
|
-
base.writeStats = writeStats;
|
|
1030
|
+
base.disk = await timed("statfs", () => getDiskInfo(folder)).catch((e: Error) => {
|
|
1031
|
+
base.diskError = String(e.stack ?? e).slice(0, 500);
|
|
1032
|
+
return undefined;
|
|
1033
|
+
});
|
|
1034
|
+
base.writeStats = getBucketWriteStats(key);
|
|
1082
1035
|
const loadedPromise = buckets.get(key);
|
|
1083
1036
|
if (loadedPromise) {
|
|
1084
1037
|
try {
|
package/test.ts
CHANGED
|
@@ -57,7 +57,7 @@ function summarize(files: FileInfo[], weightBySize: boolean) {
|
|
|
57
57
|
if (weightBySize) return summary.totalSize;
|
|
58
58
|
return summary.count;
|
|
59
59
|
},
|
|
60
|
-
expectedOutputCount:
|
|
60
|
+
expectedOutputCount: 100,
|
|
61
61
|
});
|
|
62
62
|
let addStart = performance.now();
|
|
63
63
|
for (let file of files) {
|
package/treeSummary.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type SummaryEntry<S> = {
|
|
2
|
+
path: string;
|
|
3
|
+
kind: "self" | "subtree" | "group" | "truncated";
|
|
4
|
+
summary: S;
|
|
5
|
+
weight: number;
|
|
6
|
+
};
|
|
7
|
+
export declare class TreeSummary<T, S> {
|
|
8
|
+
private config;
|
|
9
|
+
private root;
|
|
10
|
+
private nodeCount;
|
|
11
|
+
private maxTrackedNodes;
|
|
12
|
+
constructor(config: {
|
|
13
|
+
getPath: (value: T) => string;
|
|
14
|
+
createSummary: () => S;
|
|
15
|
+
addToSummary: (value: T, summary: S) => void;
|
|
16
|
+
mergeSummaries: (target: S, source: S) => void;
|
|
17
|
+
getWeight: (summary: S) => number;
|
|
18
|
+
expectedOutputCount?: number;
|
|
19
|
+
});
|
|
20
|
+
add(value: T): void;
|
|
21
|
+
private splitNode;
|
|
22
|
+
getTrackedNodeCount(): number;
|
|
23
|
+
getSummaries(maxCount: number): SummaryEntry<S>[];
|
|
24
|
+
private isRefinable;
|
|
25
|
+
private refine;
|
|
26
|
+
private splitParts;
|
|
27
|
+
private entryForParts;
|
|
28
|
+
private subtreeEntry;
|
|
29
|
+
private buildOutputSummary;
|
|
30
|
+
private prune;
|
|
31
|
+
}
|