Skip to content

Commit db2103b

Browse files
blubskyeclaude
andcommitted
Fix autoclean channel cloning loop during API outages
- Add cleaningLocks sync.Map to prevent concurrent cleans of same channel - Add connection check before running scheduled cleans - Move DB update to immediately after channel creation (before deletion) - Make old channel deletion non-critical (new channel is already functional) - Improve logging with [AutoClean] prefix for better traceability This prevents the issue where API failures during channel deletion caused the autoclean to repeatedly clone channels because the DB still had the old channel ID. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 582aa43 commit db2103b

File tree

1 file changed

+59
-25
lines changed

1 file changed

+59
-25
lines changed

internal/bot/cleaner.go

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ package bot
33
import (
44
"fmt"
55
"log"
6+
"sync"
67
"time"
78

89
"github.com/bwmarrin/discordgo"
910
)
1011

1112
// AutoCleanWorker manages automatic channel cleaning
1213
type AutoCleanWorker struct {
13-
bot *Bot
14-
stopChan chan bool
15-
ticker *time.Ticker
14+
bot *Bot
15+
stopChan chan bool
16+
ticker *time.Ticker
17+
cleaningLocks sync.Map // Prevents concurrent cleans of the same channel
1618
}
1719

1820
// NewAutoCleanWorker creates a new auto-clean worker
@@ -49,9 +51,15 @@ func (w *AutoCleanWorker) Stop() {
4951

5052
// checkScheduledCleans checks for channels that need cleaning
5153
func (w *AutoCleanWorker) checkScheduledCleans() {
54+
// Don't run if bot is not connected
55+
if w.bot.Session == nil || w.bot.Session.State == nil || w.bot.Session.State.User == nil {
56+
log.Printf("[AutoClean] Skipping check - bot not connected")
57+
return
58+
}
59+
5260
rows, err := w.bot.DB.Query(`
5361
SELECT guild_id, channel_id, interval_hours, warning_minutes, next_run, custom_message, custom_image
54-
FROM autoclean
62+
FROM autoclean
5563
WHERE enabled = 1 AND datetime(next_run) <= datetime('now')
5664
`)
5765
if err != nil {
@@ -69,19 +77,38 @@ func (w *AutoCleanWorker) checkScheduledCleans() {
6977
continue
7078
}
7179

80+
// Check if already cleaning this channel (prevent duplicate cleans)
81+
lockKey := guildID + ":" + channelID
82+
if _, alreadyCleaning := w.cleaningLocks.LoadOrStore(lockKey, true); alreadyCleaning {
83+
log.Printf("[AutoClean] Channel %s already being cleaned, skipping", channelID)
84+
continue
85+
}
86+
7287
// Clean the channel
7388
go w.cleanChannel(guildID, channelID, intervalHours, customMessage, customImage)
7489
}
7590
}
7691

7792
// cleanChannel performs the actual channel cleaning
7893
func (w *AutoCleanWorker) cleanChannel(guildID, channelID string, intervalHours int, customMessage, customImage string) {
79-
log.Printf("Cleaning channel %s in guild %s", channelID, guildID)
94+
lockKey := guildID + ":" + channelID
95+
96+
// Always release the lock when done
97+
defer w.cleaningLocks.Delete(lockKey)
98+
99+
log.Printf("[AutoClean] Cleaning channel %s in guild %s", channelID, guildID)
100+
101+
// Double-check connection before proceeding
102+
if w.bot.Session == nil || w.bot.Session.State == nil {
103+
log.Printf("[AutoClean] Aborting clean - bot disconnected")
104+
w.markCleanFailed(guildID, channelID)
105+
return
106+
}
80107

81108
// Get the original channel
82109
oldChannel, err := w.bot.Session.Channel(channelID)
83110
if err != nil {
84-
log.Printf("Failed to get channel %s: %v", channelID, err)
111+
log.Printf("[AutoClean] Failed to get channel %s: %v", channelID, err)
85112
w.markCleanFailed(guildID, channelID)
86113
return
87114
}
@@ -101,25 +128,44 @@ func (w *AutoCleanWorker) cleanChannel(guildID, channelID string, intervalHours
101128
})
102129

103130
if err != nil {
104-
log.Printf("Failed to clone channel %s: %v", channelID, err)
131+
log.Printf("[AutoClean] Failed to clone channel %s: %v", channelID, err)
105132
w.markCleanFailed(guildID, channelID)
106133
return
107134
}
108135

136+
// CRITICAL: Update database IMMEDIATELY after creating new channel
137+
// This prevents the loop where old channel keeps getting selected
138+
nextRun := time.Now().Add(time.Duration(intervalHours) * time.Hour)
139+
_, err = w.bot.DB.Exec(`
140+
UPDATE autoclean
141+
SET channel_id = ?, next_run = ?, last_clean = datetime('now'), warned = 0
142+
WHERE guild_id = ? AND channel_id = ?`,
143+
newChannel.ID, nextRun.Format(time.RFC3339), guildID, channelID)
144+
145+
if err != nil {
146+
log.Printf("[AutoClean] CRITICAL: Failed to update database with new channel ID: %v", err)
147+
// Try to delete the new channel since we couldn't update DB
148+
w.bot.Session.ChannelDelete(newChannel.ID)
149+
w.markCleanFailed(guildID, channelID)
150+
return
151+
}
152+
153+
log.Printf("[AutoClean] Database updated: old=%s -> new=%s", channelID, newChannel.ID)
154+
109155
// Move new channel to same position (Discord might not respect position in create)
110156
newPosition := oldChannel.Position
111157
_, err = w.bot.Session.ChannelEditComplex(newChannel.ID, &discordgo.ChannelEdit{
112158
Position: &newPosition,
113159
})
114160
if err != nil {
115-
log.Printf("Warning: Failed to reposition channel: %v", err)
161+
log.Printf("[AutoClean] Warning: Failed to reposition channel: %v", err)
116162
}
117163

118-
// Delete the old channel
164+
// Delete the old channel (non-critical - if this fails, we still have a working new channel)
119165
_, err = w.bot.Session.ChannelDelete(channelID)
120166
if err != nil {
121-
log.Printf("Failed to delete old channel %s: %v", channelID, err)
122-
// Don't return - we still want to update the database with new channel ID
167+
log.Printf("[AutoClean] Warning: Failed to delete old channel %s: %v (new channel %s is active)", channelID, err, newChannel.ID)
168+
// Don't fail - the new channel is already set up and DB is updated
123169
}
124170

125171
// Send completion message
@@ -145,22 +191,10 @@ func (w *AutoCleanWorker) cleanChannel(guildID, channelID string, intervalHours
145191

146192
_, err = w.bot.Session.ChannelMessageSendEmbed(newChannel.ID, embed)
147193
if err != nil {
148-
log.Printf("Failed to send clean message: %v", err)
149-
}
150-
151-
// Update database with new channel ID and next run time
152-
nextRun := time.Now().Add(time.Duration(intervalHours) * time.Hour)
153-
_, err = w.bot.DB.Exec(`
154-
UPDATE autoclean
155-
SET channel_id = ?, next_run = ?, last_clean = datetime('now')
156-
WHERE guild_id = ? AND channel_id = ?`,
157-
newChannel.ID, nextRun.Format(time.RFC3339), guildID, channelID)
158-
159-
if err != nil {
160-
log.Printf("Failed to update autoclean database: %v", err)
194+
log.Printf("[AutoClean] Warning: Failed to send clean message: %v", err)
161195
}
162196

163-
log.Printf("Successfully cleaned channel. Old: %s, New: %s", channelID, newChannel.ID)
197+
log.Printf("[AutoClean] Successfully cleaned channel. Old: %s, New: %s", channelID, newChannel.ID)
164198
}
165199

166200
// markCleanFailed marks a clean as failed and reschedules

0 commit comments

Comments
 (0)