Close: guard channel delete with pendingDelete so a restart can't orphan it
The button and slash close paths deleted the channel via a bare setTimeout that never set the pendingDelete flag, so a restart in the 5s grace window orphaned the channel (closed in DB, still present in Discord) with no recovery — only the auto-close path used the flag correctly. Extract scheduleTicketChannelDelete() in services/tickets.js: a grace-delayed, queue-routed (enqueueDelete) delete that clears pendingDelete on success. All three close paths now use it. Button/slash set pendingDelete:true and keep discordThreadId populated so resumePendingDeletes() recovers the delete on the next boot. The button path previously nulled discordThreadId before the delete, which made the channel unrecoverable.
This commit is contained in:
@@ -293,6 +293,32 @@ async function attemptCloseTransition(gmailThreadId, extraSet = {}, extraUnset =
|
||||
return { transitioned, ticket };
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the final ticket-channel delete after a short grace period (so staff
|
||||
* read the close message first), routed through the channel queue.
|
||||
*
|
||||
* The delete is guarded by the `pendingDelete` flag: the caller MUST have already
|
||||
* set `pendingDelete: true` on the ticket AND left `discordThreadId` populated, so
|
||||
* that a restart during the grace window is recovered on boot by
|
||||
* resumePendingDeletes() (which re-fetches the channel and deletes it). The flag
|
||||
* is cleared once enqueueDelete resolves; if the doc is gone the unset is a no-op.
|
||||
*
|
||||
* Shared by all three close paths (auto-close, button, slash) so they behave
|
||||
* identically and none can orphan a channel on a mid-close restart.
|
||||
*/
|
||||
function scheduleTicketChannelDelete(channel, gmailThreadId, delayMs = 5000) {
|
||||
// Lazy require — broccolini-discord re-exports trackTimeout; cycle-safe at call time.
|
||||
const { trackTimeout } = require('../broccolini-discord');
|
||||
trackTimeout(setTimeout(() => {
|
||||
enqueueDelete(channel).then(() => {
|
||||
withRetry(() => Ticket.updateOne(
|
||||
{ gmailThreadId },
|
||||
{ $unset: { pendingDelete: '' } }
|
||||
)).catch(() => {});
|
||||
}).catch(() => {});
|
||||
}, delayMs));
|
||||
}
|
||||
|
||||
// --- SCHEDULED CHECKS ---
|
||||
// These accept `client` and optionally `sendTicketClosedEmail` to avoid circular deps.
|
||||
|
||||
@@ -340,16 +366,7 @@ async function checkAutoClose(client, sendTicketClosedEmail, _TicketModel, _reco
|
||||
if (_deps && _deps.scheduleDelete) {
|
||||
_deps.scheduleDelete(channel, ticket);
|
||||
} else {
|
||||
// Lazy require — broccolini-discord re-exports trackTimeout; cycle-safe.
|
||||
const { trackTimeout } = require('../broccolini-discord');
|
||||
trackTimeout(setTimeout(() => {
|
||||
enqueueDelete(channel).then(() => {
|
||||
withRetry(() => Ticket.updateOne(
|
||||
{ gmailThreadId: ticket.gmailThreadId },
|
||||
{ $unset: { pendingDelete: '' } }
|
||||
)).catch(() => {});
|
||||
}).catch(() => {});
|
||||
}, 5000));
|
||||
scheduleTicketChannelDelete(channel, ticket.gmailThreadId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -467,6 +484,7 @@ module.exports = {
|
||||
checkTicketCreationRateLimit,
|
||||
checkTicketLimits,
|
||||
attemptCloseTransition,
|
||||
scheduleTicketChannelDelete,
|
||||
checkAutoClose,
|
||||
checkAutoUnclaim,
|
||||
reconcileDeletedTicketChannels,
|
||||
|
||||
Reference in New Issue
Block a user