@onlineapps/conn-orch-registry 3.0.1 → 4.0.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/README.md +149 -19
- package/docs/REGISTRY_CLIENT_GUIDE.md +5 -5
- package/examples/basicUsage.js +14 -3
- package/examples/event-consumer-example.js +10 -23
- package/package.json +14 -20
- package/src/events.js +2 -2
- package/src/index.js +0 -1
- package/src/queueManager.js +168 -123
- package/src/registryClient.js +358 -281
- package/src/registryEventConsumer.js +107 -52
package/src/registryClient.js
CHANGED
|
@@ -22,15 +22,16 @@
|
|
|
22
22
|
* @see api/docs/biz/30-operations/registration-wire.md §2 MQ wire format
|
|
23
23
|
* @see api/docs/biz/30-operations/schema-v3.md
|
|
24
24
|
*
|
|
25
|
-
* @module @onlineapps/
|
|
25
|
+
* @module @onlineapps/conn-orch-registry/src/registryClient
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
const EventEmitter = require('events');
|
|
29
29
|
const QueueManager = require('./queueManager');
|
|
30
30
|
const RegistryEventConsumer = require('./registryEventConsumer');
|
|
31
31
|
const { v4: uuidv4 } = require('uuid');
|
|
32
|
-
const queueConfig = require('@onlineapps/mq-client-core
|
|
32
|
+
const { queueConfig } = require('@onlineapps/mq-client-core');
|
|
33
33
|
const DEFAULTS = require('./defaults');
|
|
34
|
+
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class ServiceRegistryClient extends EventEmitter {
|
|
@@ -39,23 +40,42 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
39
40
|
* @param {string} opts.amqpUrl - AMQP URI for connecting to RabbitMQ
|
|
40
41
|
* @param {string} opts.serviceName - Name of the service (e.g., 'invoicing')
|
|
41
42
|
* @param {string} opts.version - Version of the service (e.g., '1.2.0')
|
|
42
|
-
* @param {string}
|
|
43
|
-
*
|
|
43
|
+
* @param {string} opts.specificationEndpoint - Path the `register` and `heartbeat` messages
|
|
44
|
+
* carry. REQUIRED: the wire format declares it required
|
|
45
|
+
* (api/docs/biz/30-operations/registration-wire.md §2.1) and the registry stores what it
|
|
46
|
+
* is sent, so a default here would write a path nobody configured into `registry:services`.
|
|
47
|
+
* @param {number} opts.heartbeatInterval - Milliseconds between heartbeats. REQUIRED: the
|
|
48
|
+
* cadence has ONE owner and it is not a library default (owner confirmation
|
|
49
|
+
* docs/governance/confirmations/biz-health-freshness.md 001 point 3); ServiceWrapper
|
|
50
|
+
* passes `wrapper.registry.heartbeatInterval`, itself fed from BIZ_HEARTBEAT_INTERVAL_MS.
|
|
44
51
|
* @param {string} [opts.apiQueue='api_services_queuer'] - Queue name for heartbeat and API traffic
|
|
45
52
|
* @param {string} [opts.registryQueue='registry.register'] - Queue name for registry messages
|
|
46
53
|
* @param {Object} [opts.validationProof=null] - Validation proof object {hash, data} (injected from outside)
|
|
47
54
|
* @param {number} [opts.registrationTimeoutMs] - How long register() waits for the registry response
|
|
48
55
|
* before rejecting. Default comes from ./defaults.js (30000 ms); never read from the environment here.
|
|
56
|
+
* @param {Object} opts.logger - Logger with info/warn/error/debug. Required and validated here:
|
|
57
|
+
* registration, heartbeat and revalidation events are what this client exists to report, and
|
|
58
|
+
* until 2026-09-05 they went to stdout through bare printing while this parameter was accepted
|
|
59
|
+
* and never read. Owner confirmation:
|
|
60
|
+
* docs/governance/confirmations/connector-logger-contract.md 001 + 002.
|
|
49
61
|
*/
|
|
50
|
-
constructor({ amqpUrl, serviceName, version, specificationEndpoint
|
|
51
|
-
heartbeatInterval
|
|
52
|
-
|
|
62
|
+
constructor({ amqpUrl, serviceName, version, specificationEndpoint,
|
|
63
|
+
heartbeatInterval, apiQueue = 'api_services_queuer', registryQueue = 'registry.register',
|
|
64
|
+
redis = null, storageConfig = {}, validationProof = null, logger,
|
|
53
65
|
registryKeyPrefix = DEFAULTS.registryKeyPrefix,
|
|
54
66
|
registrationTimeoutMs = DEFAULTS.registrationTimeoutMs }) {
|
|
55
67
|
super();
|
|
56
68
|
if (!amqpUrl || !serviceName || !version) {
|
|
57
|
-
throw new Error(
|
|
69
|
+
throw new Error(
|
|
70
|
+
'[RegistryClient] Missing constructor options - amqpUrl, serviceName, and version are required. '
|
|
71
|
+
+ 'Fix: pass all three to new ServiceRegistryClient({ amqpUrl, serviceName, version, logger }).'
|
|
72
|
+
);
|
|
58
73
|
}
|
|
74
|
+
this.logger = assertLogger(
|
|
75
|
+
'RegistryClient',
|
|
76
|
+
logger,
|
|
77
|
+
'registration, heartbeat and revalidation events reach the service log'
|
|
78
|
+
);
|
|
59
79
|
if (typeof registrationTimeoutMs !== 'number' || !Number.isFinite(registrationTimeoutMs) || registrationTimeoutMs <= 0) {
|
|
60
80
|
throw new Error(
|
|
61
81
|
`[RegistryClient] ${serviceName}: Invalid registrationTimeoutMs - expected a positive number of milliseconds, `
|
|
@@ -66,13 +86,35 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
66
86
|
this.registrationTimeoutMs = registrationTimeoutMs;
|
|
67
87
|
this.serviceName = serviceName;
|
|
68
88
|
this.version = version;
|
|
69
|
-
//
|
|
89
|
+
// Both of these were module defaults until d.278c, and both were statements this
|
|
90
|
+
// package had no standing to make (architecture-principles §3, no fallbacks).
|
|
91
|
+
if (typeof specificationEndpoint !== 'string' || specificationEndpoint === '') {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[RegistryClient] ${serviceName}: Missing constructor option - specificationEndpoint is required, `
|
|
94
|
+
+ `got ${JSON.stringify(specificationEndpoint)}. Expected: the path the register message must carry `
|
|
95
|
+
+ '(api/docs/biz/30-operations/registration-wire.md §2.1 declares it required). '
|
|
96
|
+
+ 'Fix: pass service.specificationEndpoint from the service configuration; nothing may substitute a path for it.'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (heartbeatInterval === undefined) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[RegistryClient] ${serviceName}: Missing constructor option - heartbeatInterval is required. `
|
|
102
|
+
+ 'Expected: the cadence its one owner decided (docs/governance/confirmations/biz-health-freshness.md 001 '
|
|
103
|
+
+ 'point 3), passed as a positive number of milliseconds. '
|
|
104
|
+
+ 'Fix: pass wrapper.registry.heartbeatInterval (ServiceWrapper reads it from BIZ_HEARTBEAT_INTERVAL_MS).'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (typeof heartbeatInterval !== 'number' || !Number.isFinite(heartbeatInterval) || heartbeatInterval <= 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`[RegistryClient] ${serviceName}: Invalid heartbeatInterval - expected a positive number of milliseconds, `
|
|
110
|
+
+ `got ${JSON.stringify(heartbeatInterval)}. Fix: pass the cadence as a number; a string is not parsed here.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
70
113
|
this.specificationEndpoint = specificationEndpoint;
|
|
71
114
|
this.heartbeatInterval = heartbeatInterval;
|
|
72
115
|
this.apiQueue = apiQueue;
|
|
73
116
|
this.registryQueue = registryQueue;
|
|
74
|
-
this.
|
|
75
|
-
this.queueManager = new QueueManager(amqpUrl, serviceName);
|
|
117
|
+
this.queueManager = new QueueManager(amqpUrl, serviceName, this.logger);
|
|
76
118
|
this.heartbeatTimer = null;
|
|
77
119
|
|
|
78
120
|
// Event consumer (optional, activated via subscribeToChanges)
|
|
@@ -93,119 +135,131 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
93
135
|
async init() {
|
|
94
136
|
await this.queueManager.init();
|
|
95
137
|
|
|
96
|
-
// Validation proof is now injected via constructor (not loaded here)
|
|
138
|
+
// Validation proof is now injected via constructor (not loaded here).
|
|
139
|
+
// One line either way: a proof is the normal case (info), its absence means the
|
|
140
|
+
// registry falls back to Tier 2 validation, which is a degraded mode (warn).
|
|
97
141
|
if (this.validationProof) {
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
this.logger.info('[RegistryClient] Validation proof provided via constructor', {
|
|
143
|
+
serviceName: this.serviceName,
|
|
144
|
+
hasValidationProof: true,
|
|
145
|
+
proofHash: this.validationProof.validationProof.substring(0, 32)
|
|
146
|
+
});
|
|
100
147
|
} else {
|
|
101
|
-
|
|
148
|
+
this.logger.warn('[RegistryClient] No validation proof - registry will run Tier 2 validation', {
|
|
149
|
+
serviceName: this.serviceName,
|
|
150
|
+
hasValidationProof: false
|
|
151
|
+
});
|
|
102
152
|
}
|
|
103
153
|
|
|
104
|
-
// FÁZE 0.5:
|
|
105
|
-
|
|
154
|
+
// FÁZE 0.5 + 0.6: the response queue and its consumer, in one step.
|
|
155
|
+
//
|
|
156
|
+
// The queue is NOT asserted here any more. `consume()` in the core declares a
|
|
157
|
+
// business queue itself, with the arguments `queueConfig` owns
|
|
158
|
+
// (`transports/rabbitmqClient.js` § _prepareQueueForConsume), so a separate
|
|
159
|
+
// `ensureQueues()` call ahead of it was a second declaration of one queue —
|
|
160
|
+
// agreeing today, and 406 PRECONDITION-FAILED the day the two drift apart
|
|
161
|
+
// (`.claude/rules/change-discipline.md` § One rail per concern).
|
|
162
|
+
const consumerStartTime = Date.now();
|
|
106
163
|
this.serviceRegistryQueue = `${this.serviceName}.registry`;
|
|
107
|
-
console.log(`[FÁZE 0.5] Service Queue Creation - STARTING`);
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
// Ensure existence of service-specific queue only (infrastructure queues must already exist)
|
|
111
|
-
await this.queueManager.ensureQueues([this.serviceRegistryQueue]);
|
|
112
|
-
console.log(`[FÁZE 0.5] Service Queue Creation - PASSED (${Date.now() - queueCreationStartTime}ms)`);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error(`[FÁZE 0.5] Service Queue Creation - FAILED: ${error.message}`);
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
164
|
|
|
118
|
-
// FÁZE 0.6: Spuštění konzumerů
|
|
119
|
-
const consumerStartTime = Date.now();
|
|
120
|
-
console.log(`[FÁZE 0.6] Consumer Startup - STARTING`);
|
|
121
|
-
|
|
122
|
-
// CRITICAL: Before consume(), we must assertQueue with correct parameters
|
|
123
|
-
// amqplib's channel.consume() may internally call assertQueue() WITHOUT parameters
|
|
124
|
-
// This causes 406 PRECONDITION-FAILED if queue exists with different arguments
|
|
125
|
-
console.log(`[RegistryClient] [CONSUMER] About to consume from ${this.serviceRegistryQueue}`);
|
|
126
|
-
console.log(`[RegistryClient] [CONSUMER] ⚠ WARNING: amqplib's channel.consume() may internally call assertQueue() WITHOUT parameters`);
|
|
127
|
-
console.log(`[RegistryClient] [CONSUMER] ⚠ WARNING: Queue should already be asserted by ensureQueues() above with correct parameters from queueConfig.js`);
|
|
128
|
-
|
|
129
|
-
// Start consuming service registry queue for registry responses and events
|
|
130
165
|
const CONSUME_TIMEOUT = 5000; // 5 seconds
|
|
131
|
-
|
|
166
|
+
// `requeueOnError: false` is the budget this consumer has always had, said in
|
|
167
|
+
// the core's word: ONE attempt. The difference is where a failure goes — the
|
|
168
|
+
// hand-written `nack(msg, false, false)` this replaces asked the broker to drop
|
|
169
|
+
// the message, while the policy rejects it into `<service>.dlq` and publishes a
|
|
170
|
+
// `message_dlq` event. The handler reports failure by THROWING; it settles
|
|
171
|
+
// nothing itself (@onlineapps/mq-client-core README § consume() — the delivery
|
|
172
|
+
// contract).
|
|
173
|
+
const consumeRegistryPromise = this.queueManager.consume(
|
|
132
174
|
this.serviceRegistryQueue,
|
|
133
|
-
msg =>
|
|
134
|
-
|
|
135
|
-
if (!msg) {
|
|
136
|
-
console.warn(`[RegistryClient] ${this.serviceName}: Received null message from ${this.serviceRegistryQueue} (queue may be deleted or connection closed)`);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
this._handleRegistryMessage(msg);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
console.error(`[RegistryClient] ${this.serviceName}: Error in consume callback:`, error);
|
|
143
|
-
// Nack message on error
|
|
144
|
-
try {
|
|
145
|
-
this.queueManager.channel.nack(msg, false, false);
|
|
146
|
-
} catch (nackErr) {
|
|
147
|
-
console.error(`[RegistryClient] ${this.serviceName}: Failed to nack message:`, nackErr);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
{ noAck: false }
|
|
175
|
+
msg => this._handleRegistryMessage(msg),
|
|
176
|
+
{ requeueOnError: false }
|
|
152
177
|
);
|
|
153
178
|
let consumeRegistryTimeout;
|
|
154
179
|
const consumeRegistryTimeoutPromise = new Promise((_, reject) => {
|
|
155
180
|
consumeRegistryTimeout = setTimeout(() => {
|
|
156
|
-
reject(new Error(
|
|
181
|
+
reject(new Error(
|
|
182
|
+
`[RegistryClient] consume() timeout for ${this.serviceRegistryQueue} after ${CONSUME_TIMEOUT}ms - `
|
|
183
|
+
+ 'the broker never confirmed the consumer. Fix: check RabbitMQ responsiveness and that the queue exists.'
|
|
184
|
+
));
|
|
157
185
|
}, CONSUME_TIMEOUT);
|
|
158
186
|
});
|
|
159
187
|
|
|
160
188
|
try {
|
|
161
189
|
await Promise.race([consumeRegistryPromise, consumeRegistryTimeoutPromise]);
|
|
162
|
-
console.log(`[FÁZE 0.6] Consumer Startup - PASSED (${Date.now() - consumerStartTime}ms)`);
|
|
163
190
|
} catch (consumeErr) {
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
// Not logged before throwing: the thrown message already names the service,
|
|
192
|
+
// the queue and the cause.
|
|
193
|
+
throw new Error(
|
|
194
|
+
`[RegistryClient] ${this.serviceName}: Failed to start consumer on ${this.serviceRegistryQueue} - `
|
|
195
|
+
+ `${consumeErr.message}. Fix: check RabbitMQ health; without this consumer no registry reply is ever read.`,
|
|
196
|
+
{ cause: consumeErr }
|
|
197
|
+
);
|
|
166
198
|
} finally {
|
|
167
199
|
if (consumeRegistryTimeout) clearTimeout(consumeRegistryTimeout);
|
|
168
200
|
}
|
|
201
|
+
|
|
202
|
+
// One line for the whole boot of this client: which queues it talks on, and how
|
|
203
|
+
// long the consumer took to attach. Replaces the four FÁZE 0.5/0.6 markers.
|
|
204
|
+
this.logger.info('[RegistryClient] Initialized', {
|
|
205
|
+
serviceName: this.serviceName,
|
|
206
|
+
version: this.version,
|
|
207
|
+
registryQueue: this.registryQueue,
|
|
208
|
+
responseQueue: this.serviceRegistryQueue,
|
|
209
|
+
consumerStartupMs: Date.now() - consumerStartTime
|
|
210
|
+
});
|
|
169
211
|
}
|
|
170
212
|
|
|
171
213
|
/**
|
|
172
214
|
* Internal handler for incoming messages from the registry queue.
|
|
173
215
|
* Handles registration responses and revalidation requests.
|
|
216
|
+
*
|
|
217
|
+
* Settling is NOT this method's business (d.198b-3). A return means "handled"
|
|
218
|
+
* and the core acks once; a THROW means "not handled" and the core applies the
|
|
219
|
+
* delivery policy the consumer was registered with — one attempt, then
|
|
220
|
+
* `<service>.dlq` with a `message_dlq` event. The hand-written
|
|
221
|
+
* `channel.ack` / `channel.nack(msg, false, false)` that used to end this method
|
|
222
|
+
* asked the broker to DROP a message it could not read.
|
|
223
|
+
*
|
|
174
224
|
* @param {Object} msg - AMQP message
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
* @throws {Error} If the payload is not readable as a registry message
|
|
175
227
|
* @private
|
|
176
228
|
*/
|
|
177
|
-
_handleRegistryMessage(msg) {
|
|
178
|
-
//
|
|
179
|
-
if (!msg) {
|
|
180
|
-
console.warn(`[RegistryClient] ${this.serviceName}: Received null message (queue may be deleted or connection closed)`);
|
|
181
|
-
return; // Don't try to parse or ack null message
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Log which queue this message came from (if available in msg properties)
|
|
229
|
+
async _handleRegistryMessage(msg) {
|
|
230
|
+
// Which queue this message came from (if available in msg properties)
|
|
185
231
|
const queueName = msg.fields?.routingKey || msg.fields?.exchange || 'unknown';
|
|
186
|
-
|
|
187
|
-
|
|
232
|
+
|
|
188
233
|
let payload;
|
|
189
234
|
try {
|
|
190
235
|
payload = JSON.parse(msg.content.toString());
|
|
191
|
-
|
|
236
|
+
// One line per inbound message, after parsing: the "processing from queue"
|
|
237
|
+
// line that used to precede it said nothing this one does not.
|
|
238
|
+
this.logger.debug('[RegistryClient] Registry message received', {
|
|
239
|
+
serviceName: this.serviceName,
|
|
240
|
+
queue: queueName,
|
|
241
|
+
type: payload.type,
|
|
242
|
+
requestId: payload.requestId
|
|
243
|
+
});
|
|
192
244
|
} catch (err) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
245
|
+
// Not logged before throwing, and NOT emitted as an 'error' event either: an
|
|
246
|
+
// EventEmitter with no 'error' listener RETHROWS what is emitted, which would
|
|
247
|
+
// turn a delivery the core knows how to reject into an uncatchable crash of the
|
|
248
|
+
// service. The rejection below is the one report, and the core turns it into a
|
|
249
|
+
// dead-letter plus a `message_dlq` event (connector-logger-contract 003).
|
|
250
|
+
throw new Error(
|
|
251
|
+
`[RegistryClient] Failed to parse registry message from ${queueName} - ${err.message}. `
|
|
252
|
+
+ 'Expected: JSON written by api_services_registry. '
|
|
253
|
+
+ 'Fix: the message is rejected into the dead-letter queue unread; read it there '
|
|
254
|
+
+ 'through the DLQ dashboard to see what was published.',
|
|
255
|
+
{ cause: err }
|
|
256
|
+
);
|
|
204
257
|
}
|
|
205
258
|
|
|
206
259
|
// Handle revalidation request from registry (infra version change)
|
|
207
260
|
if (payload.type === 'revalidate') {
|
|
208
|
-
|
|
261
|
+
this.logger.info('[RegistryClient] Revalidation requested by the registry', {
|
|
262
|
+
serviceName: this.serviceName,
|
|
209
263
|
reason: payload.reason,
|
|
210
264
|
infraFingerprint: payload.infraFingerprint?.substring(0, 16)
|
|
211
265
|
});
|
|
@@ -215,17 +269,16 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
215
269
|
// Handle registration response from registry
|
|
216
270
|
// Registry sends 'register.confirmed' after validation
|
|
217
271
|
if ((payload.type === 'registerResponse' || payload.type === 'register.confirmed') && payload.requestId) {
|
|
218
|
-
|
|
272
|
+
this.logger.info('[RegistryClient] Registration response received', {
|
|
273
|
+
serviceName: this.serviceName,
|
|
274
|
+
type: payload.type,
|
|
219
275
|
requestId: payload.requestId,
|
|
220
|
-
|
|
276
|
+
queue: queueName,
|
|
221
277
|
pendingCount: this.pendingRegistrations ? this.pendingRegistrations.size : 0,
|
|
222
|
-
|
|
223
|
-
pendingKeys: this.pendingRegistrations ? Array.from(this.pendingRegistrations.keys()) : [],
|
|
224
|
-
queueName
|
|
278
|
+
matched: !!(this.pendingRegistrations && this.pendingRegistrations.has(payload.requestId))
|
|
225
279
|
});
|
|
226
280
|
|
|
227
281
|
if (this.pendingRegistrations && this.pendingRegistrations.has(payload.requestId)) {
|
|
228
|
-
console.log(`[RegistryClient] ${this.serviceName}: [REGISTRATION] Found matching pending registration, extracting resolve function...`);
|
|
229
282
|
const { resolve } = this.pendingRegistrations.get(payload.requestId);
|
|
230
283
|
|
|
231
284
|
// Clear associated timeout
|
|
@@ -235,18 +288,20 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
235
288
|
}
|
|
236
289
|
|
|
237
290
|
this.pendingRegistrations.delete(payload.requestId);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
291
|
+
|
|
292
|
+
// What the registry actually answered, in one structured record. Nine
|
|
293
|
+
// stdout lines printed these same fields one per line.
|
|
294
|
+
this.logger.debug('[RegistryClient] Registry verdict payload', {
|
|
295
|
+
serviceName: this.serviceName,
|
|
296
|
+
requestId: payload.requestId,
|
|
297
|
+
payloadKeys: Object.keys(payload),
|
|
298
|
+
success: payload.success,
|
|
299
|
+
validated: payload.validated,
|
|
300
|
+
errorCode: payload.errorCode,
|
|
301
|
+
hasCertificate: !!payload.certificate,
|
|
302
|
+
certificateId: payload.certificate?.id,
|
|
303
|
+
certificateServiceName: payload.certificate?.serviceName
|
|
304
|
+
});
|
|
250
305
|
|
|
251
306
|
// Resolve the registration promise with the response
|
|
252
307
|
const registrationResult = {
|
|
@@ -256,50 +311,71 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
256
311
|
version: this.version,
|
|
257
312
|
registrationId: payload.registrationId,
|
|
258
313
|
validated: payload.validated || payload.success, // Consider successful registration as validated
|
|
259
|
-
certificate: payload.certificate || null // Include certificate from Registry
|
|
314
|
+
certificate: payload.certificate || null, // Include certificate from Registry
|
|
315
|
+
// WHY the registry decided as it did, in a stable machine code the caller
|
|
316
|
+
// can branch on instead of matching the message text. Passed through
|
|
317
|
+
// verbatim - no default, no fallback (architecture-principles §3): the
|
|
318
|
+
// explicit `null` the registry sends on a confirmed registration stays
|
|
319
|
+
// null, and a payload carrying no code at all leaves this undefined.
|
|
320
|
+
// @see api/docs/biz/30-operations/registration-wire.md §8
|
|
321
|
+
errorCode: payload.errorCode,
|
|
322
|
+
// The parked registration of §2.5 — "you have not pre-validated yet" — is
|
|
323
|
+
// not a refusal and carries no errorCode; what it carries is the state and
|
|
324
|
+
// what to run about it. Both were read off the wire and dropped here until
|
|
325
|
+
// d.278c, so a caller saw `success: false` and nothing that explained it.
|
|
326
|
+
// Verbatim, like errorCode: a confirmed registration sends neither key and
|
|
327
|
+
// this leaves both undefined rather than inventing a status.
|
|
328
|
+
// @see api/docs/biz/30-operations/registration-wire.md §2.5
|
|
329
|
+
validationStatus: payload.validationStatus,
|
|
330
|
+
instructions: payload.instructions
|
|
260
331
|
};
|
|
261
|
-
|
|
262
|
-
console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] Created registrationResult:`, JSON.stringify({
|
|
263
|
-
success: registrationResult.success,
|
|
264
|
-
validated: registrationResult.validated,
|
|
265
|
-
hasCertificate: !!registrationResult.certificate,
|
|
266
|
-
certificateId: registrationResult.certificate?.id || 'none',
|
|
267
|
-
serviceName: registrationResult.serviceName,
|
|
268
|
-
version: registrationResult.version
|
|
269
|
-
}));
|
|
270
|
-
|
|
271
|
-
console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] About to call resolve() function...`);
|
|
272
|
-
console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] resolve function type:`, typeof resolve);
|
|
273
|
-
console.log(`[RegistryClient] ${this.serviceName}: [RESOLVE] resolve function exists:`, !!resolve);
|
|
274
|
-
|
|
332
|
+
|
|
275
333
|
try {
|
|
276
334
|
// CRITICAL: Call resolve synchronously - this must happen immediately
|
|
277
335
|
resolve(registrationResult);
|
|
278
|
-
|
|
279
|
-
|
|
336
|
+
this.logger.info('[RegistryClient] Registration resolved', {
|
|
337
|
+
serviceName: this.serviceName,
|
|
338
|
+
requestId: payload.requestId,
|
|
339
|
+
success: registrationResult.success,
|
|
340
|
+
validated: registrationResult.validated,
|
|
341
|
+
hasCertificate: !!registrationResult.certificate,
|
|
342
|
+
certificateId: registrationResult.certificate?.id || null
|
|
343
|
+
});
|
|
280
344
|
} catch (resolveError) {
|
|
281
|
-
|
|
282
|
-
|
|
345
|
+
this.logger.error('[RegistryClient] Resolving the registration promise threw', {
|
|
346
|
+
serviceName: this.serviceName,
|
|
347
|
+
requestId: payload.requestId,
|
|
348
|
+
error: resolveError.message,
|
|
349
|
+
stack: resolveError.stack
|
|
350
|
+
});
|
|
283
351
|
throw resolveError;
|
|
284
352
|
}
|
|
285
353
|
} else {
|
|
286
|
-
|
|
354
|
+
this.logger.warn('[RegistryClient] Registration response has no matching pending request', {
|
|
355
|
+
serviceName: this.serviceName,
|
|
356
|
+
type: payload.type,
|
|
357
|
+
requestId: payload.requestId
|
|
358
|
+
});
|
|
287
359
|
}
|
|
288
360
|
}
|
|
289
361
|
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
this.queueManager.channel.ack(msg);
|
|
294
|
-
} catch (ackErr) {
|
|
295
|
-
console.error(`[RegistryClient] ${this.serviceName}: Failed to ack message:`, ackErr.message);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
362
|
+
// No ack here: returning IS the ack (d.198b-3). The core settles the delivery
|
|
363
|
+
// once the handler resolves, on the channel that delivered it.
|
|
298
364
|
}
|
|
299
365
|
|
|
300
366
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
367
|
+
* Confirms an infrastructure queue exists, without creating it.
|
|
368
|
+
*
|
|
369
|
+
* A consumer never creates an infrastructure queue: it is brought into being by
|
|
370
|
+
* the infrastructure service that owns it (`api/docs/standards/queue-ownership.md`),
|
|
371
|
+
* with the arguments the central `queueConfig` declares. What this client may do
|
|
372
|
+
* is ask whether it is there, so a publish that would otherwise vanish into the
|
|
373
|
+
* default exchange fails fast and says which queue is missing.
|
|
374
|
+
*
|
|
375
|
+
* The check goes through the core's queue channel (d.198b-3). Until then it opened
|
|
376
|
+
* a raw amqplib channel of its own, precisely because a 404 kills the channel it
|
|
377
|
+
* arrives on — the core owns that recreation now.
|
|
378
|
+
*
|
|
303
379
|
* @param {string} queueName
|
|
304
380
|
* @returns {Promise<void>}
|
|
305
381
|
* @private
|
|
@@ -309,7 +385,7 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
309
385
|
return;
|
|
310
386
|
}
|
|
311
387
|
|
|
312
|
-
if (!queueConfig
|
|
388
|
+
if (!queueConfig.isInfrastructureQueue(queueName)) {
|
|
313
389
|
return;
|
|
314
390
|
}
|
|
315
391
|
|
|
@@ -317,62 +393,28 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
317
393
|
return;
|
|
318
394
|
}
|
|
319
395
|
|
|
320
|
-
if (!this.queueManager
|
|
321
|
-
throw new Error(
|
|
396
|
+
if (!this.queueManager.isConnected()) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`[RegistryClient] ${this.serviceName}: MQ connection not ready - cannot verify infrastructure queue ${queueName}. `
|
|
399
|
+
+ 'Fix: complete init() first and check that RabbitMQ is accessible.'
|
|
400
|
+
);
|
|
322
401
|
}
|
|
323
402
|
|
|
324
|
-
// Add timeout to prevent hanging
|
|
325
|
-
const QUEUE_VERIFY_TIMEOUT = 10000; // 10 seconds
|
|
326
|
-
const verifyPromise = (async () => {
|
|
327
|
-
let verificationChannel;
|
|
328
|
-
let channelTimeout;
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
// Wrapped createChannel with timeout
|
|
332
|
-
const createChannelPromise = this.queueManager.conn.createChannel();
|
|
333
|
-
const createChannelTimeoutPromise = new Promise((_, reject) => {
|
|
334
|
-
channelTimeout = setTimeout(() => {
|
|
335
|
-
reject(new Error(`createChannel timeout after ${QUEUE_VERIFY_TIMEOUT / 2}ms`));
|
|
336
|
-
}, QUEUE_VERIFY_TIMEOUT / 2);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
verificationChannel = await Promise.race([createChannelPromise, createChannelTimeoutPromise]);
|
|
340
|
-
if (channelTimeout) clearTimeout(channelTimeout);
|
|
341
|
-
|
|
342
|
-
await verificationChannel.checkQueue(queueName);
|
|
343
|
-
this._verifiedInfraQueues.add(queueName);
|
|
344
|
-
} catch (error) {
|
|
345
|
-
if (error.code === 404) {
|
|
346
|
-
throw new Error(`[RegistryClient] Infrastructure queue '${queueName}' is missing. Ensure the responsible infrastructure service has initialized it.`);
|
|
347
|
-
}
|
|
348
|
-
throw error;
|
|
349
|
-
} finally {
|
|
350
|
-
if (channelTimeout) clearTimeout(channelTimeout);
|
|
351
|
-
if (verificationChannel) {
|
|
352
|
-
try {
|
|
353
|
-
await verificationChannel.close();
|
|
354
|
-
} catch (closeErr) {
|
|
355
|
-
console.warn(`[RegistryClient] ${this.serviceName}: Failed to close verification channel for ${queueName}:`, closeErr.message);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
})();
|
|
360
|
-
|
|
361
|
-
let verifyTimeout;
|
|
362
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
363
|
-
verifyTimeout = setTimeout(() => {
|
|
364
|
-
reject(new Error(`[RegistryClient] ${this.serviceName}: Timeout verifying infrastructure queue '${queueName}' after ${QUEUE_VERIFY_TIMEOUT}ms. Queue may not exist or RabbitMQ is unresponsive.`));
|
|
365
|
-
}, QUEUE_VERIFY_TIMEOUT);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
403
|
try {
|
|
369
|
-
await
|
|
404
|
+
await this.queueManager.checkQueue(queueName);
|
|
405
|
+
this._verifiedInfraQueues.add(queueName);
|
|
370
406
|
} catch (error) {
|
|
371
|
-
//
|
|
407
|
+
// Cleared so the next call asks again: a queue that was missing a second ago
|
|
408
|
+
// may have been created by its owner since.
|
|
372
409
|
this._verifiedInfraQueues.delete(queueName);
|
|
410
|
+
if (error.code === 404) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`[RegistryClient] Infrastructure queue '${queueName}' is missing - the broker answered 404 for it. `
|
|
413
|
+
+ 'Fix: start the infrastructure service that owns this queue before this service registers.',
|
|
414
|
+
{ cause: error }
|
|
415
|
+
);
|
|
416
|
+
}
|
|
373
417
|
throw error;
|
|
374
|
-
} finally {
|
|
375
|
-
if (verifyTimeout) clearTimeout(verifyTimeout);
|
|
376
418
|
}
|
|
377
419
|
}
|
|
378
420
|
|
|
@@ -385,17 +427,27 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
385
427
|
* @param {Object} serviceInfo.metadata - Additional service metadata
|
|
386
428
|
* @param {string} serviceInfo.health - Health check endpoint
|
|
387
429
|
* @param {Object} serviceInfo.spec - OpenAPI specification
|
|
430
|
+
* @param {(boolean|null)} [serviceInfo.deployable] - Manifest-conformance verdict of the
|
|
431
|
+
* caller's last validation run: `true`, `false`, or `null` when the run never measured
|
|
432
|
+
* it. Emitted only when the caller states one; see the `deployable` block below.
|
|
388
433
|
* @param {number} serviceInfo.timeout - Timeout for registration response (default: 30000ms)
|
|
389
|
-
* @returns {Promise<
|
|
434
|
+
* @returns {Promise<{success: boolean, message: string, serviceName: string, version: string,
|
|
435
|
+
* registrationId: (string|undefined), validated: boolean, certificate: (Object|null),
|
|
436
|
+
* errorCode: (string|null|undefined)}>} The registry verdict. `errorCode` is the
|
|
437
|
+
* registry's machine reason for a refusal (`UNDECLARED_SERVICE`,
|
|
438
|
+
* `INVALID_OPERATIONS_SCHEMA`, `VALIDATOR_UNAVAILABLE`, …) — branch on it rather
|
|
439
|
+
* than on `message`, whose wording is not a contract. It is passed through
|
|
440
|
+
* verbatim: `null` on a confirmed registration, `undefined` when the reply
|
|
441
|
+
* carried no code at all.
|
|
442
|
+
* @see api/docs/biz/30-operations/registration-wire.md §8
|
|
390
443
|
*/
|
|
391
444
|
async register(serviceInfo = {}) {
|
|
392
|
-
// DEBUG: Check validation proof status
|
|
393
|
-
console.log(`[RegistryClient] ${this.serviceName}: >>> REGISTER CALLED <<<`);
|
|
394
|
-
console.log(`[RegistryClient] ${this.serviceName}: this.validationProof =`, this.validationProof);
|
|
395
|
-
|
|
396
445
|
// Validate input
|
|
397
446
|
if (serviceInfo.endpoints && !Array.isArray(serviceInfo.endpoints)) {
|
|
398
|
-
throw new Error(
|
|
447
|
+
throw new Error(
|
|
448
|
+
'[RegistryClient] Invalid serviceInfo - endpoints must be an array. '
|
|
449
|
+
+ 'Fix: pass endpoints as an array of endpoint descriptors, or omit the key entirely.'
|
|
450
|
+
);
|
|
399
451
|
}
|
|
400
452
|
|
|
401
453
|
// FAIL-FAST: a client that was never initialized, or that has been closed, is not
|
|
@@ -403,9 +455,9 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
403
455
|
// channel WITHOUT restarting the response consumer started by init() - so the
|
|
404
456
|
// registry answered into a queue nobody was reading and register() waited out the
|
|
405
457
|
// full timeout. No fallbacks (architecture-principles §3), fail fast (§4).
|
|
406
|
-
if (!this.queueManager.
|
|
458
|
+
if (!this.queueManager.isConnected()) {
|
|
407
459
|
throw new Error(
|
|
408
|
-
`[RegistryClient] ${this.serviceName}: Cannot register - the MQ
|
|
460
|
+
`[RegistryClient] ${this.serviceName}: Cannot register - the MQ connection is not open `
|
|
409
461
|
+ '(client not initialized or already closed). Expected: init() completed before register(). '
|
|
410
462
|
+ 'Fix: call init() on a fresh ServiceRegistryClient; a closed client is not reusable.'
|
|
411
463
|
);
|
|
@@ -436,16 +488,31 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
436
488
|
msg.workspaceScoped = serviceInfo.workspaceScoped;
|
|
437
489
|
}
|
|
438
490
|
|
|
491
|
+
// The manifest-conformance verdict, verbatim from the caller: `true`, `false`,
|
|
492
|
+
// or `null` when validation stopped before step 7 measured it. The registry
|
|
493
|
+
// projects it into `infrastructure:health:<service>` beside `status`
|
|
494
|
+
// (owner confirmation `api/docs/governance/confirmations/biz-service-manifest.md`
|
|
495
|
+
// 001 §4), so `null` is a value this message must be able to carry — hence a
|
|
496
|
+
// presence check and not a truthiness one. A caller that sends no verdict
|
|
497
|
+
// (anything other than ServiceWrapper) puts no key on the wire; nothing is
|
|
498
|
+
// substituted for it.
|
|
499
|
+
if ('deployable' in serviceInfo) {
|
|
500
|
+
msg.deployable = serviceInfo.deployable;
|
|
501
|
+
}
|
|
502
|
+
|
|
439
503
|
// Include validation proof if loaded
|
|
440
504
|
// Structure: { validationProof: "hash", validationData: { ... } }
|
|
441
505
|
// See: @onlineapps/service-validator-core/README.md#validation-proof-structure
|
|
442
506
|
if (this.validationProof) {
|
|
443
507
|
msg.validationProof = this.validationProof.validationProof;
|
|
444
508
|
msg.validationData = this.validationProof.validationData;
|
|
445
|
-
|
|
446
|
-
|
|
509
|
+
// The proof itself is reported on the "Registration message sent" line below;
|
|
510
|
+
// repeating it here would log the same fact twice per registration.
|
|
447
511
|
} else {
|
|
448
|
-
|
|
512
|
+
this.logger.warn('[RegistryClient] Registering without a validation proof - registry will run Tier 2', {
|
|
513
|
+
serviceName: this.serviceName,
|
|
514
|
+
version: this.version
|
|
515
|
+
});
|
|
449
516
|
}
|
|
450
517
|
|
|
451
518
|
// Create promise to wait for registration response.
|
|
@@ -482,44 +549,56 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
482
549
|
// Send registration message to registry
|
|
483
550
|
// CRITICAL: Use queueConfig.js to get correct parameters (TTL, max-length, etc.)
|
|
484
551
|
// This prevents 406 PRECONDITION-FAILED errors from TTL mismatches
|
|
485
|
-
console.log(`[RegistryClient] [PUBLISH] Preparing to publish to ${this.registryQueue}`);
|
|
486
|
-
|
|
487
552
|
try {
|
|
488
553
|
await this._ensureInfrastructureQueue(this.registryQueue);
|
|
489
554
|
} catch (queueError) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
555
|
+
// Not logged before throwing: the rejection carries this exact message.
|
|
556
|
+
throw this._abortPendingRegistration(msgId, new Error(
|
|
557
|
+
`[RegistryClient] ${this.serviceName}: Cannot register - infrastructure queue verification failed: ${queueError.message}`
|
|
558
|
+
));
|
|
493
559
|
}
|
|
494
560
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// Send message synchronously (sendToQueue is synchronous, returns boolean).
|
|
499
|
-
// The channel existed when register() started; it can still have been closed while
|
|
500
|
-
// the infrastructure-queue check awaited above.
|
|
501
|
-
if (!this.queueManager.channel || this.queueManager.channel.closed) {
|
|
561
|
+
// The connection was open when register() started; it can still have been lost
|
|
562
|
+
// while the infrastructure-queue check awaited above.
|
|
563
|
+
if (!this.queueManager.isConnected()) {
|
|
502
564
|
throw this._abortPendingRegistration(msgId, new Error(
|
|
503
|
-
`[RegistryClient] ${this.serviceName}: Cannot publish - the MQ
|
|
504
|
-
+ 'message was sent. Expected: an open
|
|
565
|
+
`[RegistryClient] ${this.serviceName}: Cannot publish - the MQ connection closed before the registration `
|
|
566
|
+
+ 'message was sent. Expected: an open connection for the whole register() call. '
|
|
505
567
|
+ 'Fix: re-create the client (init() + register()) once the MQ connection is back.'
|
|
506
568
|
));
|
|
507
569
|
}
|
|
508
570
|
|
|
509
571
|
try {
|
|
510
|
-
|
|
572
|
+
// The core's publish resolves or THROWS; there is no falsy return to read
|
|
573
|
+
// (@onlineapps/mq-client-core README § API — "publish() … failure is signalled
|
|
574
|
+
// by a thrown PublishError, never by a falsy return"). Serialisation is its
|
|
575
|
+
// job too, so the message travels as the object it is.
|
|
576
|
+
await this.queueManager.publish(
|
|
511
577
|
this.registryQueue,
|
|
512
|
-
|
|
578
|
+
msg,
|
|
513
579
|
{ persistent: true }
|
|
514
580
|
);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
581
|
+
// The one line per registration attempt: what was published, where, under
|
|
582
|
+
// which request id, and whether it carried a proof. Six stdout lines
|
|
583
|
+
// ("REGISTER CALLED", the proof pair, "Preparing to publish", "Sending
|
|
584
|
+
// registration message", "Message type") said this in pieces.
|
|
585
|
+
this.logger.info('[RegistryClient] Registration message sent', {
|
|
586
|
+
serviceName: this.serviceName,
|
|
587
|
+
version: this.version,
|
|
588
|
+
registryQueue: this.registryQueue,
|
|
589
|
+
responseQueue: this.serviceRegistryQueue,
|
|
590
|
+
requestId: msgId,
|
|
591
|
+
hasValidationProof: !!this.validationProof,
|
|
592
|
+
proofHash: this.validationProof
|
|
593
|
+
? this.validationProof.validationProof.substring(0, 32)
|
|
594
|
+
: null,
|
|
595
|
+
timeoutMs: timeout
|
|
596
|
+
});
|
|
519
597
|
} catch (sendError) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
598
|
+
// Not logged before throwing: the rejection carries this exact message.
|
|
599
|
+
throw this._abortPendingRegistration(msgId, new Error(
|
|
600
|
+
`[RegistryClient] ${this.serviceName}: Failed to send registration message: ${sendError.message}`
|
|
601
|
+
));
|
|
523
602
|
}
|
|
524
603
|
|
|
525
604
|
this.emit('registerSent', msg);
|
|
@@ -529,16 +608,10 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
529
608
|
// which is what a legitimate registry rejection looks like (e.g. missing
|
|
530
609
|
// validation proof). The caller must be able to tell "the registry said no"
|
|
531
610
|
// from "the registry never answered" (architecture-principles §3, §4).
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
responseType: typeof response,
|
|
537
|
-
responseKeys: response ? Object.keys(response) : [],
|
|
538
|
-
hasSuccess: !!response?.success,
|
|
539
|
-
hasCertificate: !!response?.certificate
|
|
540
|
-
});
|
|
541
|
-
return response;
|
|
611
|
+
// No log around the await: the same event is already reported once, from
|
|
612
|
+
// _handleRegistryMessage ("Registration resolved"), and a timeout or an abort
|
|
613
|
+
// rejects with a message of its own.
|
|
614
|
+
return responsePromise;
|
|
542
615
|
}
|
|
543
616
|
|
|
544
617
|
/**
|
|
@@ -566,7 +639,21 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
566
639
|
|
|
567
640
|
/**
|
|
568
641
|
* Deregisters the service from the registry.
|
|
569
|
-
*
|
|
642
|
+
*
|
|
643
|
+
* The registry sends NO reply to a `deregister`
|
|
644
|
+
* (api/docs/biz/30-operations/registration-wire.md §2.3 — `processDeregistration`
|
|
645
|
+
* removes the record and publishes `service.deregistered` to monitoring), so
|
|
646
|
+
* there is no verdict to report and none is invented. Until d.278c this method
|
|
647
|
+
* answered a hardcoded `{ success: true, message: 'Service deregistered
|
|
648
|
+
* successfully' }` whatever happened — a claim about the registry made by the
|
|
649
|
+
* one party that never heard from it.
|
|
650
|
+
*
|
|
651
|
+
* What this call knows, and therefore what it returns: the request it published,
|
|
652
|
+
* where it published it, and that the heartbeat timer is stopped. A publish that
|
|
653
|
+
* fails REJECTS; reaching the return means the broker took the message.
|
|
654
|
+
*
|
|
655
|
+
* @returns {Promise<{requestId: string, serviceName: string, version: string,
|
|
656
|
+
* publishedTo: string, heartbeatStopped: boolean, publishedAt: string}>}
|
|
570
657
|
*/
|
|
571
658
|
async deregister() {
|
|
572
659
|
const msg = {
|
|
@@ -577,15 +664,18 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
577
664
|
timestamp: new Date().toISOString()
|
|
578
665
|
};
|
|
579
666
|
|
|
580
|
-
// Send deregistration message to registry
|
|
581
|
-
// CRITICAL: Use queueConfig.js to get correct parameters (TTL, max-length, etc.)
|
|
582
|
-
console.log(`[RegistryClient] [PUBLISH] Preparing to publish to ${this.registryQueue} (deregister)`);
|
|
583
667
|
await this._ensureInfrastructureQueue(this.registryQueue);
|
|
584
|
-
this.queueManager.
|
|
668
|
+
await this.queueManager.publish(
|
|
585
669
|
this.registryQueue,
|
|
586
|
-
|
|
670
|
+
msg,
|
|
587
671
|
{ persistent: true }
|
|
588
672
|
);
|
|
673
|
+
this.logger.info('[RegistryClient] Deregistration request published', {
|
|
674
|
+
serviceName: this.serviceName,
|
|
675
|
+
version: this.version,
|
|
676
|
+
registryQueue: this.registryQueue,
|
|
677
|
+
requestId: msg.id
|
|
678
|
+
});
|
|
589
679
|
|
|
590
680
|
this.emit('deregisterSent', msg);
|
|
591
681
|
|
|
@@ -593,10 +683,12 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
593
683
|
this.stopHeartbeat();
|
|
594
684
|
|
|
595
685
|
return {
|
|
596
|
-
|
|
597
|
-
message: 'Service deregistered successfully',
|
|
686
|
+
requestId: msg.id,
|
|
598
687
|
serviceName: this.serviceName,
|
|
599
|
-
version: this.version
|
|
688
|
+
version: this.version,
|
|
689
|
+
publishedTo: this.registryQueue,
|
|
690
|
+
heartbeatStopped: this.heartbeatTimer === null,
|
|
691
|
+
publishedAt: msg.timestamp
|
|
600
692
|
};
|
|
601
693
|
}
|
|
602
694
|
|
|
@@ -623,14 +715,19 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
623
715
|
specificationEndpoint: this.specificationEndpoint,
|
|
624
716
|
timestamp: new Date().toISOString()
|
|
625
717
|
};
|
|
626
|
-
// CRITICAL: Use queueConfig.js to get correct parameters (TTL, max-length, etc.)
|
|
627
|
-
console.log(`[RegistryClient] [PUBLISH] Preparing to publish to ${this.registryQueue} (heartbeat)`);
|
|
628
718
|
await this._ensureInfrastructureQueue(this.registryQueue);
|
|
629
|
-
this.queueManager.
|
|
719
|
+
await this.queueManager.publish(
|
|
630
720
|
this.registryQueue,
|
|
631
|
-
|
|
721
|
+
msg,
|
|
632
722
|
{ persistent: true }
|
|
633
723
|
);
|
|
724
|
+
// debug, not info: this repeats every heartbeatInterval, so at
|
|
725
|
+
// info it would bury every other line this client writes.
|
|
726
|
+
this.logger.debug('[RegistryClient] Heartbeat sent', {
|
|
727
|
+
serviceName: this.serviceName,
|
|
728
|
+
version: this.version,
|
|
729
|
+
registryQueue: this.registryQueue
|
|
730
|
+
});
|
|
634
731
|
this.emit('heartbeatSent', msg);
|
|
635
732
|
}
|
|
636
733
|
|
|
@@ -640,7 +737,10 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
640
737
|
startHeartbeat() {
|
|
641
738
|
// Send immediately after init
|
|
642
739
|
this.sendHeartbeat().catch(err => {
|
|
643
|
-
|
|
740
|
+
this.logger.error('[RegistryClient] Initial heartbeat failed', {
|
|
741
|
+
serviceName: this.serviceName,
|
|
742
|
+
error: err.message
|
|
743
|
+
});
|
|
644
744
|
});
|
|
645
745
|
|
|
646
746
|
// Repeat at the configured interval using a safe recursive timeout to avoid parallel runs
|
|
@@ -648,7 +748,10 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
648
748
|
try {
|
|
649
749
|
await this.sendHeartbeat();
|
|
650
750
|
} catch (err) {
|
|
651
|
-
|
|
751
|
+
this.logger.error('[RegistryClient] Heartbeat failed', {
|
|
752
|
+
serviceName: this.serviceName,
|
|
753
|
+
error: err.message
|
|
754
|
+
});
|
|
652
755
|
} finally {
|
|
653
756
|
// Schedule next heartbeat only after previous one finished (or failed)
|
|
654
757
|
if (this.heartbeatTimer !== null) {
|
|
@@ -684,7 +787,8 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
684
787
|
queueManager: this.queueManager,
|
|
685
788
|
serviceName: this.serviceName,
|
|
686
789
|
redis: this.redis,
|
|
687
|
-
storageConfig: this.storageConfig
|
|
790
|
+
storageConfig: this.storageConfig,
|
|
791
|
+
logger: this.logger
|
|
688
792
|
});
|
|
689
793
|
|
|
690
794
|
// Forward events from consumer
|
|
@@ -699,18 +803,6 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
699
803
|
}
|
|
700
804
|
}
|
|
701
805
|
|
|
702
|
-
/**
|
|
703
|
-
* Get service spec from event consumer (requires subscribeToChanges)
|
|
704
|
-
* @param {string} serviceName - Name of the service
|
|
705
|
-
* @returns {Promise<Object>} - Service specification
|
|
706
|
-
*/
|
|
707
|
-
async getServiceSpec(serviceName) {
|
|
708
|
-
if (!this.eventConsumer) {
|
|
709
|
-
throw new Error('Event consumer not initialized. Call subscribeToChanges() first.');
|
|
710
|
-
}
|
|
711
|
-
return this.eventConsumer.getServiceSpec(serviceName);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
806
|
/**
|
|
715
807
|
* Get basic service state for service discovery.
|
|
716
808
|
*
|
|
@@ -731,8 +823,16 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
731
823
|
* cookbook-router expects `{ status }`; the registry writes it lowercase
|
|
732
824
|
* ('active'/'inactive') into the summary, so no normalization happens here.
|
|
733
825
|
*
|
|
826
|
+
* @typedef {Object} ServiceSummary
|
|
827
|
+
* @property {string} serviceName
|
|
828
|
+
* @property {string} status - lowercase, as the registry writes it ('active'/'inactive')
|
|
829
|
+
* @property {string} [version]
|
|
830
|
+
* @property {string} [lastHeartbeatAt]
|
|
831
|
+
*/
|
|
832
|
+
|
|
833
|
+
/**
|
|
734
834
|
* @param {string} serviceName
|
|
735
|
-
* @returns {Promise<
|
|
835
|
+
* @returns {Promise<ServiceSummary|null>}
|
|
736
836
|
*/
|
|
737
837
|
async getService(serviceName) {
|
|
738
838
|
if (!serviceName || typeof serviceName !== 'string') {
|
|
@@ -774,29 +874,6 @@ class ServiceRegistryClient extends EventEmitter {
|
|
|
774
874
|
};
|
|
775
875
|
}
|
|
776
876
|
|
|
777
|
-
/**
|
|
778
|
-
* Check if service is active (requires subscribeToChanges)
|
|
779
|
-
* @param {string} serviceName - Name of the service
|
|
780
|
-
* @returns {boolean}
|
|
781
|
-
*/
|
|
782
|
-
isServiceActive(serviceName) {
|
|
783
|
-
if (!this.eventConsumer) {
|
|
784
|
-
throw new Error('Event consumer not initialized. Call subscribeToChanges() first.');
|
|
785
|
-
}
|
|
786
|
-
return this.eventConsumer.isServiceActive(serviceName);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Get list of active services (requires subscribeToChanges)
|
|
791
|
-
* @returns {Array<string>}
|
|
792
|
-
*/
|
|
793
|
-
getActiveServices() {
|
|
794
|
-
if (!this.eventConsumer) {
|
|
795
|
-
return [];
|
|
796
|
-
}
|
|
797
|
-
return this.eventConsumer.getActiveServices();
|
|
798
|
-
}
|
|
799
|
-
|
|
800
877
|
/**
|
|
801
878
|
* Releases resources: stops heartbeat and closes connection.
|
|
802
879
|
* @returns {Promise<void>}
|