Skip to content

Commit d451678

Browse files
HandyS11claude
andcommitted
fix(discord): offload workspace heal + connection start off the Ready chain
Live verification showed these two handlers, not command registration, were what kept the gateway's Ready-blocking warning firing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 10190c2 commit d451678

2 files changed

Lines changed: 42 additions & 11 deletions

File tree

src/RustPlusBot.Features.Pairing/Hosting/PairingHostedService.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,28 @@ public async Task StopAsync(CancellationToken cancellationToken)
3030
await supervisor.StopAllAsync().ConfigureAwait(false);
3131
}
3232

33-
private async Task OnReadyAsync()
33+
private Task OnReadyAsync()
3434
{
3535
// Ready fires on every (re)connect; only start listeners once per process. Ready is dispatched
3636
// serially on the gateway thread, so the plain bool guard needs no synchronization (same pattern
37-
// as DiscordBotService and WorkspaceHostedService).
37+
// as DiscordBotService and WorkspaceHostedService) — set the flag before offloading so a
38+
// re-fired Ready can't double-start.
3839
if (_started)
3940
{
40-
return;
41+
return Task.CompletedTask;
4142
}
4243

4344
_started = true;
45+
46+
// Starting listeners connects to every active Rust server; doing it inline blocks the gateway
47+
// task and stalls event dispatch, so offload it. Failures must be caught here — nothing awaits
48+
// this.
49+
_ = Task.Run(StartListenersAsync);
50+
return Task.CompletedTask;
51+
}
52+
53+
private async Task StartListenersAsync()
54+
{
4455
try
4556
{
4657
await supervisor.StartAllActiveAsync().ConfigureAwait(false);

src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,44 @@ public async Task StopAsync(CancellationToken cancellationToken)
6868
}
6969
}
7070

71-
private async Task OnReadyAsync()
71+
private Task OnReadyAsync()
7272
{
73+
// Ready fires on every gateway (re)connect; only heal once per process. Ready is dispatched
74+
// serially on the gateway thread, so no synchronization is needed — set the flag before
75+
// offloading so a re-fired Ready can't double-run.
7376
if (_startupDone)
7477
{
75-
return;
78+
return Task.CompletedTask;
7679
}
7780

7881
_startupDone = true;
79-
var scope = scopeFactory.CreateAsyncScope();
80-
await using (scope.ConfigureAwait(false))
82+
83+
// Healing sweeps every provisioned guild's channels over REST; doing it inline blocks the
84+
// gateway task and stalls event dispatch, so offload it. Failures must be caught here —
85+
// nothing awaits this.
86+
_ = Task.Run(HealProvisionedGuildsAsync);
87+
return Task.CompletedTask;
88+
}
89+
90+
private async Task HealProvisionedGuildsAsync()
91+
{
92+
try
8193
{
82-
var store = scope.ServiceProvider.GetRequiredService<IWorkspaceStore>();
83-
var reconciler = scope.ServiceProvider.GetRequiredService<IWorkspaceReconciler>();
84-
foreach (var guildId in await store.GetProvisionedGuildIdsAsync().ConfigureAwait(false))
94+
var scope = scopeFactory.CreateAsyncScope();
95+
await using (scope.ConfigureAwait(false))
8596
{
86-
await reconciler.HealGuildAsync(guildId).ConfigureAwait(false);
97+
var store = scope.ServiceProvider.GetRequiredService<IWorkspaceStore>();
98+
var reconciler = scope.ServiceProvider.GetRequiredService<IWorkspaceReconciler>();
99+
foreach (var guildId in await store.GetProvisionedGuildIdsAsync().ConfigureAwait(false))
100+
{
101+
await reconciler.HealGuildAsync(guildId).ConfigureAwait(false);
102+
}
87103
}
88104
}
105+
catch (Exception ex) // Broad catch is intentional: a faulting startup heal must not crash the host.
106+
{
107+
logger.LogError(ex, "Startup self-heal failed.");
108+
}
89109
}
90110

91111
private async Task OnChannelDestroyedAsync(SocketChannel channel)

0 commit comments

Comments
 (0)