Skip to content

Implement graceful shutdown that drains active WebSocket connections before exit #175

Description

@levibliz

Background

The current shutdown function in src/index.js calls wss.close() which stops accepting new connections but does not send WebSocket close frames to currently connected clients. Clients receive a hard TCP disconnect, losing any in-flight messages and potentially corrupting application state.

Category: Enhancement
Difficulty: Medium
Priority: High
Labels: enhancement, backend, infrastructure

Problem Statement

During rolling deploys or planned restarts (SIGTERM), all currently connected clients are hard-disconnected without warning. For fleet tracking, active trackers lose their session and must reconnect, potentially missing location updates during the gap.

Requirements

  • Before calling wss.close(), iterate over all connected clients and send a close frame with code 1001 (Going Away) and reason "Server shutting down".
  • Give clients 2 seconds to acknowledge before hard-terminating them.
  • After all clients are closed or the timeout elapses, call wss.close().
  • Emit a structured log entry for each client closed during drain: { clientId, code: 1001 }.
  • The forced exit timeout (5 seconds total) must still apply.
  • Update tests/index.test.js to assert that shutdown sends close frames to active clients.

Acceptance Criteria

  • shutdown() sends ws.close(1001, 'Server shutting down') to all OPEN clients.
  • wss.close() is called after clients have been notified.
  • Hard terminate fallback fires if drain exceeds 2 seconds.
  • Structured log entries emitted for each closed client include clientId.
  • Tests pass.

Implementation Notes

export function shutdown(wss, signal) {
  logger.info('Shutting down', { signal });
  for (const client of wss.clients) {
    if (client.readyState === WebSocket.OPEN) {
      logger.info('Closing client connection', { clientId: client._clientId ?? 'unknown', code: 1001 });
      client.close(1001, 'Server shutting down');
    }
  }
  const drainTimeout = setTimeout(() => {
    wss.close(() => { logger.info('Server closed'); process.exit(0); });
    setTimeout(() => { logger.error('Forced shutdown'); process.exit(1); }, 3000);
  }, 2000);
  drainTimeout.unref();
}

Files Likely to Change

  • src/index.js
  • tests/index.test.js

Definition of Done

Clients receive proper WebSocket close frames during server shutdown. Drain is tested with a mock wss.

Estimated Complexity

Medium — requires understanding the WebSocket lifecycle and careful timeout management.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions