waJob.service.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. const waJobModel = require('../model/waJob.model')
  2. const { logInfo, logError } = require('../utils/logger')
  3. /**
  4. * Enqueue job kirim WA ke queue (asynchronous)
  5. * Context: WA_CREATE_LAPORAN, WA_REMINDER, dll
  6. */
  7. exports.enqueueWa = async (templateId, contacts, body, context = '', buttons = []) => {
  8. try {
  9. const job = await waJobModel.create({
  10. templateId,
  11. contacts,
  12. body,
  13. buttons,
  14. context,
  15. status: 'pending',
  16. nextRunAt: new Date(),
  17. })
  18. logInfo('WA_JOB', 'Job enqueued', { jobId: job._id, context, contactsCount: contacts.length })
  19. return job
  20. } catch (err) {
  21. logError('WA_JOB', 'Failed to enqueue WA job', { error: err.message, context })
  22. throw err
  23. }
  24. }
  25. /**
  26. * Claim & process next pending job
  27. * Menggunakan findOneAndUpdate untuk atomic lock (cegah double-process di multi-instance)
  28. */
  29. exports.processNextJob = async () => {
  30. try {
  31. const job = await waJobModel.findOneAndUpdate(
  32. {
  33. status: 'pending',
  34. nextRunAt: { $lte: new Date() },
  35. },
  36. {
  37. $set: {
  38. status: 'processing',
  39. claimedAt: new Date(),
  40. },
  41. },
  42. { new: true, sort: { nextRunAt: 1 } }
  43. )
  44. return job
  45. } catch (err) {
  46. logError('WA_JOB', 'Failed to claim next job', { error: err.message })
  47. return null
  48. }
  49. }
  50. /**
  51. * Mark job sukses
  52. */
  53. exports.markJobSuccess = async (jobId, response) => {
  54. try {
  55. await waJobModel.findByIdAndUpdate(jobId, {
  56. status: 'sent',
  57. response,
  58. lastError: null,
  59. })
  60. logInfo('WA_JOB', 'Job marked as sent', { jobId })
  61. } catch (err) {
  62. logError('WA_JOB', 'Failed to mark job success', { error: err.message, jobId })
  63. }
  64. }
  65. /**
  66. * Mark job gagal, increment attempt, jadwal retry
  67. */
  68. exports.markJobFailed = async (jobId, error, attempt) => {
  69. try {
  70. const maxAttempt = 3
  71. const backoffMs = Math.pow(2, attempt) * 1000 // 2s, 4s, 8s
  72. const nextRunAt = new Date(Date.now() + backoffMs)
  73. const willRetry = attempt < maxAttempt
  74. await waJobModel.findByIdAndUpdate(jobId, {
  75. status: willRetry ? 'pending' : 'failed',
  76. lastError: error.message || String(error),
  77. attempt,
  78. nextRunAt: willRetry ? nextRunAt : null,
  79. claimedAt: null,
  80. })
  81. logInfo('WA_JOB', `Job ${willRetry ? 'will retry' : 'permanently failed'}`, {
  82. jobId,
  83. attempt,
  84. nextRunAtMs: willRetry ? backoffMs : null,
  85. error: error.message,
  86. })
  87. } catch (err) {
  88. logError('WA_JOB', 'Failed to mark job failed', { error: err.message, jobId })
  89. }
  90. }
  91. /**
  92. * Reset in-progress jobs saat startup (kalau server crashed saat processing)
  93. */
  94. exports.resetStuckJobs = async () => {
  95. try {
  96. const result = await waJobModel.updateMany(
  97. { status: 'processing', claimedAt: { $lt: new Date(Date.now() - 60 * 1000) } },
  98. { $set: { status: 'pending', claimedAt: null } }
  99. )
  100. if (result.modifiedCount > 0) {
  101. logInfo('WA_JOB', 'Reset stuck jobs', { count: result.modifiedCount })
  102. }
  103. } catch (err) {
  104. logError('WA_JOB', 'Failed to reset stuck jobs', { error: err.message })
  105. }
  106. }
  107. /**
  108. * Get job status by ID
  109. */
  110. exports.getJobStatus = async (jobId) => {
  111. try {
  112. return await waJobModel.findById(jobId)
  113. } catch (err) {
  114. logError('WA_JOB', 'Failed to get job status', { error: err.message, jobId })
  115. return null
  116. }
  117. }