@objy/server 0.0.0-stage → 0.1.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/API.md +1469 -0
- package/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +21 -2
- package/objydb.config.json +48 -0
- package/package.json +42 -4
- package/public/index.html +4134 -0
- package/public/objy-db-logo.png +0 -0
- package/src/aggregate.js +438 -0
- package/src/api.js +15 -0
- package/src/auth.js +472 -0
- package/src/clock.js +18 -0
- package/src/cluster-secret.js +25 -0
- package/src/cluster-status.js +562 -0
- package/src/compression-stats.js +32 -0
- package/src/config.js +360 -0
- package/src/crdt.js +1076 -0
- package/src/derived-state-scheduler.js +128 -0
- package/src/files.js +324 -0
- package/src/hnsw.js +269 -0
- package/src/http-cors.js +31 -0
- package/src/http-router.js +162 -0
- package/src/index.js +137 -0
- package/src/interfaces/auth.js +123 -0
- package/src/interfaces/cluster.js +296 -0
- package/src/interfaces/public-object.js +2452 -0
- package/src/interfaces/ui.js +38 -0
- package/src/lib/crypto.cjs +54 -0
- package/src/lib/jwt.cjs +231 -0
- package/src/local-wal.js +122 -0
- package/src/memory-guard.js +152 -0
- package/src/ndjson.js +117 -0
- package/src/node-runtime.js +29 -0
- package/src/object-hooks.js +282 -0
- package/src/object-id.js +7 -0
- package/src/object-storage.js +263 -0
- package/src/object-store.js +288 -0
- package/src/patch.js +87 -0
- package/src/permissions.js +178 -0
- package/src/persistence.js +1918 -0
- package/src/plugin-loader.js +21 -0
- package/src/query-planner.js +266 -0
- package/src/query.js +416 -0
- package/src/references.js +349 -0
- package/src/replication-scheduler.js +303 -0
- package/src/restore-status.js +77 -0
- package/src/runtime.js +47 -0
- package/src/server.js +242 -0
- package/src/shape.js +286 -0
- package/src/shard-map-store.js +85 -0
- package/src/shard-map.js +333 -0
- package/src/shard-router.js +737 -0
- package/src/store.js +3410 -0
- package/src/system-secrets.js +107 -0
- package/src/vector.js +83 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { freemem, totalmem } from "node:os";
|
|
2
|
+
import { getHeapStatistics } from "node:v8";
|
|
3
|
+
import { DEFAULT_SHARD, peerKey } from "./shard-map.js";
|
|
4
|
+
import { CLUSTER_SECRET_HEADER } from "./interfaces/cluster.js";
|
|
5
|
+
import { summarizeCompression } from "./compression-stats.js";
|
|
6
|
+
|
|
7
|
+
export const CLUSTER_STATUS_PATH = "/cluster/status";
|
|
8
|
+
export const DEFAULT_CLUSTER_STATUS_CACHE_MS = 1_000;
|
|
9
|
+
|
|
10
|
+
const clusterStatusCache = new WeakMap();
|
|
11
|
+
|
|
12
|
+
export function localClusterStatus(node, memoryGuard = null, replicationScheduler = null, restoreStatus = null, { details = false } = {}) {
|
|
13
|
+
const memory = memorySnapshot();
|
|
14
|
+
const shardMap = node.shardMapSnapshot();
|
|
15
|
+
const ownedShards = shardEntries(shardMap).filter(([shard]) => node.ownsShard(shard)).map(([shard]) => shard);
|
|
16
|
+
const indexSummary = node.indexSummary();
|
|
17
|
+
const vectorSummary = node.vectorSummary();
|
|
18
|
+
const restore = restoreSnapshot(restoreStatus);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
generatedAt: new Date().toISOString(),
|
|
23
|
+
node: {
|
|
24
|
+
id: node.id,
|
|
25
|
+
url: node.url || null,
|
|
26
|
+
},
|
|
27
|
+
memory,
|
|
28
|
+
memoryGuard: memoryGuard?.status?.() || null,
|
|
29
|
+
ownedShards,
|
|
30
|
+
replication: details ? {
|
|
31
|
+
...compactReplicationStatus(replicationScheduler),
|
|
32
|
+
seq: node.seq,
|
|
33
|
+
logLength: node.log.length,
|
|
34
|
+
trimmed: node.trimmed,
|
|
35
|
+
retainOps: node.retainOps,
|
|
36
|
+
syncBatchSize: node.syncBatchSize,
|
|
37
|
+
scheduler: replicationScheduler?.status?.() || null,
|
|
38
|
+
} : compactReplicationStatus(replicationScheduler),
|
|
39
|
+
indexes: details ? { ...node.indexReadiness(), ...indexSummary } : indexSummary,
|
|
40
|
+
vectors: details ? { ...vectorSummary, collections: node.vectorStats() } : vectorSummary,
|
|
41
|
+
compression: details
|
|
42
|
+
? node.compressionStats?.() || emptyCompressionStats()
|
|
43
|
+
: node.compressionSummary?.() || emptyCompressionSummary(),
|
|
44
|
+
restoring: restore?.status === "restoring",
|
|
45
|
+
...(restore?.status === "restoring" ? { restore } : {}),
|
|
46
|
+
process: {
|
|
47
|
+
pid: process.pid,
|
|
48
|
+
uptimeMs: Math.round(process.uptime() * 1000),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function collectClusterStatus(node, options = {}) {
|
|
54
|
+
const cacheMs = Math.max(0, Number(options.cacheMs ?? DEFAULT_CLUSTER_STATUS_CACHE_MS) || 0);
|
|
55
|
+
if (!cacheMs) return buildClusterStatus(node, options);
|
|
56
|
+
const key = options.details ? "details" : "compact";
|
|
57
|
+
let cache = clusterStatusCache.get(node);
|
|
58
|
+
if (!cache) {
|
|
59
|
+
cache = new Map();
|
|
60
|
+
clusterStatusCache.set(node, cache);
|
|
61
|
+
}
|
|
62
|
+
const cached = cache.get(key);
|
|
63
|
+
if (cached?.value && cached.expiresAt > Date.now()) return cached.value;
|
|
64
|
+
if (cached?.promise) return cached.promise;
|
|
65
|
+
|
|
66
|
+
const entry = {};
|
|
67
|
+
entry.promise = buildClusterStatus(node, options).then((value) => {
|
|
68
|
+
entry.value = value;
|
|
69
|
+
entry.expiresAt = Date.now() + cacheMs;
|
|
70
|
+
entry.promise = null;
|
|
71
|
+
return value;
|
|
72
|
+
}, (err) => {
|
|
73
|
+
cache.delete(key);
|
|
74
|
+
throw err;
|
|
75
|
+
});
|
|
76
|
+
cache.set(key, entry);
|
|
77
|
+
return entry.promise;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function buildClusterStatus(node, {
|
|
81
|
+
timeoutMs = 1500,
|
|
82
|
+
memoryGuard = null,
|
|
83
|
+
replicationScheduler = null,
|
|
84
|
+
restoreStatus = null,
|
|
85
|
+
details = false,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const local = {
|
|
88
|
+
...localClusterStatus(node, memoryGuard, replicationScheduler, restoreStatus, { details }),
|
|
89
|
+
local: true,
|
|
90
|
+
reachable: true,
|
|
91
|
+
};
|
|
92
|
+
const remotes = await Promise.all(remoteStatusPeers(node).map((peer) => fetchPeerStatus(peer, timeoutMs, details)));
|
|
93
|
+
const nodes = uniqueNodes([local, ...remotes]);
|
|
94
|
+
const shardMap = node.shardMapSnapshot();
|
|
95
|
+
const compression = compressionSummary(nodes, shardMap, { details });
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
generatedAt: new Date().toISOString(),
|
|
100
|
+
node: local.node,
|
|
101
|
+
shards: shardSummaries(node, shardMap, nodes),
|
|
102
|
+
rss: rssSummary(nodes, shardMap, { details }),
|
|
103
|
+
heap: heapSummary(nodes, shardMap, { details }),
|
|
104
|
+
compression: details ? compression : compression.logical,
|
|
105
|
+
indexes: aggregateIndexSummary(nodes),
|
|
106
|
+
vectors: aggregateVectorSummary(nodes),
|
|
107
|
+
nodes: nodes.map((status) => nodeStatusResponse(status, details)),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function restoreSnapshot(restoreStatus) {
|
|
112
|
+
if (!restoreStatus) return null;
|
|
113
|
+
return typeof restoreStatus.snapshot === "function" ? restoreStatus.snapshot() : restoreStatus;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function compactReplicationStatus(replicationScheduler) {
|
|
117
|
+
const status = replicationScheduler?.status?.() || {};
|
|
118
|
+
const error = status.lastError || null;
|
|
119
|
+
const pendingHints = Number(status.pendingHintScopes) || 0;
|
|
120
|
+
const inflightPulls = (Number(status.queuedPulls) || 0) + (Number(status.inflightPulls) || 0);
|
|
121
|
+
return {
|
|
122
|
+
healthy:
|
|
123
|
+
!error
|
|
124
|
+
&& (!Array.isArray(status.lastSyncFailures) || status.lastSyncFailures.length === 0)
|
|
125
|
+
&& pendingHints === 0
|
|
126
|
+
&& inflightPulls === 0
|
|
127
|
+
&& status.syncingAll !== true,
|
|
128
|
+
lastSuccessfulSyncAt: status.lastSyncAt || null,
|
|
129
|
+
pendingHints,
|
|
130
|
+
inflightPulls,
|
|
131
|
+
error,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function aggregateIndexSummary(nodes) {
|
|
136
|
+
const summary = { ready: 0, building: 0, error: 0, activeTasks: 0 };
|
|
137
|
+
for (const status of nodes) {
|
|
138
|
+
const indexes = status.indexes || {};
|
|
139
|
+
summary.ready += Number(indexes.ready) || 0;
|
|
140
|
+
summary.building += Number(indexes.building) || 0;
|
|
141
|
+
summary.error += Number(indexes.error) || 0;
|
|
142
|
+
summary.activeTasks += Number(indexes.activeTasks) || 0;
|
|
143
|
+
}
|
|
144
|
+
return summary;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function aggregateVectorSummary(nodes) {
|
|
148
|
+
const summary = { storedVectors: 0, activeIndexes: 0, warmingIndexes: 0, errors: 0 };
|
|
149
|
+
for (const status of nodes) {
|
|
150
|
+
const vectors = status.vectors || {};
|
|
151
|
+
summary.storedVectors += Number(vectors.storedVectors) || 0;
|
|
152
|
+
summary.activeIndexes += Number(vectors.activeIndexes) || 0;
|
|
153
|
+
summary.warmingIndexes += Number(vectors.warmingIndexes) || 0;
|
|
154
|
+
summary.errors += Number(vectors.errors) || 0;
|
|
155
|
+
}
|
|
156
|
+
return summary;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function nodeStatusResponse(status, details) {
|
|
160
|
+
if (details) {
|
|
161
|
+
const { compression, ...response } = status;
|
|
162
|
+
return response;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: status.ok !== false,
|
|
166
|
+
generatedAt: status.generatedAt,
|
|
167
|
+
local: status.local === true,
|
|
168
|
+
reachable: status.reachable !== false,
|
|
169
|
+
...(status.status ? { status: status.status } : {}),
|
|
170
|
+
...(status.error ? { error: status.error } : {}),
|
|
171
|
+
node: status.node,
|
|
172
|
+
memory: status.memory || null,
|
|
173
|
+
memoryGuard: status.memoryGuard || null,
|
|
174
|
+
ownedShards: status.ownedShards || [],
|
|
175
|
+
replication: status.replication || null,
|
|
176
|
+
restoring: status.restoring === true,
|
|
177
|
+
...(status.restore ? { restore: status.restore } : {}),
|
|
178
|
+
process: status.process || null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function fetchPeerStatus(peer, timeoutMs, details = false) {
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(`${peer.url}${CLUSTER_STATUS_PATH}${details ? "?details=1" : ""}`, {
|
|
187
|
+
signal: controller.signal,
|
|
188
|
+
headers: peer.secret ? { [CLUSTER_SECRET_HEADER]: peer.secret } : {},
|
|
189
|
+
});
|
|
190
|
+
const body = await response.json().catch(() => null);
|
|
191
|
+
if (!response.ok || !body) {
|
|
192
|
+
return unreachablePeer(peer, `status ${response.status}`, response.status);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
...body,
|
|
196
|
+
ok: body.ok !== false,
|
|
197
|
+
local: false,
|
|
198
|
+
reachable: true,
|
|
199
|
+
sourcePeer: describePeer(peer),
|
|
200
|
+
status: response.status,
|
|
201
|
+
};
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const message = err.name === "AbortError" ? `timeout after ${timeoutMs}ms` : err.message;
|
|
204
|
+
return unreachablePeer(peer, message);
|
|
205
|
+
} finally {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function unreachablePeer(peer, error, status = 0) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
local: false,
|
|
214
|
+
reachable: false,
|
|
215
|
+
status,
|
|
216
|
+
generatedAt: new Date().toISOString(),
|
|
217
|
+
node: {
|
|
218
|
+
id: peer.nodeId || null,
|
|
219
|
+
url: peer.url || null,
|
|
220
|
+
},
|
|
221
|
+
memory: null,
|
|
222
|
+
memoryGuard: null,
|
|
223
|
+
ownedShards: [],
|
|
224
|
+
restoring: false,
|
|
225
|
+
sourcePeer: describePeer(peer),
|
|
226
|
+
error,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function remoteStatusPeers(node) {
|
|
231
|
+
return node.shardMap.allPeers().filter((peer) => peer.url && !node._isSelfPeer(peer));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function uniqueNodes(nodes) {
|
|
235
|
+
const out = new Map();
|
|
236
|
+
for (const status of nodes) {
|
|
237
|
+
const key = status.node?.id || status.node?.url || status.sourcePeer?.url || status.sourcePeer?.key;
|
|
238
|
+
if (!key || out.has(key)) continue;
|
|
239
|
+
out.set(key, status);
|
|
240
|
+
}
|
|
241
|
+
return [...out.values()].sort((left, right) => {
|
|
242
|
+
if (left.local) return -1;
|
|
243
|
+
if (right.local) return 1;
|
|
244
|
+
return nodeLabel(left).localeCompare(nodeLabel(right));
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function rssSummary(nodes, shardMap, { details = false } = {}) {
|
|
249
|
+
const perNode = nodes.map(nodeMemoryEntry);
|
|
250
|
+
const physical = memoryTotals(perNode);
|
|
251
|
+
const logical = logicalMemoryTotals(nodes, perNode, shardMap, memoryTotals, { details });
|
|
252
|
+
return {
|
|
253
|
+
cluster: logical,
|
|
254
|
+
logical,
|
|
255
|
+
physical,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function heapSummary(nodes, shardMap, { details = false } = {}) {
|
|
260
|
+
const perNode = nodes.map(nodeHeapEntry);
|
|
261
|
+
const physical = heapTotals(perNode);
|
|
262
|
+
const logical = logicalMemoryTotals(nodes, perNode, shardMap, heapTotals, { details });
|
|
263
|
+
return {
|
|
264
|
+
cluster: logical,
|
|
265
|
+
logical,
|
|
266
|
+
physical,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function compressionSummary(nodes, shardMap, { details = false } = {}) {
|
|
271
|
+
const perNode = nodes.map(nodeCompressionEntry);
|
|
272
|
+
const physical = compressionTotals(perNode);
|
|
273
|
+
const logical = logicalMemoryTotals(nodes, perNode, shardMap, compressionTotals, { details });
|
|
274
|
+
return {
|
|
275
|
+
cluster: logical,
|
|
276
|
+
logical,
|
|
277
|
+
physical,
|
|
278
|
+
...(details ? { nodes: perNode } : {}),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function nodeMemoryEntry(status) {
|
|
283
|
+
const rss = status.memory?.rss ?? null;
|
|
284
|
+
const totalMemory = status.memory?.systemTotal ?? null;
|
|
285
|
+
const freeMemory = status.memory?.systemFree ?? null;
|
|
286
|
+
return {
|
|
287
|
+
key: nodeStatusKey(status),
|
|
288
|
+
nodeId: status.node?.id || null,
|
|
289
|
+
url: status.node?.url || status.sourcePeer?.url || null,
|
|
290
|
+
local: status.local === true,
|
|
291
|
+
reachable: status.reachable !== false,
|
|
292
|
+
rss,
|
|
293
|
+
rssMb: rss === null ? null : mib(rss),
|
|
294
|
+
totalMemory,
|
|
295
|
+
totalMemoryMb: totalMemory === null ? null : mib(totalMemory),
|
|
296
|
+
freeMemory,
|
|
297
|
+
freeMemoryMb: freeMemory === null ? null : mib(freeMemory),
|
|
298
|
+
rssPercent: percent(rss, totalMemory),
|
|
299
|
+
error: status.reachable === false ? status.error : undefined,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function nodeHeapEntry(status) {
|
|
304
|
+
const heapUsed = status.memory?.heapUsed ?? null;
|
|
305
|
+
const heapTotal = status.memory?.heapTotal ?? null;
|
|
306
|
+
const heapLimit = status.memory?.heapLimit ?? heapTotal;
|
|
307
|
+
return {
|
|
308
|
+
key: nodeStatusKey(status),
|
|
309
|
+
nodeId: status.node?.id || null,
|
|
310
|
+
url: status.node?.url || status.sourcePeer?.url || null,
|
|
311
|
+
local: status.local === true,
|
|
312
|
+
reachable: status.reachable !== false,
|
|
313
|
+
used: heapUsed,
|
|
314
|
+
usedMb: heapUsed === null ? null : mib(heapUsed),
|
|
315
|
+
total: heapLimit,
|
|
316
|
+
totalMb: heapLimit === null ? null : mib(heapLimit),
|
|
317
|
+
committed: heapTotal,
|
|
318
|
+
committedMb: heapTotal === null ? null : mib(heapTotal),
|
|
319
|
+
heapUsed,
|
|
320
|
+
heapUsedMb: heapUsed === null ? null : mib(heapUsed),
|
|
321
|
+
heapTotal,
|
|
322
|
+
heapTotalMb: heapTotal === null ? null : mib(heapTotal),
|
|
323
|
+
heapLimit,
|
|
324
|
+
heapLimitMb: heapLimit === null ? null : mib(heapLimit),
|
|
325
|
+
percent: percent(heapUsed, heapLimit),
|
|
326
|
+
error: status.reachable === false ? status.error : undefined,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function nodeCompressionEntry(status) {
|
|
331
|
+
const compression = status.compression || emptyCompressionStats();
|
|
332
|
+
return {
|
|
333
|
+
key: nodeStatusKey(status),
|
|
334
|
+
nodeId: status.node?.id || null,
|
|
335
|
+
url: status.node?.url || status.sourcePeer?.url || null,
|
|
336
|
+
local: status.local === true,
|
|
337
|
+
reachable: status.reachable !== false,
|
|
338
|
+
collectionCount: Number(compression.collectionCount) || 0,
|
|
339
|
+
liveObjects: Number(compression.liveObjects) || 0,
|
|
340
|
+
compressedObjects: Number(compression.compressedObjects) || 0,
|
|
341
|
+
coveragePercent: Number(compression.coveragePercent) || 0,
|
|
342
|
+
uncompressedBytes: Number(compression.uncompressedBytes) || 0,
|
|
343
|
+
compressedBytes: Number(compression.compressedBytes) || 0,
|
|
344
|
+
savedBytes: Number(compression.savedBytes) || 0,
|
|
345
|
+
savedPercent: Number(compression.savedPercent) || 0,
|
|
346
|
+
collections: Array.isArray(compression.collections) ? compression.collections : [],
|
|
347
|
+
error: status.reachable === false ? status.error : undefined,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function memoryTotals(entries, extra = {}) {
|
|
352
|
+
const total = sumField(entries, "rss");
|
|
353
|
+
const totalMemory = sumField(entries, "totalMemory");
|
|
354
|
+
const freeMemory = sumField(entries, "freeMemory");
|
|
355
|
+
const online = entries.filter((entry) => entry.reachable).length;
|
|
356
|
+
return {
|
|
357
|
+
rss: total,
|
|
358
|
+
rssMb: mib(total),
|
|
359
|
+
totalMemory,
|
|
360
|
+
totalMemoryMb: mib(totalMemory),
|
|
361
|
+
freeMemory,
|
|
362
|
+
freeMemoryMb: mib(freeMemory),
|
|
363
|
+
rssPercent: percent(total, totalMemory),
|
|
364
|
+
nodeCount: entries.length,
|
|
365
|
+
onlineNodeCount: online,
|
|
366
|
+
offlineNodeCount: entries.length - online,
|
|
367
|
+
...extra,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function heapTotals(entries, extra = {}) {
|
|
372
|
+
const used = sumField(entries, "used");
|
|
373
|
+
const total = sumField(entries, "total");
|
|
374
|
+
const committed = sumField(entries, "committed");
|
|
375
|
+
const online = entries.filter((entry) => entry.reachable).length;
|
|
376
|
+
return {
|
|
377
|
+
used,
|
|
378
|
+
usedMb: mib(used),
|
|
379
|
+
total,
|
|
380
|
+
totalMb: mib(total),
|
|
381
|
+
committed,
|
|
382
|
+
committedMb: mib(committed),
|
|
383
|
+
heapUsed: used,
|
|
384
|
+
heapUsedMb: mib(used),
|
|
385
|
+
heapLimit: total,
|
|
386
|
+
heapLimitMb: mib(total),
|
|
387
|
+
heapTotal: committed,
|
|
388
|
+
heapTotalMb: mib(committed),
|
|
389
|
+
percent: percent(used, total),
|
|
390
|
+
nodeCount: entries.length,
|
|
391
|
+
onlineNodeCount: online,
|
|
392
|
+
offlineNodeCount: entries.length - online,
|
|
393
|
+
...extra,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function compressionTotals(entries, extra = {}) {
|
|
398
|
+
const summary = summarizeCompression(entries);
|
|
399
|
+
const online = entries.filter((entry) => entry.reachable).length;
|
|
400
|
+
return {
|
|
401
|
+
...summary,
|
|
402
|
+
nodeCount: entries.length,
|
|
403
|
+
onlineNodeCount: online,
|
|
404
|
+
offlineNodeCount: entries.length - online,
|
|
405
|
+
...extra,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function logicalMemoryTotals(nodes, perNode, shardMap, totals = memoryTotals, { details = false } = {}) {
|
|
410
|
+
const byKey = new Map(perNode.map((entry) => [entry.key, entry]));
|
|
411
|
+
const selected = new Map();
|
|
412
|
+
const selectedReplicas = [];
|
|
413
|
+
const missingShards = [];
|
|
414
|
+
let sawConfiguredPeer = false;
|
|
415
|
+
const shards = shardEntries(shardMap);
|
|
416
|
+
|
|
417
|
+
for (const [shard, config] of shards) {
|
|
418
|
+
const peers = config.peers || [];
|
|
419
|
+
if (peers.length) sawConfiguredPeer = true;
|
|
420
|
+
const status = peers
|
|
421
|
+
.map((peer) => nodes.find((candidate) => peerMatchesStatus(peer, candidate)))
|
|
422
|
+
.find((candidate) => candidate && candidate.reachable !== false);
|
|
423
|
+
|
|
424
|
+
if (!status) {
|
|
425
|
+
missingShards.push(shard);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const key = nodeStatusKey(status);
|
|
430
|
+
const entry = byKey.get(key);
|
|
431
|
+
if (entry && !selected.has(key)) selected.set(key, entry);
|
|
432
|
+
selectedReplicas.push({
|
|
433
|
+
shard,
|
|
434
|
+
nodeId: status.node?.id || null,
|
|
435
|
+
url: status.node?.url || status.sourcePeer?.url || null,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!sawConfiguredPeer) {
|
|
440
|
+
return totals(perNode, {
|
|
441
|
+
shardCount: shards.length,
|
|
442
|
+
coveredShardCount: perNode.some((entry) => entry.reachable) ? shards.length : 0,
|
|
443
|
+
missingShardCount: 0,
|
|
444
|
+
...(details ? { missingShards: [], selectedReplicas: [] } : {}),
|
|
445
|
+
mode: "physical-fallback",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return totals([...selected.values()], {
|
|
450
|
+
shardCount: shards.length,
|
|
451
|
+
coveredShardCount: selectedReplicas.length,
|
|
452
|
+
missingShardCount: missingShards.length,
|
|
453
|
+
...(details ? { missingShards, selectedReplicas } : {}),
|
|
454
|
+
mode: "one-replica-per-shard",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function shardSummaries(node, shardMap = node.shardMapSnapshot(), nodes = []) {
|
|
459
|
+
return shardEntries(shardMap).map(([name, config]) => {
|
|
460
|
+
const peers = (config.peers || []).map((peer) => enrichedPeer(node, peer, nodes));
|
|
461
|
+
return {
|
|
462
|
+
name,
|
|
463
|
+
owned: node.ownsShard(name),
|
|
464
|
+
peerCount: peers.length,
|
|
465
|
+
peers,
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function shardEntries(shardMap) {
|
|
471
|
+
const entries = Object.entries(shardMap.shards || {});
|
|
472
|
+
if (!entries.length) return [[DEFAULT_SHARD, { peers: [] }]];
|
|
473
|
+
return entries.sort(([left], [right]) => left.localeCompare(right));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function enrichedPeer(node, peer, nodes) {
|
|
477
|
+
const status = nodes.find((candidate) => peerMatchesStatus(peer, candidate));
|
|
478
|
+
return {
|
|
479
|
+
...describePeer(peer),
|
|
480
|
+
self: node._isSelfPeer(peer),
|
|
481
|
+
online: node._isSelfPeer(peer) || status?.reachable === true,
|
|
482
|
+
nodeId: status?.node?.id || peer.nodeId || null,
|
|
483
|
+
error: status?.reachable === false ? status.error : undefined,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function peerMatchesStatus(peer, status) {
|
|
488
|
+
if (!status) return false;
|
|
489
|
+
return (
|
|
490
|
+
(peer.url && (status.node?.url === peer.url || status.sourcePeer?.url === peer.url)) ||
|
|
491
|
+
(peer.nodeId && status.node?.id === peer.nodeId) ||
|
|
492
|
+
(peer.key && (status.node?.id === peer.key || status.sourcePeer?.key === peer.key))
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function describePeer(peer = {}) {
|
|
497
|
+
return {
|
|
498
|
+
key: peerKey(peer),
|
|
499
|
+
url: peer.url || null,
|
|
500
|
+
nodeId: peer.nodeId || null,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function memorySnapshot() {
|
|
505
|
+
const usage = process.memoryUsage();
|
|
506
|
+
const heap = getHeapStatistics();
|
|
507
|
+
const systemTotal = totalmem();
|
|
508
|
+
const systemFree = freemem();
|
|
509
|
+
return {
|
|
510
|
+
rss: usage.rss,
|
|
511
|
+
rssMb: mib(usage.rss),
|
|
512
|
+
systemTotal,
|
|
513
|
+
systemTotalMb: mib(systemTotal),
|
|
514
|
+
systemFree,
|
|
515
|
+
systemFreeMb: mib(systemFree),
|
|
516
|
+
rssSystemPercent: percent(usage.rss, systemTotal),
|
|
517
|
+
heapUsed: usage.heapUsed,
|
|
518
|
+
heapUsedMb: mib(usage.heapUsed),
|
|
519
|
+
heapTotal: usage.heapTotal,
|
|
520
|
+
heapTotalMb: mib(usage.heapTotal),
|
|
521
|
+
heapLimit: heap.heap_size_limit,
|
|
522
|
+
heapLimitMb: mib(heap.heap_size_limit),
|
|
523
|
+
external: usage.external,
|
|
524
|
+
externalMb: mib(usage.external),
|
|
525
|
+
arrayBuffers: usage.arrayBuffers,
|
|
526
|
+
arrayBuffersMb: mib(usage.arrayBuffers),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function sumField(entries, field) {
|
|
531
|
+
return entries.reduce((sum, entry) => sum + (Number(entry[field]) || 0), 0);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function emptyCompressionStats() {
|
|
535
|
+
return {
|
|
536
|
+
...emptyCompressionSummary(),
|
|
537
|
+
collections: [],
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function emptyCompressionSummary() {
|
|
542
|
+
return summarizeCompression([]);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function percent(value, total) {
|
|
546
|
+
const numerator = Number(value);
|
|
547
|
+
const denominator = Number(total);
|
|
548
|
+
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return null;
|
|
549
|
+
return Math.round((numerator / denominator) * 10_000) / 100;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function mib(bytes) {
|
|
553
|
+
return Math.round((bytes / 1024 / 1024) * 10) / 10;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function nodeLabel(status) {
|
|
557
|
+
return nodeStatusKey(status);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function nodeStatusKey(status) {
|
|
561
|
+
return status.node?.id || status.node?.url || status.sourcePeer?.url || status.sourcePeer?.key || "";
|
|
562
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function summarizeCompression(entries = []) {
|
|
2
|
+
const collectionCount = entries.reduce((sum, entry) => {
|
|
3
|
+
const count = Number(entry.collectionCount);
|
|
4
|
+
return sum + (Number.isFinite(count) ? count : 1);
|
|
5
|
+
}, 0);
|
|
6
|
+
const liveObjects = sum(entries, "liveObjects");
|
|
7
|
+
const compressedObjects = sum(entries, "compressedObjects");
|
|
8
|
+
const uncompressedBytes = sum(entries, "uncompressedBytes");
|
|
9
|
+
const compressedBytes = sum(entries, "compressedBytes");
|
|
10
|
+
const savedBytes = uncompressedBytes - compressedBytes;
|
|
11
|
+
return {
|
|
12
|
+
collectionCount,
|
|
13
|
+
liveObjects,
|
|
14
|
+
compressedObjects,
|
|
15
|
+
coveragePercent: percentOf(compressedObjects, liveObjects),
|
|
16
|
+
uncompressedBytes,
|
|
17
|
+
compressedBytes,
|
|
18
|
+
savedBytes,
|
|
19
|
+
savedPercent: percentOf(savedBytes, uncompressedBytes),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function percentOf(value, total) {
|
|
24
|
+
const numerator = Number(value);
|
|
25
|
+
const denominator = Number(total);
|
|
26
|
+
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return 0;
|
|
27
|
+
return Math.round((numerator / denominator) * 10_000) / 100;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sum(entries, field) {
|
|
31
|
+
return entries.reduce((total, entry) => total + (Number(entry[field]) || 0), 0);
|
|
32
|
+
}
|