@onlineapps/service-wrapper 2.0.37 → 2.0.39

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 -1
  2. package/src/ServiceWrapper.js +226 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.0.37",
3
+ "version": "2.0.39",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -33,6 +33,7 @@
33
33
  "@onlineapps/conn-orch-orchestrator": "^1.0.1",
34
34
  "@onlineapps/conn-orch-registry": "^1.1.16",
35
35
  "@onlineapps/conn-orch-validator": "^2.0.0",
36
+ "@onlineapps/service-common": "^1.0.0",
36
37
  "@onlineapps/monitoring-core": "^1.0.0"
37
38
  },
38
39
  "devDependencies": {
@@ -330,19 +330,50 @@ class ServiceWrapper {
330
330
  // CRITICAL: Start workflow listeners ONLY after successful validation and registration with certificate
331
331
  // Both workflow.init and service-specific listeners start here
332
332
  if (registrationResult.success && registrationResult.certificate) {
333
- this.logger?.info('✓ Certificate validated, starting workflow listeners...');
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...');
334
339
  try {
335
- await this.mqClient.setupServiceQueues(serviceName, { includeWorkflow: true });
336
- this.logger?.info('✓ Business queues initialized via QueueManager');
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
+ }
337
353
  } catch (queueError) {
338
- this.logger?.error('Failed to initialize business queues', { error: queueError.message });
354
+ this.logger?.error('Failed to initialize business queues', {
355
+ error: queueError.message,
356
+ stack: queueError.stack
357
+ });
339
358
  throw new Error(`Failed to initialize business queues for ${serviceName}: ${queueError.message}`);
340
359
  }
341
360
 
342
- // Start workflow.init listener (for all services)
343
- await this._startWorkflowInitListener();
344
- // Start service-specific workflow listener
345
- await this._startServiceWorkflowListener();
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
+ }
346
377
  } else {
347
378
  this.logger?.warn('⚠ Registration succeeded but no certificate received - workflow listeners NOT started');
348
379
  }
@@ -368,6 +399,158 @@ class ServiceWrapper {
368
399
  }, heartbeatInterval);
369
400
  }
370
401
 
402
+ /**
403
+ * Verify infrastructure services are ready before creating business queues
404
+ * Phase 8: Business Services Infrastructure Verification
405
+ *
406
+ * Checks:
407
+ * 1. All infrastructure services are UP (via Redis key infrastructure:health:all)
408
+ * 2. Required infrastructure queues exist (workflow.init, registry.register, etc.)
409
+ *
410
+ * Uses exponential backoff retry mechanism if infrastructure not ready.
411
+ * @private
412
+ */
413
+ async _verifyInfrastructureReady(serviceName) {
414
+ const maxRetries = parseInt(process.env.INFRASTRUCTURE_VERIFY_MAX_RETRIES) || 12; // 12 * 5s = 60s max
415
+ const baseDelay = parseInt(process.env.INFRASTRUCTURE_VERIFY_BASE_DELAY) || 5000; // 5 seconds
416
+ const maxDelay = parseInt(process.env.INFRASTRUCTURE_VERIFY_MAX_DELAY) || 30000; // 30 seconds max
417
+
418
+ // Required infrastructure queues that must exist before business queues can be created
419
+ const requiredInfrastructureQueues = [
420
+ 'workflow.init', // Gateway responsibility
421
+ 'registry.register', // Registry responsibility
422
+ 'registry.heartbeats' // Registry responsibility
423
+ ];
424
+
425
+ // Optional infrastructure queues (may not exist if services are not running)
426
+ const optionalInfrastructureQueues = [
427
+ 'validation.requests', // Validator responsibility
428
+ 'workflow.completed', // Delivery Dispatcher responsibility
429
+ 'workflow.failed' // Delivery Dispatcher responsibility
430
+ ];
431
+
432
+ this.logger?.info('[InfrastructureVerify] Starting infrastructure readiness verification...', {
433
+ maxRetries,
434
+ baseDelay,
435
+ requiredQueues: requiredInfrastructureQueues
436
+ });
437
+
438
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
439
+ 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
+ }
475
+
476
+ // Step 2: Verify required infrastructure queues exist
477
+ this.logger?.info('[InfrastructureVerify] Verifying required infrastructure queues exist...');
478
+
479
+ const transport = this.mqClient._transport;
480
+ if (!transport || !transport._queueChannel) {
481
+ throw new Error('MQ transport not initialized - cannot verify infrastructure queues');
482
+ }
483
+
484
+ const channel = transport._queueChannel;
485
+ if (channel.closed) {
486
+ throw new Error('MQ channel is closed - cannot verify infrastructure queues');
487
+ }
488
+
489
+ const missingQueues = [];
490
+ for (const queueName of requiredInfrastructureQueues) {
491
+ try {
492
+ await channel.checkQueue(queueName);
493
+ this.logger?.info(`[InfrastructureVerify] ✓ Queue exists: ${queueName}`);
494
+ } catch (checkErr) {
495
+ if (checkErr.code === 404) {
496
+ missingQueues.push(queueName);
497
+ this.logger?.warn(`[InfrastructureVerify] ✗ Queue missing: ${queueName}`);
498
+ } else {
499
+ // Other error (e.g., channel closed) - treat as missing
500
+ missingQueues.push(queueName);
501
+ this.logger?.warn(`[InfrastructureVerify] ✗ Cannot check queue ${queueName}: ${checkErr.message}`);
502
+ }
503
+ }
504
+ }
505
+
506
+ 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
+ }
516
+ }
517
+
518
+ // Step 3: Log optional queues status (for debugging)
519
+ for (const queueName of optionalInfrastructureQueues) {
520
+ try {
521
+ await channel.checkQueue(queueName);
522
+ this.logger?.info(`[InfrastructureVerify] ✓ Optional queue exists: ${queueName}`);
523
+ } catch (checkErr) {
524
+ if (checkErr.code === 404) {
525
+ this.logger?.debug(`[InfrastructureVerify] Optional queue missing (not critical): ${queueName}`);
526
+ }
527
+ }
528
+ }
529
+
530
+ // All checks passed!
531
+ this.logger?.info('[InfrastructureVerify] ✓ Infrastructure verification complete - all required queues exist');
532
+ return;
533
+
534
+ } catch (error) {
535
+ if (attempt === maxRetries) {
536
+ // Final attempt failed - throw error
537
+ this.logger?.error('[InfrastructureVerify] ✗ Infrastructure verification failed after all retries', {
538
+ error: error.message,
539
+ stack: error.stack
540
+ });
541
+ throw new Error(`Infrastructure verification failed for ${serviceName}: ${error.message}`);
542
+ }
543
+
544
+ // Not final attempt - continue retry loop
545
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
546
+ this.logger?.warn(`[InfrastructureVerify] Verification failed (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms...`, {
547
+ error: error.message
548
+ });
549
+ await new Promise(resolve => setTimeout(resolve, delay));
550
+ }
551
+ }
552
+ }
553
+
371
554
  /**
372
555
  * Initialize cache connector
373
556
  * @private
@@ -488,12 +671,25 @@ class ServiceWrapper {
488
671
  return;
489
672
  }
490
673
 
491
- // Subscribe to workflow.init queue (for all services)
492
- await this.mqClient.consume('workflow.init', async (message) => {
493
- await this._processWorkflowMessage(message, 'workflow.init');
494
- });
674
+ const queueName = 'workflow.init';
675
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Starting workflow.init listener for queue: ${queueName}`);
676
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Service: ${this.config.service?.name || 'unnamed-service'}`);
677
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Timestamp: ${new Date().toISOString()}`);
495
678
 
496
- this.logger?.info('✓ workflow.init listener started (after successful registration)');
679
+ try {
680
+ // Subscribe to workflow.init queue (for all services)
681
+ await this.mqClient.consume(queueName, async (message) => {
682
+ await this._processWorkflowMessage(message, queueName);
683
+ });
684
+
685
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] ✓ workflow.init listener started successfully for queue: ${queueName}`);
686
+ } catch (consumeErr) {
687
+ this.logger?.error(`[ServiceWrapper] [CONSUMER] ✗ Failed to start workflow.init listener for queue: ${queueName}`, {
688
+ error: consumeErr.message,
689
+ stack: consumeErr.stack
690
+ });
691
+ throw consumeErr;
692
+ }
497
693
  }
498
694
 
499
695
  /**
@@ -509,12 +705,24 @@ class ServiceWrapper {
509
705
  const serviceName = this.config.service?.name || 'unnamed-service';
510
706
  const serviceQueue = `${serviceName}.workflow`;
511
707
 
512
- // Subscribe to service-specific queue
513
- await this.mqClient.consume(serviceQueue, async (message) => {
514
- await this._processWorkflowMessage(message, serviceQueue);
515
- });
708
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Starting service-specific workflow listener for queue: ${serviceQueue}`);
709
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Service: ${serviceName}`);
710
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Timestamp: ${new Date().toISOString()}`);
516
711
 
517
- this.logger?.info(`✓ Service-specific workflow listener started: ${serviceQueue}`);
712
+ try {
713
+ // Subscribe to service-specific queue
714
+ await this.mqClient.consume(serviceQueue, async (message) => {
715
+ await this._processWorkflowMessage(message, serviceQueue);
716
+ });
717
+
718
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] ✓ Service-specific workflow listener started successfully for queue: ${serviceQueue}`);
719
+ } catch (consumeErr) {
720
+ this.logger?.error(`[ServiceWrapper] [CONSUMER] ✗ Failed to start service-specific workflow listener for queue: ${serviceQueue}`, {
721
+ error: consumeErr.message,
722
+ stack: consumeErr.stack
723
+ });
724
+ throw consumeErr;
725
+ }
518
726
  }
519
727
 
520
728
  /**