@onlineapps/conn-orch-registry 3.0.0 → 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 -24
- package/src/events.js +2 -2
- package/src/index.js +0 -1
- package/src/queueManager.js +168 -123
- package/src/registryClient.js +365 -286
- package/src/registryEventConsumer.js +107 -52
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Implements opt-in subscription to registry.changes exchange.
|
|
6
6
|
* Maintains local service index and enables lazy loading of specs.
|
|
7
7
|
*
|
|
8
|
-
* @module @onlineapps/
|
|
8
|
+
* @module @onlineapps/conn-orch-registry/src/registryEventConsumer
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const EventEmitter = require('events');
|
|
12
|
+
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
12
13
|
|
|
13
14
|
// The storage connector arrives by INJECTION. There used to be an implicit
|
|
14
15
|
// default here — `require('@onlineapps/connector-storage')` inside a try/catch
|
|
@@ -27,12 +28,19 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
27
28
|
* @param {Object} [opts.redis] - Optional Redis client for index persistence
|
|
28
29
|
* @param {Object} [opts.storageConfig] - Configuration for StorageConnector
|
|
29
30
|
* @param {Object} [opts.StorageConnector] - Injectable StorageConnector class for testing
|
|
31
|
+
* @param {Object} opts.logger - Logger with info/warn/error/debug; required, validated here
|
|
32
|
+
* (docs/governance/confirmations/connector-logger-contract.md 001)
|
|
30
33
|
* @param {boolean} [opts.cacheEnabled=true] - Enable local spec caching
|
|
31
34
|
* @param {number} [opts.maxCacheSize=50] - Maximum cached specs
|
|
32
35
|
*/
|
|
33
|
-
constructor({ queueManager, serviceName, redis = null, storageConfig = {}, StorageConnector = null, cacheEnabled = true, maxCacheSize = 50 }) {
|
|
36
|
+
constructor({ queueManager, serviceName, redis = null, storageConfig = {}, StorageConnector = null, logger, cacheEnabled = true, maxCacheSize = 50 }) {
|
|
34
37
|
super();
|
|
35
38
|
|
|
39
|
+
this.logger = assertLogger(
|
|
40
|
+
'RegistryEventConsumer',
|
|
41
|
+
logger,
|
|
42
|
+
'registry index changes are readable in the service log rather than on stdout'
|
|
43
|
+
);
|
|
36
44
|
this.queueManager = queueManager;
|
|
37
45
|
this.serviceName = serviceName;
|
|
38
46
|
this.redis = redis;
|
|
@@ -92,7 +100,10 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
92
100
|
await this.storage.initialize();
|
|
93
101
|
this.emit('storageReady');
|
|
94
102
|
} else {
|
|
95
|
-
|
|
103
|
+
this.logger.warn('[RegistryEventConsumer] Index-only mode - spec downloading disabled', {
|
|
104
|
+
serviceName: this.serviceName,
|
|
105
|
+
reason: 'no storage connector configured'
|
|
106
|
+
});
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
|
|
@@ -102,49 +113,44 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
102
113
|
*/
|
|
103
114
|
async subscribeToChanges() {
|
|
104
115
|
try {
|
|
105
|
-
const channel = this.queueManager.channel;
|
|
106
|
-
|
|
107
116
|
// Assert exchange exists (fanout type for broadcasting)
|
|
108
|
-
await
|
|
117
|
+
await this.queueManager.assertExchange(this.exchangeName, 'fanout', {
|
|
109
118
|
durable: true
|
|
110
119
|
});
|
|
111
120
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} catch (error) {
|
|
123
|
-
if (error.code === 406) {
|
|
124
|
-
// Queue exists with different arguments, use it as-is
|
|
125
|
-
await channel.assertQueue(this.eventsQueueName, {
|
|
126
|
-
durable: true
|
|
127
|
-
});
|
|
128
|
-
} else {
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
121
|
+
// The events queue is a BUSINESS queue of this service, and its declaration —
|
|
122
|
+
// durable, 1 min TTL, 1000 messages, dead-letter route to `<service>.dlq` —
|
|
123
|
+
// belongs to `queueConfig` in the core (`business['registry.events']`), not to
|
|
124
|
+
// an options object written here. The 406 branch this replaces was a fallback
|
|
125
|
+
// in the literal sense architecture-principles §3 forbids: it answered "the
|
|
126
|
+
// queue exists with different arguments" by using it anyway, which is how a
|
|
127
|
+
// queue keeps whatever arguments whoever declared it first happened to pass.
|
|
128
|
+
// It is asserted here, ahead of the binding, because a binding needs the queue
|
|
129
|
+
// to exist; `consume()` below asserts the same declaration from the same owner.
|
|
130
|
+
await this.queueManager.ensureQueues([this.eventsQueueName]);
|
|
132
131
|
|
|
133
132
|
// Bind queue to exchange (receive all registry events)
|
|
134
|
-
await
|
|
133
|
+
await this.queueManager.bindQueue(this.eventsQueueName, this.exchangeName, '');
|
|
135
134
|
|
|
136
135
|
// Load initial snapshot from Redis if available
|
|
137
136
|
await this.loadIndexFromCache();
|
|
138
137
|
|
|
139
|
-
// Start consuming events
|
|
140
|
-
|
|
138
|
+
// Start consuming events on the core's delivery rail: one attempt, and a
|
|
139
|
+
// failure is rejected into `<service>.dlq` instead of being dropped
|
|
140
|
+
// (d.198b-3; @onlineapps/mq-client-core README § consume() — the delivery
|
|
141
|
+
// contract).
|
|
142
|
+
await this.queueManager.consume(
|
|
141
143
|
this.eventsQueueName,
|
|
142
144
|
msg => this._handleRegistryEvent(msg),
|
|
143
|
-
{
|
|
145
|
+
{ requeueOnError: false }
|
|
144
146
|
);
|
|
145
147
|
|
|
146
148
|
this.emit('subscribed', { queue: this.eventsQueueName, exchange: this.exchangeName });
|
|
147
|
-
|
|
149
|
+
this.logger.info('[RegistryEventConsumer] Subscribed to registry changes', {
|
|
150
|
+
serviceName: this.serviceName,
|
|
151
|
+
queue: this.eventsQueueName,
|
|
152
|
+
exchange: this.exchangeName
|
|
153
|
+
});
|
|
148
154
|
|
|
149
155
|
// Request initial snapshot
|
|
150
156
|
await this.requestSnapshot();
|
|
@@ -181,24 +187,32 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
181
187
|
break;
|
|
182
188
|
|
|
183
189
|
default:
|
|
184
|
-
|
|
190
|
+
this.logger.warn('[RegistryEventConsumer] Unknown registry event type', {
|
|
191
|
+
serviceName: this.serviceName,
|
|
192
|
+
eventType: event.type
|
|
193
|
+
});
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
// Persist index after update
|
|
188
197
|
await this.persistIndex();
|
|
189
198
|
|
|
190
|
-
//
|
|
191
|
-
this.
|
|
199
|
+
// No ack here: returning IS the ack (d.198b-3). The core settles the delivery
|
|
200
|
+
// on the channel that delivered it, once this handler resolves.
|
|
192
201
|
|
|
193
202
|
// Emit event for service to react
|
|
194
203
|
this.emit('registryEvent', event);
|
|
195
204
|
|
|
196
205
|
} catch (error) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
206
|
+
// Not emitted as an 'error' event: an EventEmitter with no 'error' listener
|
|
207
|
+
// rethrows what is emitted, so a delivery the core knows how to reject would
|
|
208
|
+
// become an uncatchable crash. The rejection is the one report.
|
|
209
|
+
//
|
|
210
|
+
// Rethrown, not nacked by hand: the consumer was registered with a
|
|
211
|
+
// one-attempt budget, so the core rejects this delivery into
|
|
212
|
+
// `<service>.dlq` and publishes a `message_dlq` event for it. The
|
|
213
|
+
// `nack(msg, false, false)` this replaces asked the broker to DROP it, and
|
|
214
|
+
// the queue declared nowhere to put it anyway.
|
|
215
|
+
throw error;
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
218
|
|
|
@@ -219,7 +233,12 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
219
233
|
});
|
|
220
234
|
|
|
221
235
|
this.emit('serviceUpdated', { service, fingerprint, version });
|
|
222
|
-
|
|
236
|
+
this.logger.info('[RegistryEventConsumer] Service spec published', {
|
|
237
|
+
serviceName: this.serviceName,
|
|
238
|
+
service,
|
|
239
|
+
fingerprint,
|
|
240
|
+
version
|
|
241
|
+
});
|
|
223
242
|
}
|
|
224
243
|
|
|
225
244
|
/**
|
|
@@ -234,7 +253,11 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
234
253
|
entry.updatedAt = event.timestamp;
|
|
235
254
|
|
|
236
255
|
this.emit('statusChanged', { service, status });
|
|
237
|
-
|
|
256
|
+
this.logger.info('[RegistryEventConsumer] Service status changed', {
|
|
257
|
+
serviceName: this.serviceName,
|
|
258
|
+
service,
|
|
259
|
+
status
|
|
260
|
+
});
|
|
238
261
|
}
|
|
239
262
|
}
|
|
240
263
|
|
|
@@ -259,7 +282,10 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
this.emit('snapshotReceived', { count: services.length });
|
|
262
|
-
|
|
285
|
+
this.logger.info('[RegistryEventConsumer] Registry snapshot applied', {
|
|
286
|
+
serviceName: this.serviceName,
|
|
287
|
+
count: services.length
|
|
288
|
+
});
|
|
263
289
|
}
|
|
264
290
|
|
|
265
291
|
/**
|
|
@@ -281,7 +307,10 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
281
307
|
fingerprintsToRemove.forEach(fp => this.specCache.delete(fp));
|
|
282
308
|
|
|
283
309
|
this.emit('serviceRemoved', { service });
|
|
284
|
-
|
|
310
|
+
this.logger.info('[RegistryEventConsumer] Service removed from index', {
|
|
311
|
+
serviceName: this.serviceName,
|
|
312
|
+
service
|
|
313
|
+
});
|
|
285
314
|
}
|
|
286
315
|
}
|
|
287
316
|
|
|
@@ -296,10 +325,13 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
296
325
|
timestamp: new Date().toISOString()
|
|
297
326
|
};
|
|
298
327
|
|
|
299
|
-
|
|
328
|
+
// Published to the fanout exchange, never to a queue: the first argument names
|
|
329
|
+
// the target for the log line, and `routingKey: ''` is a VALUE a fanout ignores
|
|
330
|
+
// — the same shape the core's own monitoring publisher uses.
|
|
331
|
+
await this.queueManager.publish(
|
|
300
332
|
this.exchangeName,
|
|
301
|
-
|
|
302
|
-
|
|
333
|
+
request,
|
|
334
|
+
{ exchange: this.exchangeName, routingKey: '' }
|
|
303
335
|
);
|
|
304
336
|
}
|
|
305
337
|
|
|
@@ -312,11 +344,19 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
312
344
|
// Get entry from index
|
|
313
345
|
const entry = this.serviceIndex.get(serviceName);
|
|
314
346
|
if (!entry) {
|
|
315
|
-
throw new Error(
|
|
347
|
+
throw new Error(
|
|
348
|
+
`[RegistryEventConsumer] Service ${serviceName} not found in index - `
|
|
349
|
+
+ 'the registry has published no entry under that name. '
|
|
350
|
+
+ 'Fix: wait for the service to register, or check the name against getActiveServices().'
|
|
351
|
+
);
|
|
316
352
|
}
|
|
317
353
|
|
|
318
354
|
if (entry.status !== 'ACTIVE') {
|
|
319
|
-
throw new Error(
|
|
355
|
+
throw new Error(
|
|
356
|
+
`[RegistryEventConsumer] Service ${serviceName} is not active (status: ${entry.status}) - `
|
|
357
|
+
+ 'only an ACTIVE entry exposes a specification. '
|
|
358
|
+
+ 'Fix: wait for the service to finish registering, or read the index entry with getServiceInfo().'
|
|
359
|
+
);
|
|
320
360
|
}
|
|
321
361
|
|
|
322
362
|
const { fingerprint, bucket, path } = entry;
|
|
@@ -340,7 +380,13 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
340
380
|
}
|
|
341
381
|
|
|
342
382
|
// Download from MinIO with verification
|
|
343
|
-
|
|
383
|
+
this.logger.debug('[RegistryEventConsumer] Downloading spec', {
|
|
384
|
+
consumer: this.serviceName,
|
|
385
|
+
service: serviceName,
|
|
386
|
+
bucket,
|
|
387
|
+
path,
|
|
388
|
+
fingerprint
|
|
389
|
+
});
|
|
344
390
|
|
|
345
391
|
const content = await this.storage.downloadWithVerification(
|
|
346
392
|
bucket,
|
|
@@ -402,7 +448,10 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
402
448
|
3600 // Expire after 1 hour
|
|
403
449
|
);
|
|
404
450
|
} catch (error) {
|
|
405
|
-
|
|
451
|
+
this.logger.error('[RegistryEventConsumer] Failed to persist index', {
|
|
452
|
+
serviceName: this.serviceName,
|
|
453
|
+
error: error.message
|
|
454
|
+
});
|
|
406
455
|
}
|
|
407
456
|
}
|
|
408
457
|
|
|
@@ -419,10 +468,16 @@ class RegistryEventConsumer extends EventEmitter {
|
|
|
419
468
|
this.serviceIndex = new Map(entries);
|
|
420
469
|
|
|
421
470
|
this.emit('indexLoaded', { count: this.serviceIndex.size });
|
|
422
|
-
|
|
471
|
+
this.logger.info('[RegistryEventConsumer] Service index loaded from cache', {
|
|
472
|
+
serviceName: this.serviceName,
|
|
473
|
+
count: this.serviceIndex.size
|
|
474
|
+
});
|
|
423
475
|
}
|
|
424
476
|
} catch (error) {
|
|
425
|
-
|
|
477
|
+
this.logger.error('[RegistryEventConsumer] Failed to load index from cache', {
|
|
478
|
+
serviceName: this.serviceName,
|
|
479
|
+
error: error.message
|
|
480
|
+
});
|
|
426
481
|
}
|
|
427
482
|
}
|
|
428
483
|
|