@onlineapps/service-wrapper 2.0.36 → 2.0.38

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 +3 -2
  2. package/src/ServiceWrapper.js +223 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.0.36",
3
+ "version": "2.0.38",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -27,12 +27,13 @@
27
27
  "@onlineapps/conn-base-cache": "^1.0.0",
28
28
  "@onlineapps/conn-base-monitoring": "^1.0.1",
29
29
  "@onlineapps/conn-infra-error-handler": "^1.0.0",
30
- "@onlineapps/conn-infra-mq": "^1.1.10",
30
+ "@onlineapps/conn-infra-mq": "^1.1.11",
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
34
  "@onlineapps/conn-orch-registry": "^1.1.16",
35
35
  "@onlineapps/conn-orch-validator": "^2.0.0",
36
+ "@onlineapps/infrastructure-tools": "^1.0.3",
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,155 @@ 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
+ const redisUrl = this.config.wrapper?.cache?.url || process.env.REDIS_URL || 'redis://api_node_cache:6379';
418
+
419
+ // Required infrastructure queues that must exist before business queues can be created
420
+ const requiredInfrastructureQueues = [
421
+ 'workflow.init', // Gateway responsibility
422
+ 'registry.register', // Registry responsibility
423
+ 'registry.heartbeats' // Registry responsibility
424
+ ];
425
+
426
+ // Optional infrastructure queues (may not exist if services are not running)
427
+ const optionalInfrastructureQueues = [
428
+ 'validation.requests', // Validator responsibility
429
+ 'workflow.completed', // Delivery Dispatcher responsibility
430
+ 'workflow.failed' // Delivery Dispatcher responsibility
431
+ ];
432
+
433
+ this.logger?.info('[InfrastructureVerify] Starting infrastructure readiness verification...', {
434
+ maxRetries,
435
+ baseDelay,
436
+ requiredQueues: requiredInfrastructureQueues
437
+ });
438
+
439
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
440
+ try {
441
+ // Step 1: Check infrastructure health via Redis
442
+ this.logger?.info(`[InfrastructureVerify] Attempt ${attempt}/${maxRetries}: Checking infrastructure health...`);
443
+
444
+ const { waitForInfrastructureReady } = require('@onlineapps/infrastructure-tools');
445
+ const waitMaxTime = 10000; // 10 seconds per attempt
446
+ const waitCheckInterval = 2000; // Check every 2 seconds
447
+
448
+ try {
449
+ await waitForInfrastructureReady({
450
+ redisUrl: redisUrl,
451
+ maxWait: waitMaxTime,
452
+ checkInterval: waitCheckInterval,
453
+ logger: {
454
+ log: (msg) => this.logger?.info(`[InfrastructureVerify] ${msg}`)
455
+ }
456
+ });
457
+ this.logger?.info('[InfrastructureVerify] ✓ All infrastructure services are UP');
458
+ } catch (healthError) {
459
+ // Infrastructure not ready yet - will retry with exponential backoff
460
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
461
+ this.logger?.warn(`[InfrastructureVerify] Infrastructure not ready (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms...`, {
462
+ error: healthError.message
463
+ });
464
+
465
+ if (attempt < maxRetries) {
466
+ await new Promise(resolve => setTimeout(resolve, delay));
467
+ continue; // Retry
468
+ } else {
469
+ throw new Error(`Infrastructure services not ready after ${maxRetries} attempts: ${healthError.message}`);
470
+ }
471
+ }
472
+
473
+ // Step 2: Verify required infrastructure queues exist
474
+ this.logger?.info('[InfrastructureVerify] Verifying required infrastructure queues exist...');
475
+
476
+ const transport = this.mqClient._transport;
477
+ if (!transport || !transport._queueChannel) {
478
+ throw new Error('MQ transport not initialized - cannot verify infrastructure queues');
479
+ }
480
+
481
+ const channel = transport._queueChannel;
482
+ if (channel.closed) {
483
+ throw new Error('MQ channel is closed - cannot verify infrastructure queues');
484
+ }
485
+
486
+ const missingQueues = [];
487
+ for (const queueName of requiredInfrastructureQueues) {
488
+ try {
489
+ await channel.checkQueue(queueName);
490
+ this.logger?.info(`[InfrastructureVerify] ✓ Queue exists: ${queueName}`);
491
+ } catch (checkErr) {
492
+ if (checkErr.code === 404) {
493
+ missingQueues.push(queueName);
494
+ this.logger?.warn(`[InfrastructureVerify] ✗ Queue missing: ${queueName}`);
495
+ } else {
496
+ // Other error (e.g., channel closed) - treat as missing
497
+ missingQueues.push(queueName);
498
+ this.logger?.warn(`[InfrastructureVerify] ✗ Cannot check queue ${queueName}: ${checkErr.message}`);
499
+ }
500
+ }
501
+ }
502
+
503
+ if (missingQueues.length > 0) {
504
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
505
+ this.logger?.warn(`[InfrastructureVerify] Missing required infrastructure queues (attempt ${attempt}/${maxRetries}): ${missingQueues.join(', ')}. Retrying in ${delay}ms...`);
506
+
507
+ if (attempt < maxRetries) {
508
+ await new Promise(resolve => setTimeout(resolve, delay));
509
+ continue; // Retry
510
+ } else {
511
+ throw new Error(`Required infrastructure queues missing after ${maxRetries} attempts: ${missingQueues.join(', ')}`);
512
+ }
513
+ }
514
+
515
+ // Step 3: Log optional queues status (for debugging)
516
+ for (const queueName of optionalInfrastructureQueues) {
517
+ try {
518
+ await channel.checkQueue(queueName);
519
+ this.logger?.info(`[InfrastructureVerify] ✓ Optional queue exists: ${queueName}`);
520
+ } catch (checkErr) {
521
+ if (checkErr.code === 404) {
522
+ this.logger?.debug(`[InfrastructureVerify] Optional queue missing (not critical): ${queueName}`);
523
+ }
524
+ }
525
+ }
526
+
527
+ // All checks passed!
528
+ this.logger?.info('[InfrastructureVerify] ✓ Infrastructure verification complete - all required queues exist');
529
+ return;
530
+
531
+ } catch (error) {
532
+ if (attempt === maxRetries) {
533
+ // Final attempt failed - throw error
534
+ this.logger?.error('[InfrastructureVerify] ✗ Infrastructure verification failed after all retries', {
535
+ error: error.message,
536
+ stack: error.stack
537
+ });
538
+ throw new Error(`Infrastructure verification failed for ${serviceName}: ${error.message}`);
539
+ }
540
+
541
+ // Not final attempt - continue retry loop
542
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
543
+ this.logger?.warn(`[InfrastructureVerify] Verification failed (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms...`, {
544
+ error: error.message
545
+ });
546
+ await new Promise(resolve => setTimeout(resolve, delay));
547
+ }
548
+ }
549
+ }
550
+
371
551
  /**
372
552
  * Initialize cache connector
373
553
  * @private
@@ -488,12 +668,25 @@ class ServiceWrapper {
488
668
  return;
489
669
  }
490
670
 
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
- });
671
+ const queueName = 'workflow.init';
672
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Starting workflow.init listener for queue: ${queueName}`);
673
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Service: ${this.config.service?.name || 'unnamed-service'}`);
674
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Timestamp: ${new Date().toISOString()}`);
495
675
 
496
- this.logger?.info('✓ workflow.init listener started (after successful registration)');
676
+ try {
677
+ // Subscribe to workflow.init queue (for all services)
678
+ await this.mqClient.consume(queueName, async (message) => {
679
+ await this._processWorkflowMessage(message, queueName);
680
+ });
681
+
682
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] ✓ workflow.init listener started successfully for queue: ${queueName}`);
683
+ } catch (consumeErr) {
684
+ this.logger?.error(`[ServiceWrapper] [CONSUMER] ✗ Failed to start workflow.init listener for queue: ${queueName}`, {
685
+ error: consumeErr.message,
686
+ stack: consumeErr.stack
687
+ });
688
+ throw consumeErr;
689
+ }
497
690
  }
498
691
 
499
692
  /**
@@ -509,12 +702,24 @@ class ServiceWrapper {
509
702
  const serviceName = this.config.service?.name || 'unnamed-service';
510
703
  const serviceQueue = `${serviceName}.workflow`;
511
704
 
512
- // Subscribe to service-specific queue
513
- await this.mqClient.consume(serviceQueue, async (message) => {
514
- await this._processWorkflowMessage(message, serviceQueue);
515
- });
705
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Starting service-specific workflow listener for queue: ${serviceQueue}`);
706
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Service: ${serviceName}`);
707
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] Timestamp: ${new Date().toISOString()}`);
516
708
 
517
- this.logger?.info(`✓ Service-specific workflow listener started: ${serviceQueue}`);
709
+ try {
710
+ // Subscribe to service-specific queue
711
+ await this.mqClient.consume(serviceQueue, async (message) => {
712
+ await this._processWorkflowMessage(message, serviceQueue);
713
+ });
714
+
715
+ this.logger?.info(`[ServiceWrapper] [CONSUMER] ✓ Service-specific workflow listener started successfully for queue: ${serviceQueue}`);
716
+ } catch (consumeErr) {
717
+ this.logger?.error(`[ServiceWrapper] [CONSUMER] ✗ Failed to start service-specific workflow listener for queue: ${serviceQueue}`, {
718
+ error: consumeErr.message,
719
+ stack: consumeErr.stack
720
+ });
721
+ throw consumeErr;
722
+ }
518
723
  }
519
724
 
520
725
  /**