Keepalive

Maintain persistent gRPC connections to NextBlock using JavaScript/TypeScript.

// Connection health tracking
interface ConnectionHealth {
  isHealthy: boolean;
  lastSuccessfulPing: Date;
  consecutiveFailures: number;
  totalPingsSent: number;
  totalPingsSuccessful: number;
  averagePingTime: number;
}

// Keepalive configuration
interface KeepaliveConfig {
  pingInterval: number;        // milliseconds
  maxConsecutiveFailures: number;
  reconnectDelay: number;      // milliseconds
  healthCheckEnabled: boolean;
  timeout: number;             // milliseconds
}

// Default keepalive configuration
const defaultKeepaliveConfig: KeepaliveConfig = {
  pingInterval: 60000,         // 60 seconds
  maxConsecutiveFailures: 3,
  reconnectDelay: 5000,        // 5 seconds
  healthCheckEnabled: true,
  timeout: 15000,              // 15 seconds
};

// Basic keepalive implementation
async function startKeepaliveTask(
  // nextblockClient: any, // Your generated gRPC client
  config: KeepaliveConfig = defaultKeepaliveConfig
): Promise<void> {
  console.log(`Starting keepalive task with ${config.pingInterval}ms interval`);

  const keepaliveInterval = setInterval(async () => {
    try {
      const startTime = Date.now();

      /* Uncomment when you have the generated gRPC client
      try {
        await Promise.race([
          nextblockClient.ping({}),
          new Promise((_, reject) => 
            setTimeout(() => reject(new Error('Timeout')), config.timeout)
          )
        ]);

        const pingTime = Date.now() - startTime;
        console.log(`Keepalive ping successful (${pingTime}ms) at ${new Date().toTimeString()}`);

      } catch (error) {
        console.error('Keepalive ping failed:', error);
        // Optionally implement reconnection logic
        clearInterval(keepaliveInterval);
        return;
      }
      */

      // Mock ping for demonstration
      await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 100)); // 50-150ms delay
      const pingTime = Date.now() - startTime;
      console.log(`Mock keepalive ping successful (${pingTime}ms) at ${new Date().toTimeString()}`);

    } catch (error) {
      console.error('Keepalive task error:', error);
      clearInterval(keepaliveInterval);
    }
  }, config.pingInterval);

  // Handle graceful shutdown
  process.on('SIGINT', () => {
    clearInterval(keepaliveInterval);
    console.log('Keepalive task stopped');
    process.exit(0);
  });

  // Return a promise that never resolves (keeps running)
  return new Promise(() => {});
}

// Advanced keepalive manager
class KeepaliveManager {
  private config: KeepaliveConfig;
  private health: ConnectionHealth;
  private keepaliveInterval?: NodeJS.Timeout;
  private isRunning: boolean = false;

  constructor(
    // private nextblockClient: any, // Your generated gRPC client
    config: KeepaliveConfig = defaultKeepaliveConfig
  ) {
    this.config = config;
    this.health = {
      isHealthy: true,
      lastSuccessfulPing: new Date(),
      consecutiveFailures: 0,
      totalPingsSent: 0,
      totalPingsSuccessful: 0,
      averagePingTime: 0,
    };
  }

  async start(): Promise<void> {
    if (this.isRunning) return;

    this.isRunning = true;
    this.keepaliveInterval = setInterval(
      () => this.sendPing(),
      this.config.pingInterval
    );
    
    console.log('Keepalive manager started');
  }

  async stop(): Promise<void> {
    this.isRunning = false;
    
    if (this.keepaliveInterval) {
      clearInterval(this.keepaliveInterval);
      this.keepaliveInterval = undefined;
    }
    
    console.log('Keepalive manager stopped');
  }

  private async sendPing(): Promise<void> {
    const startTime = Date.now();
    
    try {
      /* Uncomment when you have the generated gRPC client
      await Promise.race([
        this.nextblockClient.ping({}),
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('Timeout')), this.config.timeout)
        )
      ]);
      */

      // Mock ping delay
      await new Promise(resolve => 
        setTimeout(resolve, 50 + Math.random() * 100)
      );

      const pingTime = Date.now() - startTime;
      this.updateHealthSuccess(pingTime);

      console.log(
        `Keepalive ping successful (${pingTime}ms) - ` +
        `Health: ${this.getSuccessRate().toFixed(1)}%`
      );

    } catch (error) {
      this.updateHealthFailure();
      console.error(`Keepalive ping failed: ${error}`);

      if (!this.health.isHealthy) {
        await this.handleConnectionRecovery();
      }
    }
  }

  private updateHealthSuccess(pingTime: number): void {
    this.health.isHealthy = true;
    this.health.lastSuccessfulPing = new Date();
    this.health.consecutiveFailures = 0;
    this.health.totalPingsSent++;
    this.health.totalPingsSuccessful++;

    // Update average ping time
    if (this.health.totalPingsSuccessful === 1) {
      this.health.averagePingTime = pingTime;
    } else {
      this.health.averagePingTime = (
        (this.health.averagePingTime * (this.health.totalPingsSuccessful - 1) + pingTime) /
        this.health.totalPingsSuccessful
      );
    }
  }

  private updateHealthFailure(): void {
    this.health.consecutiveFailures++;
    this.health.totalPingsSent++;

    if (this.health.consecutiveFailures >= this.config.maxConsecutiveFailures) {
      this.health.isHealthy = false;
      console.warn(
        `Connection marked as unhealthy after ${this.health.consecutiveFailures} consecutive failures`
      );
    }
  }

  private async handleConnectionRecovery(): Promise<void> {
    console.warn('Connection unhealthy, attempting recovery...');
    
    // Wait before attempting recovery
    await new Promise(resolve => setTimeout(resolve, this.config.reconnectDelay));
    
    try {
      // Implement connection recovery logic here
      console.log('Connection recovery attempted');
      
      // Reset some health metrics on successful recovery
      // this.health.consecutiveFailures = 0;
      
    } catch (error) {
      console.error('Connection recovery failed:', error);
    }
  }

  getHealth(): ConnectionHealth {
    return { ...this.health };
  }

  isHealthy(): boolean {
    return this.health.isHealthy;
  }

  private getSuccessRate(): number {
    if (this.health.totalPingsSent === 0) return 100;
    return (this.health.totalPingsSuccessful / this.health.totalPingsSent) * 100;
  }
}

// Connection manager with integrated keepalive
class ConnectionManagerWithKeepalive {
  private nextblockConfig: any;
  private keepaliveConfig: KeepaliveConfig;
  private keepaliveManager?: KeepaliveManager;
  // private client?: any;
  private isConnected: boolean = false;

  constructor(
    nextblockConfig: any,
    keepaliveConfig: KeepaliveConfig = defaultKeepaliveConfig
  ) {
    this.nextblockConfig = nextblockConfig;
    this.keepaliveConfig = keepaliveConfig;
  }

  async connect(): Promise<boolean> {
    try {
      // Create connection (see connection.md)
      // const connection = await createNextBlockClient(this.nextblockConfig);
      // this.client = connection.client;

      // Test connection
      // await this.client.ping({});

      this.isConnected = true;
      console.log('Successfully connected to NextBlock');

      // Start keepalive
      this.keepaliveManager = new KeepaliveManager(
        // this.client,
        this.keepaliveConfig
      );
      await this.keepaliveManager.start();

      return true;

    } catch (error) {
      console.error('Failed to connect:', error);
      this.isConnected = false;
      return false;
    }
  }

  async disconnect(): Promise<void> {
    if (this.keepaliveManager) {
      await this.keepaliveManager.stop();
    }

    this.isConnected = false;
    console.log('Disconnected from NextBlock');
  }

  getConnectionHealth(): ConnectionHealth | null {
    return this.keepaliveManager?.getHealth() || null;
  }

  get connected(): boolean {
    return this.isConnected;
  }

  // get grpcClient(): any {
  //   return this.client;
  // }
}

// Health monitoring and alerting
class HealthMonitor {
  private keepaliveManager: KeepaliveManager;
  private alertThresholds = {
    successRate: 90.0,        // Alert if success rate < 90%
    avgPingTime: 1000,        // Alert if avg ping time > 1s
    consecutiveFailures: 2,   // Alert after 2 consecutive failures
  };
  private monitorInterval?: NodeJS.Timeout;

  constructor(keepaliveManager: KeepaliveManager) {
    this.keepaliveManager = keepaliveManager;
  }

  startMonitoring(checkInterval: number = 30000): void {
    this.monitorInterval = setInterval(() => {
      this.checkHealth();
    }, checkInterval);

    console.log(`Health monitoring started with ${checkInterval}ms interval`);
  }

  stopMonitoring(): void {
    if (this.monitorInterval) {
      clearInterval(this.monitorInterval);
      this.monitorInterval = undefined;
    }
    console.log('Health monitoring stopped');
  }

  private checkHealth(): void {
    const health = this.keepaliveManager.getHealth();

    // Check success rate
    const successRate = health.totalPingsSent > 0 
      ? (health.totalPingsSuccessful / health.totalPingsSent) * 100 
      : 100;

    if (successRate < this.alertThresholds.successRate) {
      this.triggerAlert('Low success rate', `Success rate: ${successRate.toFixed(1)}%`);
    }

    // Check average ping time
    if (health.averagePingTime > this.alertThresholds.avgPingTime) {
      this.triggerAlert('High ping time', `Average ping time: ${health.averagePingTime.toFixed(1)}ms`);
    }

    // Check consecutive failures
    if (health.consecutiveFailures >= this.alertThresholds.consecutiveFailures) {
      this.triggerAlert('Connection issues', `Consecutive failures: ${health.consecutiveFailures}`);
    }
  }

  private triggerAlert(alertType: string, details: string): void {
    const timestamp = new Date().toISOString();
    console.warn(`🚨 HEALTH ALERT [${timestamp}] ${alertType}: ${details}`);

    // Implement additional alerting logic here
    // - Send notifications
    // - Post to monitoring systems
    // - Trigger recovery procedures
  }
}

Usage Examples

// Basic keepalive example
async function basicKeepaliveExample(): Promise<void> {
  console.log('Starting basic keepalive example...');

  // Connect to NextBlock (see connection.md)
  // const config = configFromEnv();
  // const connection = await createNextBlockClient(config);

  // Start keepalive task
  const keepalivePromise = startKeepaliveTask(
    // connection.client,
    {
      pingInterval: 30000,     // Ping every 30 seconds
      maxConsecutiveFailures: 3,
      reconnectDelay: 5000,
      healthCheckEnabled: true,
      timeout: 15000,
    }
  );

  console.log('Application running with keepalive...');

  // Simulate application work
  setTimeout(() => {
    console.log('Application work completed, stopping...');
    process.exit(0);
  }, 300000); // Run for 5 minutes

  await keepalivePromise;
}

// Advanced keepalive with health monitoring
async function advancedKeepaliveExample(): Promise<void> {
  console.log('Starting advanced keepalive example...');

  // Configuration
  const keepaliveConfig: KeepaliveConfig = {
    pingInterval: 30000,      // Ping every 30 seconds
    maxConsecutiveFailures: 3,
    reconnectDelay: 10000,    // Wait 10 seconds before reconnection
    healthCheckEnabled: true,
    timeout: 15000,
  };

  // Use connection manager with integrated keepalive
  const manager = new ConnectionManagerWithKeepalive(
    {}, // nextblockConfig placeholder
    keepaliveConfig
  );

  try {
    const connected = await manager.connect();

    if (connected) {
      console.log('Connected with keepalive enabled');

      // Start health monitoring
      const healthMonitor = new HealthMonitor(manager.keepaliveManager!);
      healthMonitor.startMonitoring(60000); // Check every minute

      // Simulate application work with periodic health checks
      for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, 30000));

        const health = manager.getConnectionHealth();
        if (health) {
          console.log(`Connection health check ${i + 1}:`);
          console.log(`  Healthy: ${health.isHealthy}`);
          console.log(`  Success rate: ${((health.totalPingsSuccessful / Math.max(health.totalPingsSent, 1)) * 100).toFixed(1)}%`);
          console.log(`  Avg ping time: ${health.averagePingTime.toFixed(1)}ms`);
          console.log(`  Total pings: ${health.totalPingsSent}`);
        }
      }

      healthMonitor.stopMonitoring();

    } else {
      console.error('Failed to establish connection');
    }

  } catch (error) {
    console.error('Advanced keepalive example failed:', error);
  } finally {
    await manager.disconnect();
  }
}

// Connection pool with keepalive
class ConnectionPoolWithKeepalive {
  private connections: ConnectionManagerWithKeepalive[] = [];
  private currentIndex: number = 0;
  private poolSize: number;
  private nextblockConfig: any;
  private keepaliveConfig: KeepaliveConfig;

  constructor(
    nextblockConfig: any,
    keepaliveConfig: KeepaliveConfig,
    poolSize: number = 5
  ) {
    this.nextblockConfig = nextblockConfig;
    this.keepaliveConfig = keepaliveConfig;
    this.poolSize = poolSize;
  }

  async initialize(): Promise<void> {
    console.log(`Initializing connection pool with ${this.poolSize} connections...`);

    const connectionPromises = Array.from({ length: this.poolSize }, async (_, i) => {
      const manager = new ConnectionManagerWithKeepalive(
        this.nextblockConfig,
        this.keepaliveConfig
      );

      const connected = await manager.connect();
      if (connected) {
        this.connections.push(manager);
        console.log(`Connection ${i + 1} established with keepalive`);
      } else {
        console.warn(`Failed to create connection ${i + 1}`);
      }
    });

    await Promise.all(connectionPromises);
    console.log(`Connection pool initialized with ${this.connections.length} connections`);
  }

  getConnection(): ConnectionManagerWithKeepalive {
    if (this.connections.length === 0) {
      throw new Error('No available connections in pool');
    }

    const connection = this.connections[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.connections.length;
    return connection;
  }

  async closeAll(): Promise<void> {
    await Promise.all(this.connections.map(conn => conn.disconnect()));
    this.connections = [];
    console.log('All connections closed');
  }

  getPoolHealth(): ConnectionHealth[] {
    return this.connections
      .map(conn => conn.getConnectionHealth())
      .filter((health): health is ConnectionHealth => health !== null);
  }
}

// Main example runner
async function main(): Promise<void> {
  console.log('NextBlock Keepalive Examples');

  const choice = process.argv[2] || '1';

  switch (choice) {
    case '1':
      console.log('\n1. Basic Keepalive:');
      await basicKeepaliveExample();
      break;
    
    case '2':
      console.log('\n2. Advanced Keepalive with Monitoring:');
      await advancedKeepaliveExample();
      break;
    
    case '3':
      console.log('\n3. Connection Pool with Keepalive:');
      const pool = new ConnectionPoolWithKeepalive(
        {}, // config placeholder
        defaultKeepaliveConfig,
        3
      );
      
      await pool.initialize();
      
      // Use the pool for some time
      setTimeout(async () => {
        const poolHealth = pool.getPoolHealth();
        console.log(`Pool health summary: ${poolHealth.length} healthy connections`);
        await pool.closeAll();
      }, 60000);
      
      break;
    
    default:
      console.log('Usage: node keepalive.js [1|2|3]');
      console.log('  1: Basic keepalive');
      console.log('  2: Advanced keepalive with monitoring');
      console.log('  3: Connection pool with keepalive');
  }
}

if (require.main === module) {
  main().catch(console.error);
}

export {
  KeepaliveConfig,
  ConnectionHealth,
  KeepaliveManager,
  ConnectionManagerWithKeepalive,
  HealthMonitor,
  ConnectionPoolWithKeepalive,
  startKeepaliveTask,
};

Best Practices

  1. Appropriate intervals: Use 30-60 second ping intervals for most applications

  2. Health monitoring: Track connection health and implement alerting

  3. Graceful recovery: Handle connection failures with exponential backoff

  4. Resource cleanup: Always stop keepalive tasks when shutting down

  5. Timeout handling: Set reasonable timeouts for ping requests

  6. Logging: Log keepalive events for debugging and monitoring

  7. Integration: Integrate keepalive with your connection management system

  8. Connection pooling: Use connection pools for high-throughput applications

Last updated