diff --git a/apps/api/src/collectives/collectives.module.ts b/apps/api/src/collectives/collectives.module.ts index 748e44c..1f21f3f 100644 --- a/apps/api/src/collectives/collectives.module.ts +++ b/apps/api/src/collectives/collectives.module.ts @@ -26,13 +26,29 @@ class NullProvisioner implements ProvisionerPort { let cachedClient: MatrixHttpClient | null = null; +/** Login is rate-limited by the homeserver per account; a deploy restart can + * trip it (M_LIMIT_EXCEEDED). Retry with backoff instead of failing boot. */ +const LOGIN_RETRY_DELAYS_MS = [5_000, 15_000, 45_000, 90_000]; + /** Lazily logs in the provisioner bot (once per process) and wraps it. */ async function buildProvisioner(logger?: Logger): Promise { const config = loadProvisionerConfig(); if (config === null) return new NullProvisioner(); if (cachedClient === null) { cachedClient = new MatrixHttpClient(config); - await cachedClient.login(); + for (let attempt = 0; ; attempt++) { + try { + await cachedClient.login(); + break; + } catch (error) { + const isLast = attempt >= LOGIN_RETRY_DELAYS_MS.length; + const rateLimited = /M_LIMIT_EXCEEDED|HTTP 429/.test(error instanceof Error ? error.message : String(error)); + if (isLast || !rateLimited) throw error; + const wait = LOGIN_RETRY_DELAYS_MS[attempt] ?? 90_000; + logger?.warn(`provisioner matrix login rate-limited; retry ${attempt + 1} in ${Math.round(wait / 1000)}s`); + await new Promise((resolve) => setTimeout(resolve, wait)); + } + } } return new MatrixProvisioner({ matrix: cachedClient, logger }); }