querysub 0.650.0 → 0.652.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/overview.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Querysub Overview
|
|
2
|
+
|
|
3
|
+
The system is fundamentally a data synchronization system.
|
|
4
|
+
|
|
5
|
+
## Path values and time
|
|
6
|
+
|
|
7
|
+
Data exists in path values, which have a path (a structured list of strings) and a value. These are time based: they have the time that they occur at, which is a globally unique time. This time includes a somewhat unique identifier for the server that created the time (best effort — it cannot be relied on, but is usually unique), the time itself, and also a version value that our system uses to create something that happens at the same time, but just an epsilon amount after.
|
|
8
|
+
|
|
9
|
+
## Locks and validity
|
|
10
|
+
|
|
11
|
+
Path values also include what are referred to as locks. Locks are essentially a list of reads that were done at the time the path value was created.
|
|
12
|
+
|
|
13
|
+
Values are evaluated in an eventually consistent manner. A value can be rejected if its locks are rejected. This can mean that the value it read has itself become rejected, or it can mean that the value it read was not the latest value at the time — which we know if we find another value between the creation time of the value that was read and the time that it was read at. That's why it's called a lock: it's a kind of locking of that value between its creation time and the time we read it. Of course, it's a reversed lock — instead of having the writer fail because of the lock, we have the reader fail.
|
|
14
|
+
|
|
15
|
+
## Synchronization and watchers
|
|
16
|
+
|
|
17
|
+
Synchronization is very important. We synchronize values between different watchers in a client, and between machines. This is done at a low level in PathWatcher and RemoteWatcher. We have the ability to watch a path, or watch the direct children of a path. There is no recursive watching — if values need to be recursively watched, the watcher has to recursively access more and more each time it receives values, watching more each time.
|
|
18
|
+
|
|
19
|
+
## Schema
|
|
20
|
+
|
|
21
|
+
We have a schema system which can simplify accesses and make it so that if values aren't provided, we know whether to default to a primitive value or use a proxy so the code can drill down further.
|
|
22
|
+
|
|
23
|
+
## Proxy watcher
|
|
24
|
+
|
|
25
|
+
Most accesses are done using the proxy watcher. This is a system that sits on top of our other systems, letting you write regular JavaScript code to access fields and write to fields as if it were plain JavaScript, with all of the reads and writes being converted to path value reads and writes.
|
|
26
|
+
|
|
27
|
+
## Client writes, function calls, and predictions
|
|
28
|
+
|
|
29
|
+
On the client, anything that needs to impact a remote value is mutated by making a special socket function call to the server, asking it to add a function call write — which writes a path value to a specific location. We then have a function runner server that reads from that location, running those functions. The client side also runs these functions, but only as a prediction. That way most actions feel immediate. When the server function runner does eventually run, it updates with the actual results.
|
|
30
|
+
|
|
31
|
+
## Sharding
|
|
32
|
+
|
|
33
|
+
The servers are sharded — they are split up over different values. We generally don't do global path hashing. Instead, there are specific paths that a server reads on startup, and it does its hashing by looking at the child key of those paths when they match. These generally match up with lookups, so the hashes are usually the lookup keys. This allows an object's fields to be on the same server — minimizing the number of different servers we access — while still sharding the data.
|
|
34
|
+
|
|
35
|
+
## Geographic routing
|
|
36
|
+
|
|
37
|
+
Almost everything has some level of sharding or geographic control. For example, the client side tries to connect to the querysub server — the server that actually adds the function calls and interacts with the client — and it usually connects to the closest one, or the one with the lowest latency. Function runners are also sharded between geographic locations, and the client usually requests one that is close to it.
|
|
38
|
+
|
|
39
|
+
## Non-core systems
|
|
40
|
+
|
|
41
|
+
- The binary format of path values, which involves compression, etc.
|
|
42
|
+
- The error notification system.
|
|
43
|
+
- The logging and log search system.
|
|
44
|
+
- The MCP server.
|
|
45
|
+
- QReact, which implements a JSX component renderer using our synchronization system.
|
|
46
|
+
- A machine and service management system.
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ import { ProcessRecord, syncProcessRecords, listProcessRecords } from "./process
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ value: number; max: number } | undefined> {
|
|
34
|
+
const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ memory?: { value: number; max: number }; swap?: { value: number; max: number } } | undefined> {
|
|
35
35
|
if (os.platform() === "win32") {
|
|
36
36
|
throw new Error("Windows is not supported for machine resource monitoring");
|
|
37
37
|
}
|
|
@@ -40,17 +40,24 @@ const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ valu
|
|
|
40
40
|
// Linux: Use free command
|
|
41
41
|
let result = await runPromise("free -b", { quiet: true });
|
|
42
42
|
let lines = result.split("\n");
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
let parseLine = (prefix: string): { value: number; max: number } | undefined => {
|
|
44
|
+
let line = lines.find(line => line.startsWith(prefix));
|
|
45
|
+
if (!line) return undefined;
|
|
46
|
+
let parts = line.split(/\s+/);
|
|
46
47
|
let total = parseInt(parts[1]);
|
|
47
48
|
let used = parseInt(parts[2]);
|
|
48
|
-
if (total && used >= 0) {
|
|
49
|
+
if (total >= 0 && used >= 0) {
|
|
49
50
|
return { value: used, max: total };
|
|
50
51
|
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
let memory = parseLine("Mem:");
|
|
55
|
+
let swap = parseLine("Swap:");
|
|
56
|
+
if (memory || swap) {
|
|
57
|
+
return { memory, swap };
|
|
51
58
|
}
|
|
52
59
|
} catch (e: any) {
|
|
53
|
-
console.warn(`Error getting memory info: ${e.
|
|
60
|
+
console.warn(`Error getting memory info: ${e.stack}`);
|
|
54
61
|
}
|
|
55
62
|
return undefined;
|
|
56
63
|
});
|
|
@@ -98,11 +105,19 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
|
|
|
98
105
|
getDiskInfo()
|
|
99
106
|
]);
|
|
100
107
|
|
|
101
|
-
if (memoryInfo) {
|
|
108
|
+
if (memoryInfo?.memory) {
|
|
102
109
|
machineInfo.info.ram = {
|
|
103
110
|
type: "MEMORY",
|
|
104
|
-
value: memoryInfo.value,
|
|
105
|
-
max: memoryInfo.max,
|
|
111
|
+
value: memoryInfo.memory.value,
|
|
112
|
+
max: memoryInfo.memory.max,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (memoryInfo?.swap) {
|
|
117
|
+
machineInfo.info.swap = {
|
|
118
|
+
type: "SWAP",
|
|
119
|
+
value: memoryInfo.swap.value,
|
|
120
|
+
max: memoryInfo.swap.max,
|
|
106
121
|
};
|
|
107
122
|
}
|
|
108
123
|
|
|
@@ -20,11 +20,14 @@ import { MCPIndexedLogs, normalizeTime } from "./MCPIndexedLogs";
|
|
|
20
20
|
import { searchStorageLogsForMCP } from "../../../deployManager/components/storage/storageLogMCPSearch";
|
|
21
21
|
import { getAllNodeIds } from "../../../-f-node-discovery/NodeDiscovery";
|
|
22
22
|
import { NodeCapabilitiesController } from "../../../-g-core-values/NodeCapabilities";
|
|
23
|
-
import { formatTime } from "socket-function/src/formatting/format";
|
|
23
|
+
import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
|
|
24
24
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
25
|
+
import { sort } from "socket-function/src/misc";
|
|
26
|
+
import { getSuppressionEntries } from "../errorNotifications2/errorNotifications";
|
|
25
27
|
|
|
26
28
|
const DEFAULT_MCP_HTTP_PORT = 4487;
|
|
27
29
|
const NODE_INFO_TIMEOUT_MS = 5000;
|
|
30
|
+
const DEFAULT_SUPPRESSION_LIMIT = 20;
|
|
28
31
|
|
|
29
32
|
const PROTOCOL_VERSION = "2025-03-26";
|
|
30
33
|
const SERVER_INFO = { name: "querysub-indexed-logs", version: "0.1.0" };
|
|
@@ -97,6 +100,19 @@ Query syntax (case-sensitive substring match against each entry's JSON):
|
|
|
97
100
|
properties: {},
|
|
98
101
|
},
|
|
99
102
|
},
|
|
103
|
+
{
|
|
104
|
+
name: "getSuppressions",
|
|
105
|
+
description: `List error-suppression entries (patterns that stop matching errors from triggering notifications), sorted most-recently-updated first. There can be hundreds, so results are limited (default ${DEFAULT_SUPPRESSION_LIMIT}); use the optional query filter to narrow.
|
|
106
|
+
|
|
107
|
+
Returns { total, returned, results } — total is the entry count after filtering, results contains { id, pattern, notes, lastUpdated, created, timeout, expired }. All three times are formatted date strings. expired is present (true) when the entry's timeout has passed, meaning it no longer suppresses anything.`,
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
limit: { type: "number", default: DEFAULT_SUPPRESSION_LIMIT, description: "Maximum entries to return, most recently updated first." },
|
|
112
|
+
query: { type: "string", description: "Optional case-insensitive substring filter, matched against id, pattern, and notes." },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
100
116
|
];
|
|
101
117
|
|
|
102
118
|
type JsonRpcRequest = {
|
|
@@ -184,6 +200,8 @@ async function dispatch(method: string, params: unknown, mcp: MCPIndexedLogs): P
|
|
|
184
200
|
});
|
|
185
201
|
} else if (toolName === "listNodes") {
|
|
186
202
|
result = await getNodeInfos();
|
|
203
|
+
} else if (toolName === "getSuppressions") {
|
|
204
|
+
result = await getSuppressionsForMCP(args as { limit?: number; query?: string });
|
|
187
205
|
} else {
|
|
188
206
|
throw new Error(`Unknown tool ${toolName}`);
|
|
189
207
|
}
|
|
@@ -220,6 +238,34 @@ async function getNodeInfos(): Promise<NodeInfo[]> {
|
|
|
220
238
|
);
|
|
221
239
|
}
|
|
222
240
|
|
|
241
|
+
async function getSuppressionsForMCP(config: { limit?: number; query?: string }) {
|
|
242
|
+
let limit = config.limit ?? DEFAULT_SUPPRESSION_LIMIT;
|
|
243
|
+
let entries = await getSuppressionEntries();
|
|
244
|
+
let query = config.query?.toLowerCase();
|
|
245
|
+
if (query) {
|
|
246
|
+
const q = query;
|
|
247
|
+
entries = entries.filter(x => (x.id + "\n" + x.pattern + "\n" + (x.notes || "")).toLowerCase().includes(q));
|
|
248
|
+
}
|
|
249
|
+
sort(entries, x => -x.lastUpdatedTime);
|
|
250
|
+
let now = Date.now();
|
|
251
|
+
let results = entries.slice(0, limit).map(x => {
|
|
252
|
+
let expired: true | undefined;
|
|
253
|
+
if (x.timeout < now) {
|
|
254
|
+
expired = true;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
id: x.id,
|
|
258
|
+
pattern: x.pattern,
|
|
259
|
+
notes: x.notes,
|
|
260
|
+
lastUpdated: formatDateTime(x.lastUpdatedTime),
|
|
261
|
+
created: formatDateTime(x.createdTime),
|
|
262
|
+
timeout: formatDateTime(x.timeout),
|
|
263
|
+
expired,
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
return { total: entries.length, returned: results.length, results };
|
|
267
|
+
}
|
|
268
|
+
|
|
223
269
|
async function main() {
|
|
224
270
|
let mcp = new MCPIndexedLogs();
|
|
225
271
|
|
|
@@ -394,22 +394,27 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
394
394
|
continue;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
let responseMissing = !response.time || compareTime(response.time, epochTime) === 0;
|
|
398
|
+
if (responseMissing) {
|
|
399
|
+
response = { ...response, valid: true, isTransparent: true };
|
|
400
|
+
}
|
|
397
401
|
if (ourValue.isTransparent && response.isTransparent) continue;
|
|
398
402
|
|
|
399
403
|
if (request.time) {
|
|
400
404
|
let ourExact = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
|
|
401
405
|
if (!ourExact || !ourExact.valid) continue;
|
|
402
406
|
if (compareTime(ourExact.time, epochTime) === 0) continue;
|
|
407
|
+
if (ourExact.isTransparent && responseMissing) continue;
|
|
403
408
|
// The exact value = server does not have
|
|
404
409
|
// - Send it our value
|
|
405
|
-
if (!response.time) {
|
|
410
|
+
if (!response.time || responseMissing) {
|
|
406
411
|
valuesToSend.push(ourExact);
|
|
407
412
|
trackSyncAge({
|
|
408
413
|
path: request.path,
|
|
409
414
|
ourTimeId: ourExact.time.time,
|
|
410
415
|
remoteTimeId: undefined,
|
|
411
416
|
ourValid: ourExact.valid,
|
|
412
|
-
remoteValid:
|
|
417
|
+
remoteValid: undefined,
|
|
413
418
|
remoteNodeId: nodeId,
|
|
414
419
|
reason: "Remote is missing our value, sending it to them",
|
|
415
420
|
});
|
|
@@ -471,14 +476,14 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
471
476
|
}
|
|
472
477
|
// Our latest valid = server does not have & it's latest valid is older than ours
|
|
473
478
|
// - Send it our value
|
|
474
|
-
else if (
|
|
479
|
+
else if (responseMissing && compareTime(ourValue.time, epochTime) > 0) {
|
|
475
480
|
valuesToSend.push(ourValue);
|
|
476
481
|
trackSyncAge({
|
|
477
482
|
path: response.path,
|
|
478
483
|
ourTimeId: ourValue.time.time,
|
|
479
|
-
remoteTimeId:
|
|
484
|
+
remoteTimeId: undefined,
|
|
480
485
|
ourValid: ourValue.valid ?? false,
|
|
481
|
-
remoteValid:
|
|
486
|
+
remoteValid: undefined,
|
|
482
487
|
remoteNodeId: nodeId,
|
|
483
488
|
reason: "Remote is missing our value, sending it to them",
|
|
484
489
|
});
|
|
@@ -535,12 +540,13 @@ class PathAuditerService {
|
|
|
535
540
|
} else {
|
|
536
541
|
value = authorityStorage.getValueAtOrBeforeTime(request.path);
|
|
537
542
|
}
|
|
543
|
+
value = value || createMissingEpochValue(request.path);
|
|
538
544
|
results.push({
|
|
539
545
|
path: request.path,
|
|
540
|
-
time: value
|
|
541
|
-
valid: value
|
|
542
|
-
isTransparent: !!value
|
|
543
|
-
event: !!value
|
|
546
|
+
time: value.time,
|
|
547
|
+
valid: value.valid,
|
|
548
|
+
isTransparent: !!value.isTransparent,
|
|
549
|
+
event: !!value.event,
|
|
544
550
|
});
|
|
545
551
|
}
|
|
546
552
|
return results;
|
|
@@ -8,8 +8,11 @@ export const MEMORY_WARNING_THRESHOLD = 0.7;
|
|
|
8
8
|
export const MEMORY_ERROR_THRESHOLD = 0.85;
|
|
9
9
|
export const DISK_WARNING_THRESHOLD = 0.8;
|
|
10
10
|
export const DISK_ERROR_THRESHOLD = 0.92;
|
|
11
|
+
// Swap in use at all means memory pressure already happened, so warn much earlier than RAM.
|
|
12
|
+
export const SWAP_WARNING_THRESHOLD = 0.25;
|
|
13
|
+
export const SWAP_ERROR_THRESHOLD = 0.7;
|
|
11
14
|
|
|
12
|
-
/** The standard thresholds for the machine resource bar types ("MEMORY" / "DISK"), so every page showing these bars warns identically. */
|
|
15
|
+
/** The standard thresholds for the machine resource bar types ("MEMORY" / "DISK" / "SWAP"), so every page showing these bars warns identically. */
|
|
13
16
|
export function getUsageThresholds(type: string): { warningThreshold?: number; errorThreshold?: number } {
|
|
14
17
|
if (type === "MEMORY") {
|
|
15
18
|
return { warningThreshold: MEMORY_WARNING_THRESHOLD, errorThreshold: MEMORY_ERROR_THRESHOLD };
|
|
@@ -17,6 +20,9 @@ export function getUsageThresholds(type: string): { warningThreshold?: number; e
|
|
|
17
20
|
if (type === "DISK") {
|
|
18
21
|
return { warningThreshold: DISK_WARNING_THRESHOLD, errorThreshold: DISK_ERROR_THRESHOLD };
|
|
19
22
|
}
|
|
23
|
+
if (type === "SWAP") {
|
|
24
|
+
return { warningThreshold: SWAP_WARNING_THRESHOLD, errorThreshold: SWAP_ERROR_THRESHOLD };
|
|
25
|
+
}
|
|
20
26
|
return {};
|
|
21
27
|
}
|
|
22
28
|
|