@onlineapps/service-wrapper 2.0.40 → 2.0.42
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/package.json +2 -2
- package/src/ServiceWrapper.js +337 -137
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-wrapper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.42",
|
|
4
4
|
"description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@onlineapps/conn-orch-api-mapper": "^1.0.0",
|
|
32
32
|
"@onlineapps/conn-orch-cookbook": "^2.0.0",
|
|
33
33
|
"@onlineapps/conn-orch-orchestrator": "^1.0.1",
|
|
34
|
-
"@onlineapps/conn-orch-registry": "^1.1.
|
|
34
|
+
"@onlineapps/conn-orch-registry": "^1.1.25",
|
|
35
35
|
"@onlineapps/conn-orch-validator": "^2.0.0",
|
|
36
36
|
"@onlineapps/service-common": "^1.0.0",
|
|
37
37
|
"@onlineapps/monitoring-core": "^1.0.0"
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -23,6 +23,16 @@ const CacheConnector = require('@onlineapps/conn-base-cache');
|
|
|
23
23
|
const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
|
|
24
24
|
const { ValidationOrchestrator } = require('@onlineapps/conn-orch-validator');
|
|
25
25
|
|
|
26
|
+
const INFRA_QUEUE_OWNERS = {
|
|
27
|
+
'workflow.init': 'Gateway (api_gateway)',
|
|
28
|
+
'registry.register': 'Registry (api_services_registry)',
|
|
29
|
+
'registry.heartbeats': 'Registry (api_services_registry)',
|
|
30
|
+
'validation.requests': 'Validator (api_services_validator)',
|
|
31
|
+
'workflow.completed': 'Delivery Dispatcher (api_delivery_dispatcher)',
|
|
32
|
+
'workflow.failed': 'Delivery Dispatcher (api_delivery_dispatcher)',
|
|
33
|
+
'infrastructure.health.checks': 'Registry (api_services_registry)'
|
|
34
|
+
};
|
|
35
|
+
|
|
26
36
|
/**
|
|
27
37
|
* ServiceWrapper class
|
|
28
38
|
* Collection of connectors that enhance business service with infrastructure
|
|
@@ -63,6 +73,117 @@ class ServiceWrapper {
|
|
|
63
73
|
this.isInitialized = false;
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Start workflow listeners and create business queues only when BOTH conditions are met:
|
|
78
|
+
* 1. Service is validated and registered (certificate received)
|
|
79
|
+
* 2. Infrastructure is ready (all infrastructure services UP, required queues exist)
|
|
80
|
+
*
|
|
81
|
+
* Business services may create their own queues ONLY when:
|
|
82
|
+
* - Service has received certificate (validated + registered)
|
|
83
|
+
* - AND infrastructure:health:all is true
|
|
84
|
+
* - AND required infrastructure queues exist (workflow.init, registry.register, etc.)
|
|
85
|
+
*
|
|
86
|
+
* @private
|
|
87
|
+
* @param {Object} registrationResult
|
|
88
|
+
* @param {string} serviceName
|
|
89
|
+
*/
|
|
90
|
+
async _startWorkflowListenersIfReady(registrationResult, serviceName) {
|
|
91
|
+
// Condition 1: Service must be validated and registered (certificate required)
|
|
92
|
+
if (!(registrationResult?.success && registrationResult.certificate)) {
|
|
93
|
+
this.logger?.warn('⚠ Registration succeeded but no certificate received - workflow listeners NOT started');
|
|
94
|
+
this.logger?.warn('⚠ Business queues will NOT be created until certificate is received');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.logger?.info('✓ Certificate validated (service is validated and registered), verifying infrastructure readiness...');
|
|
99
|
+
|
|
100
|
+
// Condition 2: Infrastructure must be ready before creating business queues
|
|
101
|
+
// Phase 8: verify infrastructure before creating business queues
|
|
102
|
+
await this._verifyInfrastructureReady(serviceName);
|
|
103
|
+
|
|
104
|
+
this.logger?.info('✓ Infrastructure verified, creating business queues...');
|
|
105
|
+
try {
|
|
106
|
+
const createdQueues = await this.mqClient.setupServiceQueues(serviceName, { includeWorkflow: true });
|
|
107
|
+
this.logger?.info('✓ Business queues initialized via QueueManager', {
|
|
108
|
+
queues: Object.keys(createdQueues).map((k) => createdQueues[k])
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const requiredQueues = ['main', 'dlq', 'workflow'];
|
|
112
|
+
const missingQueues = requiredQueues.filter((q) => !createdQueues[q]);
|
|
113
|
+
if (missingQueues.length > 0) {
|
|
114
|
+
throw new Error(`Failed to create all required business queues. Missing: ${missingQueues.join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
} catch (queueError) {
|
|
117
|
+
this.logger?.error('Failed to initialize business queues', {
|
|
118
|
+
error: queueError.message,
|
|
119
|
+
stack: queueError.stack
|
|
120
|
+
});
|
|
121
|
+
throw new Error(`Failed to initialize business queues for ${serviceName}: ${queueError.message}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.logger?.info('✓ Starting workflow listeners...');
|
|
125
|
+
try {
|
|
126
|
+
await this._startWorkflowInitListener();
|
|
127
|
+
await this._startServiceWorkflowListener();
|
|
128
|
+
this.logger?.info('✓ All workflow listeners started successfully');
|
|
129
|
+
} catch (listenerError) {
|
|
130
|
+
this.logger?.error('Failed to start workflow listeners', {
|
|
131
|
+
error: listenerError.message,
|
|
132
|
+
stack: listenerError.stack
|
|
133
|
+
});
|
|
134
|
+
throw new Error(`Failed to start workflow listeners for ${serviceName}: ${listenerError.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Wait until infrastructure reports healthy before allowing the next step
|
|
140
|
+
* @param {string} context - human readable description (e.g. 'registry registration')
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
async _waitForInfrastructureGate(context) {
|
|
144
|
+
const { waitForInfrastructureReady } = require('@onlineapps/service-common');
|
|
145
|
+
const redisUrl = this.config.wrapper?.cache?.url || process.env.REDIS_URL || 'redis://api_node_cache:6379';
|
|
146
|
+
const maxWait = parseInt(process.env.INFRASTRUCTURE_HEALTH_WAIT_MAX_TIME) || 300000;
|
|
147
|
+
const checkInterval = parseInt(process.env.INFRASTRUCTURE_HEALTH_WAIT_CHECK_INTERVAL) || 5000;
|
|
148
|
+
|
|
149
|
+
const logWithPrefix = (level, message) => {
|
|
150
|
+
const prefixed = `[InfrastructureGate:${context}] ${message}`;
|
|
151
|
+
if (this.logger?.log) {
|
|
152
|
+
this.logger.log({ level, message: prefixed });
|
|
153
|
+
} else if (this.logger?.[level]) {
|
|
154
|
+
this.logger[level](prefixed);
|
|
155
|
+
} else {
|
|
156
|
+
console[level === 'error' ? 'error' : 'log'](prefixed);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const gateLogger = {
|
|
161
|
+
info: (message) => logWithPrefix('info', message),
|
|
162
|
+
log: (payload) => {
|
|
163
|
+
if (payload && typeof payload === 'object') {
|
|
164
|
+
logWithPrefix(payload.level || 'info', payload.message || '');
|
|
165
|
+
} else {
|
|
166
|
+
logWithPrefix('info', payload);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await waitForInfrastructureReady({
|
|
173
|
+
redisUrl,
|
|
174
|
+
maxWait,
|
|
175
|
+
checkInterval,
|
|
176
|
+
logger: gateLogger
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const message = `[InfrastructureGate] Cannot proceed with ${context}. ${error.message}`;
|
|
180
|
+
this.logger?.error(message, { context, error: error.message });
|
|
181
|
+
const gateError = new Error(message);
|
|
182
|
+
gateError.cause = error;
|
|
183
|
+
throw gateError;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
66
187
|
/**
|
|
67
188
|
* Validate constructor options
|
|
68
189
|
* @private
|
|
@@ -136,6 +257,53 @@ class ServiceWrapper {
|
|
|
136
257
|
* Initialize all wrapper components
|
|
137
258
|
* @async
|
|
138
259
|
*/
|
|
260
|
+
/**
|
|
261
|
+
* Log phase with transparent format
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
_logPhase(phase, phaseName, status, error = null, duration = null) {
|
|
265
|
+
const logMsg = `[FÁZE ${phase}] ${phaseName} - ${status}`;
|
|
266
|
+
const logData = {
|
|
267
|
+
phase,
|
|
268
|
+
phaseName,
|
|
269
|
+
status,
|
|
270
|
+
timestamp: new Date().toISOString(),
|
|
271
|
+
duration,
|
|
272
|
+
error: error ? { message: error.message, stack: error.stack } : null,
|
|
273
|
+
serviceName: this.config.service?.name || 'unnamed-service'
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (status === 'PASSED') {
|
|
277
|
+
this.logger?.info(logMsg, logData);
|
|
278
|
+
console.log(logMsg);
|
|
279
|
+
} else if (status === 'FAILED') {
|
|
280
|
+
this.logger?.error(logMsg, logData);
|
|
281
|
+
console.error(logMsg, error ? `: ${error.message}` : '');
|
|
282
|
+
} else {
|
|
283
|
+
this.logger?.info(logMsg, logData);
|
|
284
|
+
console.log(logMsg);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handle initialization error with restart strategy
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
_handleInitializationError(phase, phaseName, error, isTransient = false) {
|
|
293
|
+
this._logPhase(phase, phaseName, 'FAILED', error);
|
|
294
|
+
|
|
295
|
+
if (isTransient) {
|
|
296
|
+
// Přechodná chyba → restart může pomoci
|
|
297
|
+
console.error(`[FÁZE ${phase}] Transient error - service will restart (Docker/process manager)`);
|
|
298
|
+
process.exit(1); // Docker/process manager restartuje
|
|
299
|
+
} else {
|
|
300
|
+
// Trvalá chyba → restart nepomůže
|
|
301
|
+
console.error(`[FÁZE ${phase}] Permanent error - fix required, no restart`);
|
|
302
|
+
// TODO: Send alert (email, Slack, etc.)
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
139
307
|
async initialize() {
|
|
140
308
|
if (this.isInitialized) {
|
|
141
309
|
// Logger might not be initialized yet, use console as fallback
|
|
@@ -143,48 +311,96 @@ class ServiceWrapper {
|
|
|
143
311
|
return;
|
|
144
312
|
}
|
|
145
313
|
|
|
314
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
315
|
+
const startTime = Date.now();
|
|
316
|
+
|
|
146
317
|
try {
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
|
|
318
|
+
// FÁZE 0.1: Načtení konfigurace
|
|
319
|
+
// Konfigurace se načítá v konstruktoru, takže tady jen logujeme
|
|
320
|
+
this._logPhase('0.1', 'Configuration Load', 'PASSED', null, Date.now() - startTime);
|
|
150
321
|
|
|
151
|
-
//
|
|
152
|
-
if (this.config.wrapper?.
|
|
153
|
-
|
|
322
|
+
// FÁZE 0.2: Tier 1 validace (PŘED MQ připojením)
|
|
323
|
+
if (this.serviceRoot && this.config.wrapper?.validation?.enabled !== false) {
|
|
324
|
+
const validationStartTime = Date.now();
|
|
325
|
+
try {
|
|
326
|
+
await this._ensureValidationProof();
|
|
327
|
+
this._logPhase('0.2', 'Tier 1 Validation', 'PASSED', null, Date.now() - validationStartTime);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
this._handleInitializationError('0.2', 'Tier 1 Validation', error, false); // Trvalá chyba
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
this._logPhase('0.2', 'Tier 1 Validation', 'SKIPPED', null, 0);
|
|
154
333
|
}
|
|
155
334
|
|
|
156
|
-
//
|
|
335
|
+
// FÁZE 0.3: RabbitMQ připojení (pouze pokud Tier 1 prošla)
|
|
157
336
|
if (this.config.wrapper?.mq?.enabled !== false) {
|
|
158
|
-
|
|
159
|
-
|
|
337
|
+
const mqStartTime = Date.now();
|
|
338
|
+
try {
|
|
339
|
+
await this._initializeMQ();
|
|
340
|
+
this._logPhase('0.3', 'RabbitMQ Connection', 'PASSED', null, Date.now() - mqStartTime);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this._handleInitializationError('0.3', 'RabbitMQ Connection', error, true); // Přechodná chyba
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
this._logPhase('0.3', 'RabbitMQ Connection', 'SKIPPED', null, 0);
|
|
160
346
|
}
|
|
161
347
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
348
|
+
// FÁZE 0.4: Kontrola infrastructure:health:all (pouze pokud MQ připojeno)
|
|
349
|
+
if (this.mqClient) {
|
|
350
|
+
const healthCheckStartTime = Date.now();
|
|
351
|
+
try {
|
|
352
|
+
await this._waitForInfrastructureGate('infrastructure health check');
|
|
353
|
+
this._logPhase('0.4', 'Infrastructure Health Check', 'PASSED', null, Date.now() - healthCheckStartTime);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
this._handleInitializationError('0.4', 'Infrastructure Health Check', error, true); // Přechodná chyba
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
this._logPhase('0.4', 'Infrastructure Health Check', 'SKIPPED', null, 0);
|
|
168
359
|
}
|
|
169
360
|
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
361
|
+
// FÁZE 0.5 a 0.6: Vytvoření front a spuštění konzumerů
|
|
362
|
+
// Tyto fáze se provádějí v registryClient.init() během Fáze 0.7
|
|
363
|
+
// Logujeme je tam
|
|
364
|
+
|
|
365
|
+
// FÁZE 0.7: Registrace u Registry (pouze pokud vše výše prošlo)
|
|
173
366
|
if (this.config.wrapper?.registry?.enabled !== false) {
|
|
174
|
-
|
|
367
|
+
const registryStartTime = Date.now();
|
|
368
|
+
try {
|
|
369
|
+
await this._initializeRegistry();
|
|
370
|
+
this._logPhase('0.7', 'Registry Registration', 'PASSED', null, Date.now() - registryStartTime);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
this._handleInitializationError('0.7', 'Registry Registration', error, true); // Přechodná chyba
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
this._logPhase('0.7', 'Registry Registration', 'SKIPPED', null, 0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Initialize monitoring (needed for logging, but not critical for startup)
|
|
379
|
+
if (this.config.wrapper?.monitoring?.enabled !== false) {
|
|
380
|
+
try {
|
|
381
|
+
await this._initializeMonitoring();
|
|
382
|
+
} catch (error) {
|
|
383
|
+
// Monitoring není kritický, jen logujeme
|
|
384
|
+
console.warn('Monitoring initialization failed (non-critical):', error.message);
|
|
385
|
+
}
|
|
175
386
|
}
|
|
176
387
|
|
|
177
|
-
//
|
|
388
|
+
// Initialize cache if configured
|
|
178
389
|
if (this.config.wrapper?.cache?.enabled === true) {
|
|
179
|
-
|
|
390
|
+
try {
|
|
391
|
+
await this._initializeCache();
|
|
392
|
+
} catch (error) {
|
|
393
|
+
// Cache není kritický, jen logujeme
|
|
394
|
+
console.warn('Cache initialization failed (non-critical):', error.message);
|
|
395
|
+
}
|
|
180
396
|
}
|
|
181
397
|
|
|
182
|
-
//
|
|
398
|
+
// Setup health checks
|
|
183
399
|
if (this.config.wrapper?.health?.enabled !== false) {
|
|
184
400
|
this._setupHealthChecks();
|
|
185
401
|
}
|
|
186
402
|
|
|
187
|
-
//
|
|
403
|
+
// Initialize orchestrator for workflow processing
|
|
188
404
|
// NOTE: Orchestrator is prepared but workflow listeners will be started
|
|
189
405
|
// ONLY after receiving certificate from Registry (see _initializeRegistry)
|
|
190
406
|
if (this.mqClient) {
|
|
@@ -192,16 +408,13 @@ class ServiceWrapper {
|
|
|
192
408
|
}
|
|
193
409
|
|
|
194
410
|
this.isInitialized = true;
|
|
411
|
+
this._logPhase('INIT', 'ServiceWrapper Initialization', 'PASSED', null, Date.now() - startTime);
|
|
195
412
|
this.logger?.info(`ServiceWrapper initialized successfully for ${serviceName}`);
|
|
196
413
|
|
|
197
414
|
} catch (error) {
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
} else {
|
|
202
|
-
console.error('Failed to initialize ServiceWrapper:', error);
|
|
203
|
-
}
|
|
204
|
-
throw error;
|
|
415
|
+
// Unexpected error - log and exit
|
|
416
|
+
console.error('[UNEXPECTED ERROR]', error);
|
|
417
|
+
process.exit(1);
|
|
205
418
|
}
|
|
206
419
|
}
|
|
207
420
|
|
|
@@ -267,8 +480,18 @@ class ServiceWrapper {
|
|
|
267
480
|
logger: this.logger || console
|
|
268
481
|
});
|
|
269
482
|
|
|
270
|
-
|
|
271
|
-
|
|
483
|
+
try {
|
|
484
|
+
await this.mqClient.connect();
|
|
485
|
+
this.logger?.info('MQ connector initialized');
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const message = `[MQConnector] Unable to connect to RabbitMQ at ${mqUrl}. ` +
|
|
488
|
+
'Verify that api_services_queuer is running and accessible. ' +
|
|
489
|
+
`Original error: ${error.message || error}`;
|
|
490
|
+
this.logger?.error(message, { error: error.message, code: error.code });
|
|
491
|
+
const wrappedError = new Error(message);
|
|
492
|
+
wrappedError.cause = error;
|
|
493
|
+
throw wrappedError;
|
|
494
|
+
}
|
|
272
495
|
|
|
273
496
|
// NOTE: Business services (ServiceWrapper) do NOT create infrastructure queues
|
|
274
497
|
// Infrastructure queues are created by infrastructure services (Monitoring Consumer, Gateway)
|
|
@@ -289,6 +512,10 @@ class ServiceWrapper {
|
|
|
289
512
|
const servicePort = this.config.service?.port || process.env.PORT || 3000;
|
|
290
513
|
const mqUrl = this.config.wrapper?.mq?.url || '';
|
|
291
514
|
|
|
515
|
+
// Infrastructure health check was already done in Fáze 0.4
|
|
516
|
+
// FÁZE 0.5 a 0.6: Vytvoření front a spuštění konzumerů se provádí v registryClient.init()
|
|
517
|
+
// FÁZE 0.7: Registrace u Registry
|
|
518
|
+
|
|
292
519
|
this.registryClient = new RegistryConnector.ServiceRegistryClient({
|
|
293
520
|
amqpUrl: mqUrl,
|
|
294
521
|
serviceName: serviceName,
|
|
@@ -318,66 +545,57 @@ class ServiceWrapper {
|
|
|
318
545
|
// RegistryClient loads proof during init() and includes it in registration message
|
|
319
546
|
|
|
320
547
|
try {
|
|
548
|
+
const logMsg = `[ServiceWrapper] [REGISTRATION] About to call registryClient.register() for ${serviceName}`;
|
|
549
|
+
this.logger?.info(logMsg);
|
|
550
|
+
console.log(logMsg); // Fallback if logger not initialized
|
|
551
|
+
|
|
321
552
|
const registrationResult = await this.registryClient.register(serviceInfo);
|
|
322
|
-
|
|
553
|
+
|
|
554
|
+
const returnLog = `[ServiceWrapper] [REGISTRATION] ✓ registryClient.register() returned`;
|
|
555
|
+
const returnData = {
|
|
556
|
+
hasResult: !!registrationResult,
|
|
557
|
+
resultType: typeof registrationResult,
|
|
558
|
+
resultKeys: registrationResult ? Object.keys(registrationResult) : [],
|
|
559
|
+
registrationResult
|
|
560
|
+
};
|
|
561
|
+
this.logger?.info(returnLog, returnData);
|
|
562
|
+
console.log(returnLog, JSON.stringify(returnData, null, 2)); // Fallback if logger not initialized
|
|
563
|
+
const serviceRegisteredLog = `Service registered: ${serviceName}`;
|
|
564
|
+
const serviceRegisteredData = {
|
|
565
|
+
registrationResult,
|
|
566
|
+
hasSuccess: !!registrationResult?.success,
|
|
567
|
+
hasCertificate: !!registrationResult?.certificate,
|
|
568
|
+
certificateId: registrationResult?.certificate?.id || registrationResult?.certificate?.certificateId || 'none'
|
|
569
|
+
};
|
|
570
|
+
this.logger?.info(serviceRegisteredLog, serviceRegisteredData);
|
|
571
|
+
console.log(serviceRegisteredLog, JSON.stringify(serviceRegisteredData, null, 2)); // Fallback if logger not initialized
|
|
323
572
|
|
|
324
573
|
// Store certificate if received
|
|
325
574
|
if (registrationResult.certificate) {
|
|
326
575
|
this.certificate = registrationResult.certificate;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// CRITICAL: Start workflow listeners ONLY after successful validation and registration with certificate
|
|
331
|
-
// Both workflow.init and service-specific listeners start here
|
|
332
|
-
if (registrationResult.success && registrationResult.certificate) {
|
|
333
|
-
this.logger?.info('✓ Certificate validated, verifying infrastructure readiness...');
|
|
334
|
-
|
|
335
|
-
// PHASE 8: Verify infrastructure services are ready before creating business queues
|
|
336
|
-
await this._verifyInfrastructureReady(serviceName);
|
|
337
|
-
|
|
338
|
-
this.logger?.info('✓ Infrastructure verified, creating business queues...');
|
|
339
|
-
try {
|
|
340
|
-
// CRITICAL: Create ALL business queues atomically (main, dlq, workflow)
|
|
341
|
-
// This MUST succeed completely - if one queue fails, all fail
|
|
342
|
-
const createdQueues = await this.mqClient.setupServiceQueues(serviceName, { includeWorkflow: true });
|
|
343
|
-
this.logger?.info('✓ Business queues initialized via QueueManager', {
|
|
344
|
-
queues: Object.keys(createdQueues).map(k => createdQueues[k])
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Verify all queues were created
|
|
348
|
-
const requiredQueues = ['main', 'dlq', 'workflow'];
|
|
349
|
-
const missingQueues = requiredQueues.filter(q => !createdQueues[q]);
|
|
350
|
-
if (missingQueues.length > 0) {
|
|
351
|
-
throw new Error(`Failed to create all required business queues. Missing: ${missingQueues.join(', ')}`);
|
|
352
|
-
}
|
|
353
|
-
} catch (queueError) {
|
|
354
|
-
this.logger?.error('Failed to initialize business queues', {
|
|
355
|
-
error: queueError.message,
|
|
356
|
-
stack: queueError.stack
|
|
357
|
-
});
|
|
358
|
-
throw new Error(`Failed to initialize business queues for ${serviceName}: ${queueError.message}`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// CRITICAL: Start workflow listeners ONLY after all queues are created
|
|
362
|
-
// Queues must exist before we try to consume from them
|
|
363
|
-
this.logger?.info('✓ Starting workflow listeners...');
|
|
364
|
-
try {
|
|
365
|
-
// Start workflow.init listener (for all services)
|
|
366
|
-
await this._startWorkflowInitListener();
|
|
367
|
-
// Start service-specific workflow listener
|
|
368
|
-
await this._startServiceWorkflowListener();
|
|
369
|
-
this.logger?.info('✓ All workflow listeners started successfully');
|
|
370
|
-
} catch (listenerError) {
|
|
371
|
-
this.logger?.error('Failed to start workflow listeners', {
|
|
372
|
-
error: listenerError.message,
|
|
373
|
-
stack: listenerError.stack
|
|
374
|
-
});
|
|
375
|
-
throw new Error(`Failed to start workflow listeners for ${serviceName}: ${listenerError.message}`);
|
|
376
|
-
}
|
|
576
|
+
const certLog = `✓ Certificate received: ${registrationResult.certificate.certificateId || registrationResult.certificate.id || 'unknown'}`;
|
|
577
|
+
this.logger?.info(certLog);
|
|
578
|
+
console.log(certLog); // Fallback if logger not initialized
|
|
377
579
|
} else {
|
|
378
|
-
|
|
580
|
+
const noCertLog = `⚠ No certificate in registration result`;
|
|
581
|
+
const noCertData = {
|
|
582
|
+
registrationResultKeys: Object.keys(registrationResult || {}),
|
|
583
|
+
registrationResult
|
|
584
|
+
};
|
|
585
|
+
this.logger?.warn(noCertLog, noCertData);
|
|
586
|
+
console.warn(noCertLog, JSON.stringify(noCertData, null, 2)); // Fallback if logger not initialized
|
|
379
587
|
}
|
|
380
588
|
|
|
589
|
+
const workflowLog = `[ServiceWrapper] Calling _startWorkflowListenersIfReady with registrationResult`;
|
|
590
|
+
const workflowData = {
|
|
591
|
+
hasSuccess: !!registrationResult?.success,
|
|
592
|
+
hasCertificate: !!registrationResult?.certificate
|
|
593
|
+
};
|
|
594
|
+
this.logger?.info(workflowLog, workflowData);
|
|
595
|
+
console.log(workflowLog, JSON.stringify(workflowData, null, 2)); // Fallback if logger not initialized
|
|
596
|
+
|
|
597
|
+
await this._startWorkflowListenersIfReady(registrationResult, serviceName);
|
|
598
|
+
|
|
381
599
|
} catch (error) {
|
|
382
600
|
this.logger?.error(`Service registration failed: ${error.message}`, { error: error.message, stack: error.stack });
|
|
383
601
|
throw new Error(`Failed to register service ${serviceName}: ${error.message}`);
|
|
@@ -403,6 +621,11 @@ class ServiceWrapper {
|
|
|
403
621
|
* Verify infrastructure services are ready before creating business queues
|
|
404
622
|
* Phase 8: Business Services Infrastructure Verification
|
|
405
623
|
*
|
|
624
|
+
* IMPORTANT: This method is called ONLY after service has received certificate (validated + registered).
|
|
625
|
+
* Business queues are created when BOTH conditions are met:
|
|
626
|
+
* 1. Service has certificate (validated + registered) - checked in _startWorkflowListenersIfReady()
|
|
627
|
+
* 2. Infrastructure is ready - checked in this method
|
|
628
|
+
*
|
|
406
629
|
* Checks:
|
|
407
630
|
* 1. All infrastructure services are UP (via Redis key infrastructure:health:all)
|
|
408
631
|
* 2. Required infrastructure queues exist (workflow.init, registry.register, etc.)
|
|
@@ -437,41 +660,7 @@ class ServiceWrapper {
|
|
|
437
660
|
|
|
438
661
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
439
662
|
try {
|
|
440
|
-
|
|
441
|
-
// ServiceWrapper uses service-common (shared utility) to maintain architectural separation
|
|
442
|
-
this.logger?.info(`[InfrastructureVerify] Attempt ${attempt}/${maxRetries}: Checking infrastructure health...`);
|
|
443
|
-
|
|
444
|
-
const redisUrl = this.config.wrapper?.cache?.url || process.env.REDIS_URL || 'redis://api_node_cache:6379';
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
const { waitForInfrastructureReady } = require('@onlineapps/service-common');
|
|
448
|
-
const waitMaxTime = 10000; // 10 seconds per attempt
|
|
449
|
-
const waitCheckInterval = 2000; // Check every 2 seconds
|
|
450
|
-
|
|
451
|
-
await waitForInfrastructureReady({
|
|
452
|
-
redisUrl: redisUrl,
|
|
453
|
-
maxWait: waitMaxTime,
|
|
454
|
-
checkInterval: waitCheckInterval,
|
|
455
|
-
logger: {
|
|
456
|
-
log: (msg) => this.logger?.info(`[InfrastructureVerify] ${msg}`),
|
|
457
|
-
info: (msg) => this.logger?.info(`[InfrastructureVerify] ${msg}`)
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
this.logger?.info('[InfrastructureVerify] ✓ All infrastructure services are UP');
|
|
461
|
-
} catch (healthError) {
|
|
462
|
-
// Infrastructure not ready yet - will retry with exponential backoff
|
|
463
|
-
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
464
|
-
this.logger?.warn(`[InfrastructureVerify] Infrastructure not ready (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms...`, {
|
|
465
|
-
error: healthError.message
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
if (attempt < maxRetries) {
|
|
469
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
470
|
-
continue; // Retry
|
|
471
|
-
} else {
|
|
472
|
-
throw new Error(`Infrastructure services not ready after ${maxRetries} attempts: ${healthError.message}`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
663
|
+
await this._waitForInfrastructureGate('business queue setup');
|
|
475
664
|
|
|
476
665
|
// Step 2: Verify required infrastructure queues exist
|
|
477
666
|
this.logger?.info('[InfrastructureVerify] Verifying required infrastructure queues exist...');
|
|
@@ -487,14 +676,23 @@ class ServiceWrapper {
|
|
|
487
676
|
}
|
|
488
677
|
|
|
489
678
|
const missingQueues = [];
|
|
679
|
+
const QUEUE_CHECK_TIMEOUT = 5000; // 5 seconds per queue check
|
|
490
680
|
for (const queueName of requiredInfrastructureQueues) {
|
|
491
681
|
try {
|
|
492
|
-
|
|
682
|
+
// Add timeout to prevent hanging on checkQueue
|
|
683
|
+
const checkPromise = channel.checkQueue(queueName);
|
|
684
|
+
const checkTimeoutPromise = new Promise((_, reject) => {
|
|
685
|
+
setTimeout(() => {
|
|
686
|
+
reject(new Error(`checkQueue timeout after ${QUEUE_CHECK_TIMEOUT}ms`));
|
|
687
|
+
}, QUEUE_CHECK_TIMEOUT);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
await Promise.race([checkPromise, checkTimeoutPromise]);
|
|
493
691
|
this.logger?.info(`[InfrastructureVerify] ✓ Queue exists: ${queueName}`);
|
|
494
692
|
} catch (checkErr) {
|
|
495
|
-
if (checkErr.code === 404) {
|
|
693
|
+
if (checkErr.code === 404 || checkErr.message.includes('timeout')) {
|
|
496
694
|
missingQueues.push(queueName);
|
|
497
|
-
this.logger?.warn(`[InfrastructureVerify] ✗ Queue missing: ${queueName}`);
|
|
695
|
+
this.logger?.warn(`[InfrastructureVerify] ✗ Queue missing or timeout: ${queueName} (${checkErr.message})`);
|
|
498
696
|
} else {
|
|
499
697
|
// Other error (e.g., channel closed) - treat as missing
|
|
500
698
|
missingQueues.push(queueName);
|
|
@@ -504,25 +702,29 @@ class ServiceWrapper {
|
|
|
504
702
|
}
|
|
505
703
|
|
|
506
704
|
if (missingQueues.length > 0) {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
705
|
+
const queueDetails = missingQueues.map((queueName) => {
|
|
706
|
+
const owner = INFRA_QUEUE_OWNERS[queueName] || 'responsible infrastructure service';
|
|
707
|
+
return `${queueName} (owner: ${owner})`;
|
|
708
|
+
});
|
|
709
|
+
throw new Error(
|
|
710
|
+
`[InfrastructureVerify] Required RabbitMQ queue(s) missing: ${queueDetails.join(', ')}. ` +
|
|
711
|
+
'Infrastructure service(s) responsible for these queues are not ready yet. ' +
|
|
712
|
+
'Resolve the infrastructure issue and restart the business service.'
|
|
713
|
+
);
|
|
516
714
|
}
|
|
517
715
|
|
|
518
716
|
// Step 3: Log optional queues status (for debugging)
|
|
519
717
|
for (const queueName of optionalInfrastructureQueues) {
|
|
520
718
|
try {
|
|
521
|
-
|
|
719
|
+
const checkPromise = channel.checkQueue(queueName);
|
|
720
|
+
const checkTimeoutPromise = new Promise((_, reject) => {
|
|
721
|
+
setTimeout(() => reject(new Error('timeout')), QUEUE_CHECK_TIMEOUT);
|
|
722
|
+
});
|
|
723
|
+
await Promise.race([checkPromise, checkTimeoutPromise]);
|
|
522
724
|
this.logger?.info(`[InfrastructureVerify] ✓ Optional queue exists: ${queueName}`);
|
|
523
725
|
} catch (checkErr) {
|
|
524
|
-
if (checkErr.code === 404) {
|
|
525
|
-
this.logger?.debug(`[InfrastructureVerify] Optional queue missing (not critical): ${queueName}`);
|
|
726
|
+
if (checkErr.code === 404 || checkErr.message.includes('timeout')) {
|
|
727
|
+
this.logger?.debug(`[InfrastructureVerify] Optional queue missing or timeout (not critical): ${queueName}`);
|
|
526
728
|
}
|
|
527
729
|
}
|
|
528
730
|
}
|
|
@@ -996,9 +1198,7 @@ class ServiceWrapper {
|
|
|
996
1198
|
if (!this.config.service?.version) {
|
|
997
1199
|
throw new Error('Service version is required for validation');
|
|
998
1200
|
}
|
|
999
|
-
|
|
1000
|
-
throw new Error('Monitoring must be initialized before validation');
|
|
1001
|
-
}
|
|
1201
|
+
// Logger není povinný - validace může běžet i bez něj (použije console)
|
|
1002
1202
|
|
|
1003
1203
|
const { name: serviceName, version: serviceVersion, port: servicePort } = this.config.service;
|
|
1004
1204
|
|