Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/steady-discord-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@roomote/web': patch
---

Harden the Discord gateway: quarantined (undeliverable) events now surface in
the Discord settings diagnostics instead of accumulating invisibly, the
durable inbound and dead-letter streams are bounded with capacity pressure
reported before shedding, a single transient Redis blip no longer drops a
healthy Gateway connection or ratchets delivery restarts to the maximum
backoff forever, and a dead gateway supervisor reports to error tracking
instead of only logging.
27 changes: 20 additions & 7 deletions apps/bullmq/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
createAdminDashboardMiddleware,
resolveAdminDashboardAuth,
} from './admin-auth';
import { initBullMqSentry } from './monitoring/sentry';
import { captureBullMqMessage, initBullMqSentry } from './monitoring/sentry';
import { getRedis, closeRedis } from './redis';
import { startScheduler } from './scheduler';
import { startSandboxOidcRefreshQueue } from './sandbox-oidc-refresh-queue';
Expand Down Expand Up @@ -69,12 +69,25 @@ const redis = getRedis();

initBullMqSentry();

const discordGatewaySupervisor = startDiscordGatewaySupervisor(redis, {
...process.env,
ENCRYPTION_KEY: Env.ENCRYPTION_KEY,
R_DISCORD_GATEWAY_SECRET: Env.R_DISCORD_GATEWAY_SECRET,
TRPC_URL: Env.TRPC_URL,
});
const discordGatewaySupervisor = startDiscordGatewaySupervisor(
redis,
{
...process.env,
ENCRYPTION_KEY: Env.ENCRYPTION_KEY,
R_DISCORD_GATEWAY_SECRET: Env.R_DISCORD_GATEWAY_SECRET,
TRPC_URL: Env.TRPC_URL,
},
{
// A dead supervisor silently stops Discord ingestion while this process
// stays green; make sure it pages instead of only logging.
onFatal: (error) =>
captureBullMqMessage(
`Discord gateway supervisor stopped unexpectedly: ${error instanceof Error ? error.message : String(error)}`,
undefined,
{ component: 'discord-gateway', signal: 'discord-gateway-fatal' },
),
},
);

const { schedulerQueue, schedulerWorker, schedulerQueueEvents } =
startScheduler();
Expand Down
12 changes: 12 additions & 0 deletions apps/discord-gateway/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const DEFAULT_DELIVERY_MAX_ATTEMPTS = 8;
const DEFAULT_DELIVERY_MAX_BACKOFF_MS = 30_000;
const DEFAULT_LOGIN_RETRY_BASE_MS = 15_000;
const DEFAULT_LOGIN_RETRY_MAX_MS = 5 * 60_000;
const DEFAULT_INBOUND_MAX_ENTRIES = 50_000;
const DEFAULT_DEAD_LETTER_MAX_ENTRIES = 1_000;

function positiveInteger(value: string | undefined, fallback: number): number {
const parsed = Number(value);
Expand Down Expand Up @@ -33,6 +35,8 @@ export type DiscordGatewayConfig = {
deliveryPollMs: number;
deliveryMaxAttempts: number;
deliveryMaxBackoffMs: number;
inboundMaxEntries: number;
deadLetterMaxEntries: number;
loginRetryBaseMs: number;
loginRetryMaxMs: number;
};
Expand Down Expand Up @@ -65,6 +69,14 @@ export function resolveDiscordGatewayConfig(
env.DISCORD_GATEWAY_DELIVERY_MAX_BACKOFF_MS,
DEFAULT_DELIVERY_MAX_BACKOFF_MS,
),
inboundMaxEntries: positiveInteger(
env.DISCORD_GATEWAY_INBOUND_MAX_ENTRIES,
DEFAULT_INBOUND_MAX_ENTRIES,
),
deadLetterMaxEntries: positiveInteger(
env.DISCORD_GATEWAY_DEAD_LETTER_MAX_ENTRIES,
DEFAULT_DEAD_LETTER_MAX_ENTRIES,
),
loginRetryBaseMs: positiveInteger(
env.DISCORD_GATEWAY_LOGIN_RETRY_BASE_MS,
DEFAULT_LOGIN_RETRY_BASE_MS,
Expand Down
56 changes: 56 additions & 0 deletions apps/discord-gateway/src/delivery-loop.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/discord-gateway/src/delivery-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export async function runSupervisedDeliveryLoop(
let restartDelayMs = params.pollMs;

while (!params.signal.aborted) {
const startedAt = Date.now();
try {
// This flag represents a running delivery worker, not simply the
// presence of an API secret. Any unexpected exit clears it first.
Expand All @@ -164,6 +165,12 @@ export async function runSupervisedDeliveryLoop(
throw new Error('Discord delivery loop exited unexpectedly');
} catch (error) {
if (params.signal.aborted) break;
// A long healthy run means this failure is a fresh incident, not part
// of the previous one — start the backoff over instead of ratcheting
// toward the lifetime maximum.
if (Date.now() - startedAt > restartMaxBackoffMs * 2) {
restartDelayMs = params.pollMs;
}
await params.status
.update({
forwardingReady: false,
Expand Down
17 changes: 16 additions & 1 deletion apps/discord-gateway/src/embedded.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions apps/discord-gateway/src/embedded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ export type DiscordGatewaySupervisor = {
export function startDiscordGatewaySupervisor(
redis: Redis,
env: NodeJS.ProcessEnv = process.env,
options: {
/**
* Invoked when the supervisor itself dies. Without it the only signal is
* a console line while the host process stays green and Discord
* ingestion is silently stopped — hosts should report this to their
* error tracker.
*/
onFatal?: (error: unknown) => void;
} = {},
): DiscordGatewaySupervisor {
const service = new DiscordGatewayService(
redis,
Expand All @@ -24,6 +33,15 @@ export function startDiscordGatewaySupervisor(
console.error(
`[discord-gateway] supervisor stopped unexpectedly: ${error instanceof Error ? error.message : String(error)}`,
);
void service.status
.update({
phase: 'error',
ready: false,
connected: false,
lastError: `Discord gateway supervisor stopped: ${error instanceof Error ? error.message : String(error)}`,
})
.catch(() => undefined);
options.onFatal?.(error);
});

return {
Expand Down
94 changes: 94 additions & 0 deletions apps/discord-gateway/src/inbound-queue.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading