geonix 1.32.0 → 1.32.2
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/README.md +1 -0
- package/package.json +1 -1
- package/src/Registry.js +15 -5
- package/src/Request.js +49 -44
- package/src/Util.js +9 -0
package/README.md
CHANGED
|
@@ -295,6 +295,7 @@ Each returned part has:
|
|
|
295
295
|
| `GX_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
|
296
296
|
| `GX_STREAM_TIMEOUT` | `90000` | Max wait for a stream consumer to connect (ms) |
|
|
297
297
|
| `GX_INACTIVITY_TIMEOUT` | `90000` | TCP proxy (HTTP-over-NATS) inactivity timeout (ms) |
|
|
298
|
+
| `GX_HEALTH_TIMEOUT` | `2000` | Registry health-probe timeout per advertised address (ms) |
|
|
298
299
|
| `GX_SECRET` | — | Encryption key: AES-256-GCM payloads + HMAC-SHA256 subjects. Services without the same key cannot communicate. |
|
|
299
300
|
| `GX_DEBUG_ENDPOINT` | — | Mount path for the debug router (e.g. `/_debug`). Disabled when unset. |
|
|
300
301
|
|
package/package.json
CHANGED
package/src/Registry.js
CHANGED
|
@@ -8,6 +8,11 @@ import { logger } from "./Logger.js";
|
|
|
8
8
|
|
|
9
9
|
const REGISTRY_ENTRY_TIMEOUT = 5000;
|
|
10
10
|
const GARBAGE_COLLECTOR_INTERVAL = 500;
|
|
11
|
+
const HEALTH_TIMEOUT = 2000;
|
|
12
|
+
|
|
13
|
+
// Read via function so tests can mutate the env after module load. Mirrors the
|
|
14
|
+
// pattern used by getTransportTimeout / getStreamTimeout (per CLAUDE.md N6).
|
|
15
|
+
const getHealthTimeout = () => parseInt(process.env.GX_HEALTH_TIMEOUT, 10) || HEALTH_TIMEOUT;
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Maintains a local, in-process view of all services currently present on the NATS bus.
|
|
@@ -43,7 +48,7 @@ class Registry extends EventEmitter {
|
|
|
43
48
|
this.#isActive = false;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
async #checkHealth(address, addresses, onFirstHealthy, timeout =
|
|
51
|
+
async #checkHealth(address, addresses, onFirstHealthy, timeout = getHealthTimeout()) {
|
|
47
52
|
try {
|
|
48
53
|
const result = await (await fetchWithTimeout(`http://${address}/!!_gx/health`, {}, timeout)).json();
|
|
49
54
|
|
|
@@ -137,17 +142,22 @@ class Registry extends EventEmitter {
|
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
// Refresh timeout for a known entry;
|
|
141
|
-
//
|
|
145
|
+
// Refresh timeout for a known entry; kick off a re-probe when either (a) the advertised
|
|
146
|
+
// list changed, or (b) the entry's healthy `a` is empty and the source is still advertising
|
|
147
|
+
// addresses — that's the "all probes failed at registration" case that needs to retry.
|
|
142
148
|
#refreshKnown(entry, advertised) {
|
|
143
149
|
entry.timeout = Date.now() + REGISTRY_ENTRY_TIMEOUT;
|
|
144
150
|
if (!Array.isArray(advertised)) {
|
|
145
151
|
return;
|
|
146
152
|
}
|
|
147
|
-
|
|
153
|
+
const changed = !this.#sameAddresses(entry.advertised ?? [], advertised);
|
|
154
|
+
const stuckEmpty = (entry.a?.length ?? 0) === 0 && advertised.length > 0;
|
|
155
|
+
if (!changed && !stuckEmpty) {
|
|
148
156
|
return;
|
|
149
157
|
}
|
|
150
|
-
|
|
158
|
+
if (changed) {
|
|
159
|
+
entry.advertised = [...advertised];
|
|
160
|
+
}
|
|
151
161
|
this.#reprobe(entry).catch((e) => logger.error("registry.reprobe:", e));
|
|
152
162
|
}
|
|
153
163
|
|
package/src/Request.js
CHANGED
|
@@ -73,11 +73,16 @@ export async function Request(service, method, args, context, options) {
|
|
|
73
73
|
return directRequest(identifier, method, args, context, options, service);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
let _httpInstanceRoundRobin = 0;
|
|
76
77
|
let _httpRpcRoundRobin = 0;
|
|
77
78
|
|
|
79
|
+
const DEFAULT_HTTP_TIMEOUT = 300_000;
|
|
80
|
+
|
|
78
81
|
/**
|
|
79
82
|
* Sends a request directly to a resolved registry identifier without performing a registry
|
|
80
|
-
* lookup. Prefers HTTP RPC for
|
|
83
|
+
* lookup. Prefers HTTP RPC; for multi-instance services round-robins across instances that
|
|
84
|
+
* have advertised addresses. Falls back to NATS when no instance has addresses or the HTTP
|
|
85
|
+
* attempt fails.
|
|
81
86
|
*
|
|
82
87
|
* @param {string} identifier - Registry identifier in the form `"Name@version"` or an instance ID.
|
|
83
88
|
* @param {string} method - Method name to invoke on the service.
|
|
@@ -92,53 +97,53 @@ export async function directRequest(identifier, method, args, context, options,
|
|
|
92
97
|
const originator = rpcContext.getStore()?.originator;
|
|
93
98
|
const requestBegin = Date.now();
|
|
94
99
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
100
|
+
// HTTP path: round-robin across instances that have advertised addresses; within the
|
|
101
|
+
// chosen instance, round-robin across addresses (loopback-prefer kept by isLoopbackAddress).
|
|
102
|
+
// NATS handles the fallback when no usable instance exists or the HTTP attempt fails.
|
|
97
103
|
const entries = registry.getEntriesForIdentifier(identifier);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
logger.debug("directRequest: HTTP RPC failed, falling back to NATS", address, e.message);
|
|
104
|
+
const usable = entries.filter((e) => e.a?.length > 0);
|
|
105
|
+
if (usable.length > 0) {
|
|
106
|
+
const instance = usable[_httpInstanceRoundRobin++ % usable.length];
|
|
107
|
+
const loopback = instance.a.filter(isLoopbackAddress);
|
|
108
|
+
const remote = instance.a.filter((a) => !isLoopbackAddress(a));
|
|
109
|
+
const pool = loopback.length > 0 ? loopback : remote;
|
|
110
|
+
const address = pool[_httpRpcRoundRobin++ % pool.length];
|
|
111
|
+
|
|
112
|
+
const url = `http://${address}/!!_gx/rpc/${hash(instance.i)}`;
|
|
113
|
+
const rpcPayload = { m: method, a: args, c: context, o: originator };
|
|
114
|
+
const fetchBody = _payloadKey
|
|
115
|
+
? encryptPayload(Buffer.from(JSON.stringify(rpcPayload)))
|
|
116
|
+
: JSON.stringify(rpcPayload);
|
|
117
|
+
const contentType = _payloadKey ? "application/octet-stream" : "application/json";
|
|
118
|
+
|
|
119
|
+
let httpResponse;
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetchWithTimeout(
|
|
122
|
+
url,
|
|
123
|
+
{
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "content-type": contentType },
|
|
126
|
+
body: fetchBody,
|
|
127
|
+
},
|
|
128
|
+
options?.httpTimeout ?? DEFAULT_HTTP_TIMEOUT,
|
|
129
|
+
);
|
|
130
|
+
if (res.ok) {
|
|
131
|
+
httpResponse = _payloadKey
|
|
132
|
+
? JSON.parse(decryptPayload(Buffer.from(await res.arrayBuffer())))
|
|
133
|
+
: await res.json();
|
|
131
134
|
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
logger.debug("directRequest: HTTP RPC failed, falling back to NATS", address, e.message);
|
|
137
|
+
}
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
return httpResponse.r;
|
|
139
|
+
if (httpResponse) {
|
|
140
|
+
if (httpResponse.e) {
|
|
141
|
+
throw Error(`Request: remote error: ${httpResponse.e}`);
|
|
142
|
+
}
|
|
143
|
+
if (isStream(httpResponse.r)) {
|
|
144
|
+
return JSON.parse(await streamToString(httpResponse.r));
|
|
141
145
|
}
|
|
146
|
+
return httpResponse.r;
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
|
package/src/Util.js
CHANGED
|
@@ -211,6 +211,15 @@ export function getNetworkAddresses() {
|
|
|
211
211
|
for (const interfaceAddresses of Object.values(interfaces)) {
|
|
212
212
|
if (!interfaceAddresses) { continue; }
|
|
213
213
|
for (const addressObject of interfaceAddresses) {
|
|
214
|
+
// Skip IPv6 link-local addresses (fe80::/10). They require a zone identifier
|
|
215
|
+
// (e.g. %eth0) to route, which `fetch()` doesn't accept — advertising them
|
|
216
|
+
// just wastes a probe slot.
|
|
217
|
+
if (
|
|
218
|
+
addressObject.family === "IPv6" &&
|
|
219
|
+
addressObject.address.toLowerCase().startsWith("fe80:")
|
|
220
|
+
) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
214
223
|
const addr = addressObject.family === "IPv4"
|
|
215
224
|
? addressObject.address
|
|
216
225
|
: addressObject.family === "IPv6"
|