File size: 2,936 Bytes
94e1b2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import type express from 'express'
import type { Server } from 'http'

interface LoggerLike {
  info(message: string, meta?: unknown): void
  warn(message: string, meta?: unknown): void
  error(message: string, meta?: unknown): void
}

export function tryListen(
  app: express.Express,
  port: number,
  host: string,
  logger: LoggerLike,
  retries = 3
): Promise<Server> {
  return new Promise((resolve, reject) => {
    const attemptListen = (attemptNumber: number) => {
      const server = app
        .listen(port, host)
        .on('listening', () => {
          logger.info(`๐Ÿš€ Server listening on http://${host}:${port}`)
          logger.info(`๐Ÿ“ Environment: ${process.env.NODE_ENV || 'development'}`)
          logger.info(`๐Ÿ” Health check: http://${host}:${port}/health`)
          resolve(server)
        })
        .on('error', (error: NodeJS.ErrnoException) => {
          if (error.code === 'EADDRINUSE') {
            logger.warn(`Port ${port} is in use, attempt ${attemptNumber}/${retries}`)
            if (attemptNumber < retries) {
              setTimeout(() => {
                attemptListen(attemptNumber + 1)
              }, 1000 * attemptNumber)
            } else {
              logger.error(`Failed to bind to port ${port} after ${retries} attempts`)
              reject(
                new Error(
                  `Port ${port} is already in use. Please stop the existing process or use a different port.`
                )
              )
            }
          } else {
            logger.error('Server error', { error })
            reject(error)
          }
        })
    }

    attemptListen(1)
  })
}

interface ShutdownOptions {
  getServer: () => Server | null
  onCleanup: () => Promise<void>
  logger: LoggerLike
}

export function setupShutdownHandlers(options: ShutdownOptions): void {
  const { getServer, onCleanup, logger } = options

  const shutdown = async (signal: string): Promise<void> => {
    logger.info(`Received ${signal}, starting graceful shutdown...`)

    const server = getServer()
    if (!server) {
      logger.warn('Server instance not found, skipping server close')
      await onCleanup()
      process.exit(0)
      return
    }

    server.close(async (err) => {
      if (err) {
        logger.error('Error closing server', { error: err })
        process.exit(1)
      }
      await onCleanup()
      process.exit(0)
    })

    setTimeout(() => {
      logger.warn('Forced shutdown after timeout')
      process.exit(1)
    }, 10 * 60 * 1000)
  }

  process.on('SIGTERM', () => shutdown('SIGTERM'))
  process.on('SIGINT', () => shutdown('SIGINT'))

  process.on('uncaughtException', (error) => {
    logger.error('Uncaught exception', { error })
    shutdown('UNCAUGHT_EXCEPTION')
  })

  process.on('unhandledRejection', (reason, promise) => {
    logger.error('Unhandled rejection', { reason, promise })
    shutdown('UNHANDLED_REJECTION')
  })
}