@@ -28,14 +28,31 @@ public class UserStats
2828 public int ChannelCount { get ; set ; }
2929}
3030
31+ enum BackfillResult { Complete , NeedRetry , Skipped }
32+
33+ class RetryState
34+ {
35+ public SocketTextChannel Channel ;
36+ public ulong OldestMessageId ;
37+ public DateTime NextRetryAt ;
38+ public int DelaySec ;
39+ public const int MaxDelaySec = 300 ;
40+ }
41+
3142[ Service ]
3243public class MessageArchiveService
3344{
3445 const string DB_PATH = "ArchiveData/messages.db" ;
3546
47+ static readonly Dictionary < ulong , ulong > KnownFirstMessages = new ( )
48+ {
49+ [ 264800866169651203 ] = 264813684281311234
50+ } ;
51+
3652 readonly DiscordSocketClient _discord ;
3753 readonly LoggingService _log ;
3854 readonly SemaphoreSlim _dbLock = new ( 1 , 1 ) ;
55+ readonly List < RetryState > _retryQueue = new ( ) ;
3956 volatile bool _backfillStarted ;
4057 volatile bool _backfillComplete ;
4158
@@ -56,10 +73,20 @@ void EnsureDatabase()
5673 using var ctx = CreateContext ( ) ;
5774 ctx . Database . EnsureCreated ( ) ;
5875
59- var hasMessages = ctx . Messages . Any ( ) ;
60- _backfillComplete = hasMessages ;
76+ var oldest = ctx . Messages . OrderBy ( m => m . Timestamp ) . FirstOrDefault ( ) ;
77+ _backfillComplete = true ;
6178
62- _log . Info ( $ "Archive database ready at { DB_PATH } (has data: { hasMessages } )") ;
79+ foreach ( var ( channelId , targetId ) in KnownFirstMessages )
80+ {
81+ var hasTarget = ctx . Messages . Any ( m => m . MessageId == targetId ) ;
82+ if ( ! hasTarget )
83+ {
84+ _backfillComplete = false ;
85+ break ;
86+ }
87+ }
88+
89+ _log . Info ( $ "Archive database ready at { DB_PATH } (has data: { oldest != null } , oldest: { oldest ? . Timestamp : O} )") ;
6390 }
6491
6592 public void Start ( )
@@ -130,13 +157,17 @@ async Task BackfillAllChannelsAsync()
130157
131158 _log . Info ( "Starting full channel backfill..." ) ;
132159
133- foreach ( var guild in _discord . Guilds )
160+ var guilds = _discord . Guilds . OrderBy ( g => g . Id ) . ToList ( ) ;
161+ foreach ( var guild in guilds )
134162 {
135163 foreach ( var channel in guild . TextChannels )
136164 {
137165 try
138166 {
139- await BackfillChannelAsync ( channel ) ;
167+ if ( await BackfillChannelAsync ( channel ) == BackfillResult . NeedRetry )
168+ {
169+ // retry handled by background loop
170+ }
140171 }
141172 catch ( Exception e )
142173 {
@@ -145,34 +176,137 @@ async Task BackfillAllChannelsAsync()
145176 }
146177 }
147178
148- _backfillComplete = true ;
149- _log . Info ( "Full backfill complete. All channels archived." ) ;
179+ if ( _retryQueue . Count == 0 )
180+ {
181+ _backfillComplete = true ;
182+ _log . Info ( "Full backfill complete. All channels archived." ) ;
183+ }
184+ else
185+ {
186+ _log . Info ( $ "Initial pass done. { _retryQueue . Count } channels need deep archive retry.") ;
187+ _ = RunRetryLoopAsync ( ) ;
188+ }
150189 }
151190
152- async Task BackfillChannelAsync ( SocketTextChannel channel )
191+ async Task RunRetryLoopAsync ( )
153192 {
154- var firstPage = await channel . GetMessagesAsync ( 1 ) . FlattenAsync ( ) ;
155- if ( ! firstPage . Any ( ) )
156- return ;
193+ while ( _retryQueue . Count > 0 )
194+ {
195+ var now = DateTime . UtcNow ;
196+ var due = new List < RetryState > ( ) ;
157197
158- var totalSaved = 0 ;
159- ulong ? oldestMessageId = null ;
198+ lock ( _retryQueue )
199+ {
200+ due . AddRange ( _retryQueue . Where ( r => now >= r . NextRetryAt ) ) ;
201+ }
202+
203+ foreach ( var state in due )
204+ {
205+ try
206+ {
207+ var result = await BackfillChannelAsync ( state . Channel , state . OldestMessageId ) ;
208+ if ( result == BackfillResult . Complete )
209+ {
210+ lock ( _retryQueue ) _retryQueue . Remove ( state ) ;
211+ _log . Info ( $ "Deep archive complete for #{ state . Channel . Name } .") ;
212+ }
213+ else if ( result == BackfillResult . NeedRetry )
214+ {
215+ state . DelaySec = Math . Min ( state . DelaySec * 2 , RetryState . MaxDelaySec ) ;
216+ state . NextRetryAt = DateTime . UtcNow . AddSeconds ( state . DelaySec ) ;
217+ }
218+ }
219+ catch ( Exception e )
220+ {
221+ state . DelaySec = Math . Min ( state . DelaySec * 2 , RetryState . MaxDelaySec ) ;
222+ state . NextRetryAt = DateTime . UtcNow . AddSeconds ( state . DelaySec ) ;
223+ _log . Error ( $ "Retry failed for #{ state . Channel . Name } : { e . Message } . Next retry in { state . DelaySec } s.") ;
224+ }
225+ }
226+
227+ if ( _retryQueue . Count == 0 )
228+ {
229+ _backfillComplete = true ;
230+ _log . Info ( "Deep archive retry complete. All channels fully archived." ) ;
231+ return ;
232+ }
233+
234+ await Task . Delay ( 5000 ) ;
235+ }
236+ }
237+
238+ async Task < BackfillResult > BackfillChannelAsync ( SocketTextChannel channel , ulong ? resumeFromId = null )
239+ {
240+ KnownFirstMessages . TryGetValue ( channel . Id , out var targetMessageId ) ;
160241
161- _log . Info ( $ "Backfilling #{ channel . Name } ...") ;
242+ if ( resumeFromId == null )
243+ {
244+ var firstPage = await channel . GetMessagesAsync ( 1 ) . FlattenAsync ( ) ;
245+ if ( ! firstPage . Any ( ) )
246+ return BackfillResult . Skipped ;
247+ resumeFromId = firstPage . First ( ) . Id ;
248+ _log . Info ( $ "Backfilling #{ channel . Name } ...") ;
249+ }
250+
251+ var totalSaved = 0 ;
252+ var oldestMessageId = resumeFromId ;
253+ var emptyRetries = 0 ;
254+ var delaySec = 5 ;
162255
163256 while ( true )
164257 {
165258 List < IMessage > messages ;
166- if ( oldestMessageId == null )
259+ try
167260 {
168- messages = ( await channel . GetMessagesAsync ( 100 ) . FlattenAsync ( ) ) . ToList ( ) ;
261+ messages = ( await channel . GetMessagesAsync ( oldestMessageId . Value , Direction . Before , 100 ) . FlattenAsync ( ) ) . ToList ( ) ;
169262 }
170- else
263+ catch ( Exception e )
171264 {
172- messages = ( await channel . GetMessagesAsync ( oldestMessageId . Value , Direction . Before , 100 ) . FlattenAsync ( ) ) . ToList ( ) ;
265+ _log . Warning ( $ "Backfill fetch error #{ channel . Name } : { e . Message } . Retry in { delaySec } s.") ;
266+ await Task . Delay ( TimeSpan . FromSeconds ( delaySec ) ) ;
267+ delaySec = Math . Min ( delaySec * 2 , 60 ) ;
268+ continue ;
269+ }
270+
271+ if ( messages . Count == 0 )
272+ {
273+ if ( targetMessageId > 0 && oldestMessageId > targetMessageId )
274+ {
275+ emptyRetries ++ ;
276+ _log . Info ( $ "Deep archive: #{ channel . Name } at ID { oldestMessageId } , waiting for Discord cold storage ({ emptyRetries } ).") ;
277+
278+ if ( ! _retryQueue . Any ( r => r . Channel . Id == channel . Id ) )
279+ {
280+ lock ( _retryQueue )
281+ {
282+ _retryQueue . Add ( new RetryState
283+ {
284+ Channel = channel ,
285+ OldestMessageId = oldestMessageId . Value ,
286+ NextRetryAt = DateTime . UtcNow . AddSeconds ( delaySec ) ,
287+ DelaySec = delaySec
288+ } ) ;
289+ }
290+ }
291+ else
292+ {
293+ lock ( _retryQueue )
294+ {
295+ var existing = _retryQueue . First ( r => r . Channel . Id == channel . Id ) ;
296+ existing . OldestMessageId = oldestMessageId . Value ;
297+ existing . NextRetryAt = DateTime . UtcNow . AddSeconds ( delaySec ) ;
298+ existing . DelaySec = Math . Min ( existing . DelaySec * 2 , RetryState . MaxDelaySec ) ;
299+ }
300+ }
301+
302+ return BackfillResult . NeedRetry ;
303+ }
304+
305+ break ;
173306 }
174307
175- if ( ! messages . Any ( ) ) break ;
308+ emptyRetries = 0 ;
309+ delaySec = 5 ;
176310
177311 var newRecords = new List < MessageRecord > ( ) ;
178312
@@ -227,12 +361,33 @@ async Task BackfillChannelAsync(SocketTextChannel channel)
227361
228362 oldestMessageId = messages . Last ( ) . Id ;
229363
230- if ( messages . Count < 100 ) break ;
364+ if ( messages . Count < 100 )
365+ {
366+ if ( targetMessageId > 0 && oldestMessageId > targetMessageId )
367+ {
368+ if ( ! _retryQueue . Any ( r => r . Channel . Id == channel . Id ) )
369+ {
370+ lock ( _retryQueue )
371+ {
372+ _retryQueue . Add ( new RetryState
373+ {
374+ Channel = channel ,
375+ OldestMessageId = oldestMessageId . Value ,
376+ NextRetryAt = DateTime . UtcNow . AddSeconds ( delaySec ) ,
377+ DelaySec = delaySec
378+ } ) ;
379+ }
380+ }
381+ return BackfillResult . NeedRetry ;
382+ }
383+ break ;
384+ }
231385
232386 await Task . Delay ( 200 ) ;
233387 }
234388
235389 _log . Info ( $ "Backfill for #{ channel . Name } complete. Saved { totalSaved } messages.") ;
390+ return BackfillResult . Complete ;
236391 }
237392
238393 ArchiveDbContext CreateContext ( )
0 commit comments