Skip to content
Merged

Dev #94

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/asterisk-worker/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
44 changes: 31 additions & 13 deletions apps/asterisk-worker/src/workers/ivr.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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})`,
Expand All @@ -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 {
Expand Down
Loading