From 706971ef4116efec4fa526cff18df717b59d8359 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Thu, 16 Oct 2025 14:01:37 +0200 Subject: [PATCH] [web] shard compile server persistence by compileBackendClass (#29156) * [web] shard compile server persistence by compileBackendClass * [web] make prettier happy GitOrigin-RevId: d7cf8bde07e7110053d9d7531c007111af5cc46b --- .../src/Features/Compile/ClsiCookieManager.js | 46 +++++++++-- .../app/src/Features/Compile/ClsiManager.mjs | 77 +++++++++++++------ .../src/Compile/ClsiCookieManagerTests.js | 28 ++++++- 3 files changed, 117 insertions(+), 34 deletions(-) diff --git a/services/web/app/src/Features/Compile/ClsiCookieManager.js b/services/web/app/src/Features/Compile/ClsiCookieManager.js index a1ac0741b9..458c99d96b 100644 --- a/services/web/app/src/Features/Compile/ClsiCookieManager.js +++ b/services/web/app/src/Features/Compile/ClsiCookieManager.js @@ -20,7 +20,21 @@ if (Settings.redis.clsi_cookie_secondary != null) { } const ClsiCookieManagerFactory = function (backendGroup) { - function buildKey(projectId, userId) { + /** + * @param {string} projectId + * @param {string | null} userId + * @param {string} compileBackendClass + * @return {string} + */ + function buildKey(projectId, userId, compileBackendClass) { + if (backendGroup != null) { + return `clsiserver:${backendGroup}:${compileBackendClass}:${projectId}:${userId}` + } else { + return `clsiserver:${compileBackendClass}:${projectId}:${userId}` + } + } + + function buildOldKey(projectId, userId) { if (backendGroup != null) { return `clsiserver:${backendGroup}:${projectId}:${userId}` } else { @@ -37,7 +51,14 @@ const ClsiCookieManagerFactory = function (backendGroup) { if (!clsiCookiesEnabled) { return } - const serverId = await rclient.get(buildKey(projectId, userId)) + let serverId = await rclient.get( + buildKey(projectId, userId, compileBackendClass) + ) + if (!serverId) { + // Fallback to the old key. + // TODO(das7pad): remove this in 24h. + serverId = await rclient.get(buildOldKey(projectId, userId)) + } if (!serverId) { return await cookieManager.promises._populateServerIdViaRequest( @@ -157,7 +178,7 @@ const ClsiCookieManagerFactory = function (backendGroup) { if (serverId == null) { // We don't get a cookie back if it hasn't changed return await rclient.expire( - buildKey(projectId, userId), + buildKey(projectId, userId, compileBackendClass), _getTTLInSeconds(previous) ) } @@ -176,15 +197,28 @@ const ClsiCookieManagerFactory = function (backendGroup) { rclientSecondary, projectId, userId, + compileBackendClass, serverId ).catch(() => {}) } - await _setServerIdInRedis(rclient, projectId, userId, serverId) + await _setServerIdInRedis( + rclient, + projectId, + userId, + compileBackendClass, + serverId + ) } - async function _setServerIdInRedis(rclient, projectId, userId, serverId) { + async function _setServerIdInRedis( + rclient, + projectId, + userId, + compileBackendClass, + serverId + ) { await rclient.setex( - buildKey(projectId, userId), + buildKey(projectId, userId, compileBackendClass), _getTTLInSeconds(serverId), serverId ) diff --git a/services/web/app/src/Features/Compile/ClsiManager.mjs b/services/web/app/src/Features/Compile/ClsiManager.mjs index ed4b98e688..3f33b6e6bf 100644 --- a/services/web/app/src/Features/Compile/ClsiManager.mjs +++ b/services/web/app/src/Features/Compile/ClsiManager.mjs @@ -38,13 +38,53 @@ const CLSI_COOKIES_ENABLED = (Settings.clsiCookie?.key ?? '') !== '' // The timeout in services/clsi/app.js is 10 minutes, so we'll be on the safe side with 12 minutes const COMPILE_REQUEST_TIMEOUT_MS = 12 * 60 * 1000 -async function clearClsiServerId(projectId, userId) { - const jobs = [ClsiCookieManager.promises.clearServerId(projectId, userId)] +function getNewCompileBackendClass(projectId, compileBackendClass) { + // Sample x% of projects to move up one bracket. + if ( + SplitTestHandler.getPercentile(projectId, 'double-compile', 'release') >= + Settings.apis.clsi_new.sample + ) { + return null + } + + switch (compileBackendClass) { + case 'n2d': + return 'n4' + case 'c2d': + return 'n4' + default: + throw new Error('unknown ?compileBackendClass') + } +} + +/** + * @param {string} projectId + * @param {string | null} userId + * @param {string} compileBackendClass + * @return {Promise} + */ +async function clearClsiServerId(projectId, userId, compileBackendClass) { + const jobs = [ + ClsiCookieManager.promises.clearServerId( + projectId, + userId, + compileBackendClass + ), + ] if (Settings.apis.clsi_new?.url) { // Mirror resetting the clsiserverid in both backends. - jobs.push( - NewBackendCloudClsiCookieManager.promises.clearServerId(projectId, userId) + const newCompileBackendClass = getNewCompileBackendClass( + projectId, + compileBackendClass ) + if (newCompileBackendClass) { + jobs.push( + NewBackendCloudClsiCookieManager.promises.clearServerId( + projectId, + userId + ) + ) + } } await Promise.all(jobs) } @@ -176,14 +216,14 @@ async function deleteAuxFiles(projectId, userId, options, clsiserverid) { await DocumentUpdaterHandler.promises.clearProjectState(projectId) } finally { // always clear the clsi server id, even if prior actions failed - await clearClsiServerId(projectId, userId) + await clearClsiServerId(projectId, userId, compileBackendClass) } } } async function _sendBuiltRequest(projectId, userId, req, options) { if (options.forceNewClsiServer) { - await clearClsiServerId(projectId, userId) + await clearClsiServerId(projectId, userId, options.compileBackendClass) } const validationProblems = ClsiFormatChecker.checkRecoursesForProblems( req.compile?.resources @@ -436,28 +476,15 @@ async function _makeNewBackendRequest( if (Settings.apis.clsi_new?.url == null) { return null } + const newCompileBackendClass = getNewCompileBackendClass( + projectId, + currentCompileBackendClass + ) + if (!newCompileBackendClass) return null + url = new URL( url.toString().replace(Settings.apis.clsi.url, Settings.apis.clsi_new.url) ) - - // Sample x% of projects to move up one bracket. - if ( - SplitTestHandler.getPercentile(projectId, 'double-compile', 'release') >= - Settings.apis.clsi_new.sample - ) - return null - - let newCompileBackendClass - switch (currentCompileBackendClass) { - case 'n2d': - newCompileBackendClass = 'n4' - break - case 'c2d': - newCompileBackendClass = 'n4' - break - default: - throw new Error('unknown ?compileBackendClass') - } url.searchParams.set('compileBackendClass', newCompileBackendClass) const clsiServerId = diff --git a/services/web/test/unit/src/Compile/ClsiCookieManagerTests.js b/services/web/test/unit/src/Compile/ClsiCookieManagerTests.js index 2fa3877dba..e1991257c1 100644 --- a/services/web/test/unit/src/Compile/ClsiCookieManagerTests.js +++ b/services/web/test/unit/src/Compile/ClsiCookieManagerTests.js @@ -54,6 +54,28 @@ describe('ClsiCookieManager', function () { '', 'n2d' ) + this.redis.get + .calledWith(`clsiserver:n2d:${this.project_id}:${this.user_id}`) + .should.equal(true) + serverId.should.equal('clsi-7') + }) + + it('should fallback to old key', async function () { + this.redis.get + .withArgs(`clsiserver:n2d:${this.project_id}:${this.user_id}`) + .resolves(null) + this.redis.get + .withArgs(`clsiserver:${this.project_id}:${this.user_id}`) + .resolves('clsi-7') + const serverId = await this.ClsiCookieManager.promises.getServerId( + this.project_id, + this.user_id, + '', + 'n2d' + ) + this.redis.get + .calledWith(`clsiserver:n2d:${this.project_id}:${this.user_id}`) + .should.equal(true) this.redis.get .calledWith(`clsiserver:${this.project_id}:${this.user_id}`) .should.equal(true) @@ -177,7 +199,7 @@ describe('ClsiCookieManager', function () { null ) this.redis.setex.should.have.been.calledWith( - `clsiserver:${this.project_id}:${this.user_id}`, + `clsiserver:n2d:${this.project_id}:${this.user_id}`, this.settings.clsiCookie.ttlInSeconds, this.clsiServerId ) @@ -194,7 +216,7 @@ describe('ClsiCookieManager', function () { null ) expect(this.redis.setex).to.have.been.calledWith( - `clsiserver:${this.project_id}:${this.user_id}`, + `clsiserver:n2d:${this.project_id}:${this.user_id}`, this.settings.clsiCookie.ttlInSecondsRegular, this.clsiServerId ) @@ -245,7 +267,7 @@ describe('ClsiCookieManager', function () { null ) this.redis_secondary.setex.should.have.been.calledWith( - `clsiserver:${this.project_id}:${this.user_id}`, + `clsiserver:n2d:${this.project_id}:${this.user_id}`, this.settings.clsiCookie.ttlInSeconds, this.clsiServerId )