querysub 0.605.0 → 0.607.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/-a-archives/archives2.ts +8 -3
- package/src/-a-archives/archivesJSONT.ts +12 -12
- package/src/4-deploy/edgeBootstrap.ts +8 -0
- package/src/4-deploy/edgeNodes.ts +71 -75
- package/src/deployManager/MachinesPage.tsx +1 -1
- package/src/deployManager/components/{RouteConfigView.tsx → storage/RouteConfigView.tsx} +9 -8
- package/src/deployManager/components/storage/StoragePage.tsx +151 -0
- package/src/deployManager/components/{Tag.tsx → storage/Tag.tsx} +1 -1
- package/src/deployManager/components/storage/bucketCells.tsx +170 -0
- package/src/deployManager/components/storage/bucketRows.ts +145 -0
- package/src/deployManager/components/storage/deployNotices.tsx +81 -0
- package/src/deployManager/components/storage/operationsView.tsx +217 -0
- package/src/deployManager/components/storage/sourceNames.tsx +43 -0
- package/src/deployManager/components/storage/storageController.ts +76 -0
- package/src/deployManager/components/storage/storageServers.ts +73 -0
- package/src/deployManager/machineSchema.ts +14 -11
- package/src/library-components/UsageBar.tsx +5 -2
- package/src/deployManager/components/StoragePage.tsx +0 -652
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { listServerBuckets, clearServerWriteStats, activateServerBucket, getServerActiveBucket } from "sliftutils/storage/remoteStorage/createArchives";
|
|
3
|
+
import type { ServerBucketInfo, ActiveBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
|
|
4
|
+
import { parseStorageUrl, authenticateStorage } from "sliftutils/storage/remoteStorage/ArchivesRemote";
|
|
5
|
+
import { RemoteStorageController, STORAGE_NOT_AUTHENTICATED } from "sliftutils/storage/remoteStorage/storageController";
|
|
6
|
+
import type { AccessTotals, AccessSummaryState } from "sliftutils/storage/remoteStorage/accessStats";
|
|
7
|
+
import type { SummaryEntry } from "sliftutils/treeSummary";
|
|
8
|
+
import { getSyncedController } from "../../../library-components/SyncedController";
|
|
9
|
+
import { assertIsManagementUser } from "../../../diagnostics/managementPages";
|
|
10
|
+
import { STORAGE_ACCOUNT } from "../../../-a-archives/archives2";
|
|
11
|
+
|
|
12
|
+
// The registered controller and its synced wrapper have to keep a stable identity, so this file is a full-refresh boundary rather than a hot-reload one (matching SyncedController itself).
|
|
13
|
+
module.hotreload = false;
|
|
14
|
+
|
|
15
|
+
/** The access-stats endpoints have no client wrapper in sliftutils (a pinned dependency), so we reconnect and authenticate the same way its own callServer does. */
|
|
16
|
+
async function callStorageServer<T>(url: string, run: (controller: typeof RemoteStorageController.nodes[string]) => Promise<T>): Promise<T> {
|
|
17
|
+
SocketFunction.ENABLE_CLIENT_MODE = true;
|
|
18
|
+
let parsed = parseStorageUrl(url);
|
|
19
|
+
let nodeId = SocketFunction.connect({ address: parsed.address, port: parsed.port });
|
|
20
|
+
let controller = RemoteStorageController.nodes[nodeId];
|
|
21
|
+
try {
|
|
22
|
+
return await run(controller);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (!String((e as Error).stack ?? e).includes(STORAGE_NOT_AUTHENTICATED)) throw e;
|
|
25
|
+
await authenticateStorage({ address: parsed.address, port: parsed.port, nodeId });
|
|
26
|
+
return await run(controller);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Every storage call is proxied through here: they authenticate with this machine's identity CA, which the browser has no access to. */
|
|
31
|
+
class StoragePageControllerBase {
|
|
32
|
+
/** One call per server, so each caches and refreshes on its own. */
|
|
33
|
+
public async getServerBuckets(url: string): Promise<ServerBucketInfo[]> {
|
|
34
|
+
return await listServerBuckets({ url, account: STORAGE_ACCOUNT });
|
|
35
|
+
}
|
|
36
|
+
public async activateBucket(url: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
37
|
+
return await activateServerBucket({ url, account: STORAGE_ACCOUNT, bucketName });
|
|
38
|
+
}
|
|
39
|
+
public async getActiveBucket(url: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
40
|
+
return await getServerActiveBucket({ url, account: STORAGE_ACCOUNT, bucketName });
|
|
41
|
+
}
|
|
42
|
+
public async clearWriteStats(url: string): Promise<{ clearedBuckets: number }> {
|
|
43
|
+
return await clearServerWriteStats({ url, account: STORAGE_ACCOUNT });
|
|
44
|
+
}
|
|
45
|
+
/** Per-operation totals since startup (or the last reset), aggregated across every bucket in the account. */
|
|
46
|
+
public async getAccessStats(url: string): Promise<AccessTotals> {
|
|
47
|
+
return await callStorageServer(url, controller => controller.getAccessStats({ account: STORAGE_ACCOUNT }));
|
|
48
|
+
}
|
|
49
|
+
/** The path breakdown of one operation. weightBySize orders it (and the totals) by bytes rather than call count. */
|
|
50
|
+
public async getAccessSummaries(url: string, operation: string, maxCount: number, weightBySize: boolean): Promise<SummaryEntry<AccessSummaryState>[]> {
|
|
51
|
+
return await callStorageServer(url, controller => controller.getAccessSummaries({ account: STORAGE_ACCOUNT, operation, maxCount, weightBySize }));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const StoragePageController = SocketFunction.register(
|
|
56
|
+
"StoragePageController-4f1c93ab-77e2-4d15-9a30-1c6b8f5d2e04",
|
|
57
|
+
new StoragePageControllerBase(),
|
|
58
|
+
() => ({
|
|
59
|
+
getServerBuckets: {},
|
|
60
|
+
activateBucket: {},
|
|
61
|
+
getActiveBucket: {},
|
|
62
|
+
clearWriteStats: {},
|
|
63
|
+
getAccessStats: {},
|
|
64
|
+
getAccessSummaries: {},
|
|
65
|
+
}),
|
|
66
|
+
() => ({
|
|
67
|
+
hooks: [assertIsManagementUser],
|
|
68
|
+
}),
|
|
69
|
+
{
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
export const StorageSynced = getSyncedController(StoragePageController, {
|
|
74
|
+
reads: {},
|
|
75
|
+
writes: {},
|
|
76
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ServerBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
|
|
2
|
+
import { getLiveServiceParameters, getMachineTargets, applyCommandTemplate, ServiceParameters, ServiceConfig, DEFAULT_OVERLAP_TIME } from "../../machineSchema";
|
|
3
|
+
import { STORAGE_COMMAND_PREFIX } from "../../serviceCategories";
|
|
4
|
+
|
|
5
|
+
module.hotreload = true;
|
|
6
|
+
|
|
7
|
+
const URL_ARG_REGEX = /--url\s+(\S+)/g;
|
|
8
|
+
|
|
9
|
+
/** The release a storage service is in the middle of. Read straight from the service configs, so it resolves without contacting any storage server - which is exactly when it matters most, since a deploy is the usual reason the servers don't answer. */
|
|
10
|
+
export type StorageService = {
|
|
11
|
+
serviceKey: string;
|
|
12
|
+
serviceTitle: string;
|
|
13
|
+
releaseTime?: number;
|
|
14
|
+
overlapTime: number;
|
|
15
|
+
hasOldParameters: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type StorageServer = StorageService & {
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type StorageServerBuckets = StorageServer & {
|
|
23
|
+
/** Still waiting on this server's own endpoint - each server loads independently */
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
buckets?: ServerBucketInfo[];
|
|
26
|
+
/** This server could not be read at all. Kept per server rather than thrown, so one bad server costs its own row and nothing else. */
|
|
27
|
+
error?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function getStorageUrls(parameters: ServiceParameters): string[] {
|
|
31
|
+
if (!parameters.command.trimStart().startsWith(STORAGE_COMMAND_PREFIX)) return [];
|
|
32
|
+
let urls: string[] = [];
|
|
33
|
+
for (let target of getMachineTargets(parameters)) {
|
|
34
|
+
let command = applyCommandTemplate(parameters.command, target.variables);
|
|
35
|
+
for (let match of command.matchAll(URL_ARG_REGEX)) {
|
|
36
|
+
urls.push(match[1]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return urls;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The storage servers a set of service configs runs, worked out on the client - it already has the configs, so asking the server to re-derive them would just make the whole page wait on one uncacheable call. */
|
|
43
|
+
export function getStorageServers(configs: ServiceConfig[]): StorageServer[] {
|
|
44
|
+
let serviceByUrl = new Map<string, StorageServer>();
|
|
45
|
+
for (let config of configs) {
|
|
46
|
+
let allParameters = [getLiveServiceParameters(config)];
|
|
47
|
+
for (let parameters of allParameters) {
|
|
48
|
+
if (!parameters) continue;
|
|
49
|
+
for (let url of getStorageUrls(parameters)) {
|
|
50
|
+
if (serviceByUrl.has(url)) continue;
|
|
51
|
+
serviceByUrl.set(url, {
|
|
52
|
+
url,
|
|
53
|
+
serviceKey: parameters.key,
|
|
54
|
+
serviceTitle: config.info.title,
|
|
55
|
+
releaseTime: config.parameters.releaseTime,
|
|
56
|
+
// The overlap always comes from the newest parameters, even though it governs how long the old instances outlive the release
|
|
57
|
+
overlapTime: config.parameters.overlapTime ?? DEFAULT_OVERLAP_TIME,
|
|
58
|
+
hasOldParameters: !!config.oldParameters,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...serviceByUrl.values()];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getStorageServices(servers: StorageServer[]): StorageService[] {
|
|
67
|
+
let byKey = new Map<string, StorageService>();
|
|
68
|
+
for (let server of servers) {
|
|
69
|
+
if (byKey.has(server.serviceKey)) continue;
|
|
70
|
+
byKey.set(server.serviceKey, server);
|
|
71
|
+
}
|
|
72
|
+
return [...byKey.values()];
|
|
73
|
+
}
|
|
@@ -29,6 +29,16 @@ export const SERVICE_FOLDER = `${SERVICE_FOLDER_NAME}/`;
|
|
|
29
29
|
export const MACHINE_RESYNC_INTERVAL = timeInMinute * 15;
|
|
30
30
|
export const SERVICE_NODE_FILE_NAME = "serviceNodeId.txt";
|
|
31
31
|
|
|
32
|
+
|
|
33
|
+
export type MachineConfig = {
|
|
34
|
+
machineId: string;
|
|
35
|
+
disabled: boolean;
|
|
36
|
+
};
|
|
37
|
+
// We want all of these to have very high availability. Even if they're wrong, it's better to have an old service deployed or even view information on an old service, or even set information only in back plays rather than have the server be completely down.
|
|
38
|
+
export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"), { fallbacks: true });
|
|
39
|
+
export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"), { fallbacks: true });
|
|
40
|
+
export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"), { fallbacks: true });
|
|
41
|
+
|
|
32
42
|
export type MachineInfo = {
|
|
33
43
|
machineId: string;
|
|
34
44
|
|
|
@@ -182,13 +192,6 @@ function normalizeServiceConfig(config: ServiceConfig): ServiceConfig {
|
|
|
182
192
|
return config;
|
|
183
193
|
}
|
|
184
194
|
|
|
185
|
-
export type MachineConfig = {
|
|
186
|
-
machineId: string;
|
|
187
|
-
disabled: boolean;
|
|
188
|
-
};
|
|
189
|
-
export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"));
|
|
190
|
-
export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"));
|
|
191
|
-
export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"));
|
|
192
195
|
|
|
193
196
|
export type LaunchRecord = {
|
|
194
197
|
serviceId: string;
|
|
@@ -276,7 +279,7 @@ export class MachineServiceControllerBase {
|
|
|
276
279
|
|
|
277
280
|
public async getMachineList() {
|
|
278
281
|
return [...new Set([
|
|
279
|
-
...(await machineInfos.keys(
|
|
282
|
+
...(await machineInfos.keys()),
|
|
280
283
|
])];
|
|
281
284
|
}
|
|
282
285
|
public async deleteMachineIds(machineIds: string[]) {
|
|
@@ -303,7 +306,7 @@ export class MachineServiceControllerBase {
|
|
|
303
306
|
}
|
|
304
307
|
public async getServiceList(): Promise<string[]> {
|
|
305
308
|
return [...new Set([
|
|
306
|
-
...(await serviceConfigs.keys(
|
|
309
|
+
...(await serviceConfigs.keys()),
|
|
307
310
|
])];
|
|
308
311
|
}
|
|
309
312
|
public async getServiceConfig(serviceId: string): Promise<ServiceConfig | undefined> {
|
|
@@ -312,7 +315,7 @@ export class MachineServiceControllerBase {
|
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
public async getMachineConfigList() {
|
|
315
|
-
return await machineConfigs.values(
|
|
318
|
+
return await machineConfigs.values();
|
|
316
319
|
}
|
|
317
320
|
public async getMachineConfig(machineId: string): Promise<MachineConfig | undefined> {
|
|
318
321
|
return await machineConfigs.get(machineId);
|
|
@@ -378,7 +381,7 @@ export class MachineServiceControllerBase {
|
|
|
378
381
|
// NOTE: This is correct. This is obviously correct. For some reason, the AI wrote some really big, complicated thing here. I don't know why. This is the only way it should be done, obviously.
|
|
379
382
|
config.oldParameters = getLiveServiceParameters(serviceConfig);
|
|
380
383
|
|
|
381
|
-
await serviceConfigs.set(serviceId, config
|
|
384
|
+
await serviceConfigs.set(serviceId, config);
|
|
382
385
|
// Only notify we were or are deployed. If it's not deployed, this will be ignored anyways.
|
|
383
386
|
if (config.parameters.deploy || serviceConfig.parameters.deploy) {
|
|
384
387
|
for (let machineId of getConfigMachineIds(config)) {
|
|
@@ -24,13 +24,16 @@ export class UsageBar extends qreact.Component<{
|
|
|
24
24
|
label: string;
|
|
25
25
|
value: number;
|
|
26
26
|
max: number;
|
|
27
|
+
/** Appended after each formatted number, so byte bars can read "5.2GB" instead of a bare "5.2G". */
|
|
28
|
+
unit?: string;
|
|
27
29
|
/** Fraction of max at which the bar fill turns yellow. */
|
|
28
30
|
warningThreshold?: number;
|
|
29
31
|
/** Fraction of max at which the bar fill turns red and flashes. */
|
|
30
32
|
errorThreshold?: number;
|
|
31
33
|
}> {
|
|
32
34
|
render() {
|
|
33
|
-
let { label, value, max, warningThreshold, errorThreshold } = this.props;
|
|
35
|
+
let { label, value, max, unit, warningThreshold, errorThreshold } = this.props;
|
|
36
|
+
let suffix = unit || "";
|
|
34
37
|
let fraction = max && value / max || 0;
|
|
35
38
|
let isError = errorThreshold !== undefined && fraction >= errorThreshold;
|
|
36
39
|
let isWarning = !isError && warningThreshold !== undefined && fraction >= warningThreshold;
|
|
@@ -42,7 +45,7 @@ export class UsageBar extends qreact.Component<{
|
|
|
42
45
|
+ (isError && (" " + FLASH_CLASS_NAME) || "")
|
|
43
46
|
} />
|
|
44
47
|
<div className={css.relative}>
|
|
45
|
-
{label} ({formatNumber(value)} / {formatNumber(max)})
|
|
48
|
+
{label} ({formatNumber(value)}{suffix} / {formatNumber(max)}{suffix})
|
|
46
49
|
</div>
|
|
47
50
|
{isError && <style>{`
|
|
48
51
|
@keyframes ${FLASH_CLASS_NAME}-anim {
|