diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 0da48d160..01f7a0d9c 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -496,11 +496,6 @@ export async function createSiteResource( ); } } - - await rebuildClientAssociationsFromSiteResource( - newSiteResource, - trx - ); // we need to call this because we added to the admin role }); if (!newSiteResource) { @@ -526,6 +521,22 @@ export async function createSiteResource( await createCertificate(domainId, fullDomain, db); } + // Run in the background after the response is sent. Wrapped in its + // own transaction so it always executes on the primary — avoiding any + // replica-lag issues while still allowing the HTTP response to return + // early. + db.transaction(async (trx) => { + await rebuildClientAssociationsFromSiteResource( + newSiteResource!, + trx + ); + }).catch((err) => { + logger.error( + `Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`, + err + ); + }); + return response(res, { data: newSiteResource, success: true, diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index d0efa0cf4..8a3f93326 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -431,9 +431,6 @@ export async function updateSiteResource( }) .returning(); - // wait some time to allow for messages to be handled - await new Promise((resolve) => setTimeout(resolve, 750)); - const sshPamSet = isLicensedSshPam && (authDaemonPort !== undefined || @@ -556,11 +553,6 @@ export async function updateSiteResource( })) ); } - - await rebuildClientAssociationsFromSiteResource( - updatedSiteResource, - trx - ); } else { // Update the site resource const sshPamSet = @@ -690,7 +682,24 @@ export async function updateSiteResource( } logger.info(`Updated site resource ${siteResourceId}`); + } + }); + // Background: wait for removal messages to propagate, then rebuild + // associations for the re-created resource. Own transaction ensures + // execution on the primary against fully committed state. + (async () => { + await db.transaction(async (trx) => { + if (!updatedSiteResource) { + throw new Error("No updated resource found after update"); + } + if (sitesChanged) { + await new Promise((resolve) => setTimeout(resolve, 750)); + await rebuildClientAssociationsFromSiteResource( + updatedSiteResource, + trx + ); + } await handleMessagingForUpdatedSiteResource( existingSiteResource, updatedSiteResource, @@ -700,7 +709,12 @@ export async function updateSiteResource( })), trx ); - } + }); + })().catch((err) => { + logger.error( + `Error rebuilding client associations for site resource ${updatedSiteResource?.siteResourceId}:`, + err + ); }); return response(res, {