index.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. const amqp = require('amqplib')
  2. const path = require('path')
  3. const config = require('../../config.json')
  4. const Logger = require('../../lib/Logger')
  5. const { mq } = require('./mqPrefix')
  6. class MQManager {
  7. constructor() {
  8. this.url = config.rabbitmq.url
  9. this.connection = null
  10. this.channels = new Map()
  11. this.logger = new Logger(path.join(__dirname, '../../logs/RabbitMQ.log'), 'INFO')
  12. this.reconnecting = false
  13. this._initPromise = null
  14. this._reconnectTimer = null
  15. this._reconnectAttempt = 0
  16. }
  17. async init() {
  18. if (this.connection) return
  19. if (this._initPromise) return this._initPromise
  20. this._initPromise = (async () => {
  21. try {
  22. this.logger.info('RabbitMQ 初始化连接...')
  23. const conn = await amqp.connect(this.url)
  24. this.connection = conn
  25. this._reconnectAttempt = 0
  26. conn.on('close', () => {
  27. this.logger.warn('RabbitMQ 连接断开,准备重连')
  28. this._dropConnection()
  29. this.reconnect()
  30. })
  31. conn.on('error', (err) => {
  32. // error 事件有时会在 close 前触发;这里不要 throw,交给 close 触发的重连来恢复
  33. this.logger.error('RabbitMQ 连接错误:', err?.message || err)
  34. })
  35. this.logger.info('RabbitMQ 连接成功')
  36. } catch (e) {
  37. this.logger.error('RabbitMQ 初始化失败:', e?.message || e)
  38. this._dropConnection()
  39. this.reconnect()
  40. throw e
  41. } finally {
  42. this._initPromise = null
  43. }
  44. })()
  45. return this._initPromise
  46. }
  47. _dropConnection() {
  48. this.connection = null
  49. // 旧的 channel 失效,直接清空;调用方需重新 getChannel
  50. this.channels.clear()
  51. }
  52. async reconnect() {
  53. if (this.reconnecting) return
  54. this.reconnecting = true
  55. const attempt = ++this._reconnectAttempt
  56. const delayMs = Math.min(30000, 1000 * Math.pow(2, Math.min(attempt, 5))) // 2s..32s capped
  57. if (this._reconnectTimer) {
  58. clearTimeout(this._reconnectTimer)
  59. this._reconnectTimer = null
  60. }
  61. this._reconnectTimer = setTimeout(async () => {
  62. try {
  63. await this.init()
  64. } catch {
  65. // init() 内已记录日志并触发下一次 reconnect
  66. } finally {
  67. this.reconnecting = false
  68. }
  69. }, delayMs)
  70. }
  71. async getChannel(name = 'default') {
  72. if (!this.connection) {
  73. await this.init()
  74. }
  75. const key = mq(name)
  76. if (this.channels.has(key)) {
  77. const cached = this.channels.get(key)
  78. if (!cached.__runforgeClosed) return cached
  79. this.channels.delete(key)
  80. }
  81. const channel = await this.connection.createChannel()
  82. channel.__runforgeClosed = false
  83. this.channels.set(key, channel)
  84. channel.on('close', () => {
  85. channel.__runforgeClosed = true
  86. this.logger.warn(`Channel [${key}] 已关闭`)
  87. this.channels.delete(key)
  88. })
  89. channel.on('error', (err) => {
  90. channel.__runforgeClosed = true
  91. this.logger.warn(`Channel [${key}] 错误: ${err?.message || err}`)
  92. this.channels.delete(key)
  93. })
  94. return channel
  95. }
  96. isChannelClosedError(err) {
  97. if (!err) return false
  98. const msg = String(err.message || err).toLowerCase()
  99. return msg.includes('channel closed') || msg.includes('illegaloperationerror')
  100. }
  101. /**
  102. * 安全投递:遇到断线/Channel closed 自动等待重连并重试。
  103. * 注意:只适用于“投递端”;消费端需要重新 consume(业务层处理)。
  104. */
  105. async sendToQueueSafe(channelName, queue, content, options = {}) {
  106. let lastErr
  107. for (let i = 0; i < 3; i++) {
  108. try {
  109. const ch = await this.getChannel(channelName)
  110. return ch.sendToQueue(queue, content, options)
  111. } catch (e) {
  112. lastErr = e
  113. if (!this.isChannelClosedError(e)) throw e
  114. this.channels.delete(channelName)
  115. this.reconnect()
  116. await new Promise((r) => setTimeout(r, 500 * (i + 1)))
  117. }
  118. }
  119. throw lastErr
  120. }
  121. invalidateChannel(name = 'default') {
  122. const key = mq(name)
  123. const channel = this.channels.get(key)
  124. if (channel) {
  125. channel.__runforgeClosed = true
  126. this.channels.delete(key)
  127. }
  128. }
  129. async close() {
  130. for (const ch of this.channels.values()) {
  131. await ch.close()
  132. }
  133. this.channels.clear()
  134. if (this.connection) {
  135. await this.connection.close()
  136. this.connection = null
  137. }
  138. }
  139. }
  140. module.exports = new MQManager()