@onlineapps/service-wrapper 2.0.39 → 2.0.41

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.
Files changed (2) hide show
  1. package/package.json +2 -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.39",
3
+ "version": "2.0.41",
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.16",
34
+ "@onlineapps/conn-orch-registry": "^1.1.23",
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"
@@ -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
- const serviceName = this.config.service?.name || 'unnamed-service';
148
- // Logger might not be initialized yet, use console as fallback
149
- (this.logger || console).info(`Initializing ServiceWrapper for ${serviceName}`);
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
- // 1. Initialize monitoring first (needed for logging)
152
- if (this.config.wrapper?.monitoring?.enabled !== false) {
153
- await this._initializeMonitoring();
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
- // 2. Initialize MQ connection
335
+ // FÁZE 0.3: RabbitMQ připojení (pouze pokud Tier 1 prošla)
157
336
  if (this.config.wrapper?.mq?.enabled !== false) {
158
- await this._initializeMQ();
159
- // NOTE: workflow.init listener starts ONLY after successful registration (see _initializeRegistry)
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
- // 3. Run pre-validation (Tier 1)
163
- // Validates service structure, config, operations, cookbook tests
164
- // Skips if valid proof exists in conn-runtime/validation-proof.json
165
- // NOTE: Validation is required for service-specific workflow listener and registration
166
- if (this.serviceRoot && this.config.wrapper?.validation?.enabled !== false) {
167
- await this._ensureValidationProof();
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
- // 4. Initialize service registry
171
- // NOTE: Service-specific workflow listener ({serviceName}.workflow) will be started
172
- // ONLY after successful validation and registration (see _initializeRegistry)
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
- await this._initializeRegistry();
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
- // 5. Initialize cache if configured
388
+ // Initialize cache if configured
178
389
  if (this.config.wrapper?.cache?.enabled === true) {
179
- await this._initializeCache();
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
- // 6. Setup health checks
398
+ // Setup health checks
183
399
  if (this.config.wrapper?.health?.enabled !== false) {
184
400
  this._setupHealthChecks();
185
401
  }
186
402
 
187
- // 7. Initialize orchestrator for workflow processing
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
- // Always log errors - use logger if available, otherwise console
199
- if (this.logger) {
200
- this.logger.error('Failed to initialize ServiceWrapper', { error: error.message, stack: error.stack });
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
- await this.mqClient.connect();
271
- this.logger?.info('MQ connector initialized');
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
- this.logger?.info(`Service registered: ${serviceName}`, { registrationResult });
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
- this.logger?.info(`✓ Certificate received: ${registrationResult.certificate.certificateId}`);
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
- this.logger?.warn('⚠ Registration succeeded but no certificate received - workflow listeners NOT started');
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
- // Step 1: Check infrastructure health via Redis (using service-common)
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
- await channel.checkQueue(queueName);
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 delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
508
- this.logger?.warn(`[InfrastructureVerify] Missing required infrastructure queues (attempt ${attempt}/${maxRetries}): ${missingQueues.join(', ')}. Retrying in ${delay}ms...`);
509
-
510
- if (attempt < maxRetries) {
511
- await new Promise(resolve => setTimeout(resolve, delay));
512
- continue; // Retry
513
- } else {
514
- throw new Error(`Required infrastructure queues missing after ${maxRetries} attempts: ${missingQueues.join(', ')}`);
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
- await channel.checkQueue(queueName);
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
- if (!this.logger) {
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