From eadb1a63fa71726b7760bfb92036f561398b84fb Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 24 Jul 2025 17:49:28 -0300 Subject: [PATCH] Improve render retries and rate limits --- .../secret-sync/render/render-sync-fns.ts | 124 ++++++++++++------ 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts index 9140136a0..dffac1a8e 100644 --- a/backend/src/services/secret-sync/render/render-sync-fns.ts +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -8,7 +8,22 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types"; -const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials) => { +const MAX_RETRIES = 5; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + +const retrySleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 60000); + }); + +const getRenderEnvironmentSecrets = async ( + secretSync: TRenderSyncWithCredentials, + attempt = 0 +): Promise => { const { destinationConfig, connection: { @@ -22,35 +37,48 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential do { const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl; - const { data } = await request.get< - { - envVar: { - key: string; - value: string; - }; - cursor: string; - }[] - >(url, { - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json" + + try { + const { data } = await request.get< + { + envVar: { + key: string; + value: string; + }; + cursor: string; + }[] + >(url, { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json" + } + }); + + const secrets = data.map((item) => ({ + key: item.envVar.key, + value: item.envVar.value + })); + + allSecrets.push(...secrets); + cursor = data[data.length - 1]?.cursor; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) { + await retrySleep(); + return await getRenderEnvironmentSecrets(secretSync, attempt + 1); } - }); - - const secrets = data.map((item) => ({ - key: item.envVar.key, - value: item.envVar.value - })); - - allSecrets.push(...secrets); - - cursor = data[data.length - 1]?.cursor; + throw error; + } } while (cursor); return allSecrets; }; -const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap, key: string) => { +const putEnvironmentSecret = async ( + secretSync: TRenderSyncWithCredentials, + secretMap: TSecretMap, + key: string, + attempt = 0 +): Promise => { const { destinationConfig, connection: { @@ -58,22 +86,34 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr } } = secretSync; - await request.put( - `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`, - { - key, - value: secretMap[key].value - }, - { - headers: { - Authorization: `Bearer ${apiKey}`, - Accept: "application/json" + try { + await request.put( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`, + { + key, + value: secretMap[key].value + }, + { + headers: { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json" + } } + ); + } catch (error) { + if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) { + await retrySleep(); + return await putEnvironmentSecret(secretSync, secretMap, key, attempt + 1); } - ); + throw error; + } }; -const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick) => { +const deleteEnvironmentSecret = async ( + secretSync: TRenderSyncWithCredentials, + secret: Pick, + attempt = 0 +): Promise => { const { destinationConfig, connection: { @@ -97,18 +137,19 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s return; } + if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) { + await retrySleep(); + return await deleteEnvironmentSecret(secretSync, secret, attempt + 1); + } + throw error; } }; -const sleep = async () => - new Promise((resolve) => { - setTimeout(resolve, 500); - }); - export const RenderSyncFns = { syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { const renderSecrets = await getRenderEnvironmentSecrets(secretSync); + for await (const key of Object.keys(secretMap)) { // If value is empty skip it as render does not allow empty variables if (secretMap[key].value === "") { @@ -132,6 +173,7 @@ export const RenderSyncFns = { } } }, + getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise => { const renderSecrets = await getRenderEnvironmentSecrets(secretSync); return Object.fromEntries(renderSecrets.map((secret) => [secret.key, { value: secret.value ?? "" }]));