From 992315a26034ff33bf596e01199bac9595191b98 Mon Sep 17 00:00:00 2001 From: manjik-rumsan Date: Thu, 30 Apr 2026 16:01:26 +0545 Subject: [PATCH] fix ari connection - app registraion --- apps/asterisk-worker/src/main.ts | 16 +++++++ .../src/workers/ivr.service.ts | 44 +++++++++++++------ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/apps/asterisk-worker/src/main.ts b/apps/asterisk-worker/src/main.ts index a124382..66e79f0 100644 --- a/apps/asterisk-worker/src/main.ts +++ b/apps/asterisk-worker/src/main.ts @@ -8,6 +8,22 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app/app.module'; +// swagger-client (used by ari-client) synchronously `throw`s a plain string +// from inside its HTTP callback when ARI is unreachable, escaping the Promise +// chain. Swallow it so IVRService's reconnect loop can keep running. +process.on('uncaughtException', (err) => { + if (typeof err === 'string' || (err as { code?: string })?.code === 'HostIsNotReachable') { + Logger.warn(`Swallowed ARI/swagger error, will reconnect: ${String(err)}`, 'Bootstrap'); + return; + } + Logger.error('Uncaught exception', err as Error, 'Bootstrap'); + process.exit(1); +}); + +process.on('unhandledRejection', (reason) => { + Logger.warn(`Unhandled rejection: ${String(reason)}`, 'Bootstrap'); +}); + async function bootstrap() { const app = await NestFactory.create(AppModule); const globalPrefix = 'api'; diff --git a/apps/asterisk-worker/src/workers/ivr.service.ts b/apps/asterisk-worker/src/workers/ivr.service.ts index 29a20ab..733dee1 100644 --- a/apps/asterisk-worker/src/workers/ivr.service.ts +++ b/apps/asterisk-worker/src/workers/ivr.service.ts @@ -123,29 +123,45 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { this.client = await ari.connect(server, user, password); await this.client.start(appName); + // Confirm Asterisk actually registered our Stasis app on this WebSocket. + // If the WS opened but Asterisk hasn't bound the app, fail fast so the + // reconnect loop retries instead of leaving originate calls broken. + try { + await this.client.applications.get({ applicationName: appName }); + } catch (err) { + throw new Error( + `Stasis app '${appName}' did not register with Asterisk: ${(err as Error).message}`, + ); + } + this.isConnected = true; - // ari-client has built-in WebSocket retry (up to 10 attempts). - // Track connectivity so we can guard outbound calls during transient drops. - this.client.on('WebSocketReconnecting', () => { + // First sign of disconnect — don't wait for built-in retry to "succeed" + // with a stale Stasis registration on a restarted Asterisk. Tear down + // and do a full reconnect (new client + new start(appName)) which + // re-registers the app via a fresh WebSocket session. + this.client.once('WebSocketReconnecting', (err: Error) => { + if (this.isShuttingDown) return; this.isConnected = false; - this.logger.warn('ARI WebSocket reconnecting...'); - }); - - this.client.on('WebSocketConnected', () => { - this.isConnected = true; - this.logger.log('ARI WebSocket reconnected'); + this.logger.warn( + `ARI WebSocket dropped (${err?.message ?? 'unknown'}) — forcing full reconnect`, + ); + this.scheduleReconnect(1); }); - // When ari-client exhausts all built-in retries, start our own reconnect loop. + // Fallback: if built-in retry exhausts before our scheduled reconnect runs. this.client.once('WebSocketMaxRetries', () => { + if (this.isShuttingDown) return; this.isConnected = false; - this.logger.error( - 'ARI WebSocket max retries exceeded — scheduling full reconnect', - ); + this.logger.error('ARI WebSocket max retries exceeded'); this.scheduleReconnect(1); }); + this.client.on('WebSocketConnected', () => { + this.isConnected = true; + this.logger.log('ARI WebSocket connected'); + }); + // Share the ARI client with dependent services this.channelStateManager.setClient(this.client); this.playbackService.setClient(this.client); @@ -241,6 +257,7 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { private scheduleReconnect(attempt: number) { if (this.isShuttingDown) return; + if (this.reconnectTimer) return; const delay = Math.min(5000 * attempt, 60_000); this.logger.warn( `ARI disconnected — reconnecting in ${delay / 1000}s (attempt ${attempt})`, @@ -252,6 +269,7 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { } private async attemptReconnect(attempt: number) { + this.reconnectTimer = null; if (this.isShuttingDown) return; this.logger.log(`Attempting ARI full reconnect (attempt ${attempt})`); try {