From d97a659f92352cba75326552285398c8ae8a9638 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 6 May 2026 09:22:41 +0200 Subject: [PATCH] [web] make double compile test parameters configurable via env vars (#33406) * [web] make double compile test parameters configurable via env vars * [k8s] web: enable double compile test for free compiles on n4 instances GitOrigin-RevId: 3a5cb8ed6d044fcf3f4c0d2b9d252326bac48511 --- .../app/src/Features/Compile/ClsiManager.mjs | 22 ++-- .../unit/src/Compile/ClsiManager.test.mjs | 111 ++++++++++++++++-- 2 files changed, 113 insertions(+), 20 deletions(-) diff --git a/services/web/app/src/Features/Compile/ClsiManager.mjs b/services/web/app/src/Features/Compile/ClsiManager.mjs index 1b9a28dc34..e2802af54e 100644 --- a/services/web/app/src/Features/Compile/ClsiManager.mjs +++ b/services/web/app/src/Features/Compile/ClsiManager.mjs @@ -82,23 +82,25 @@ async function clearBaseHistoryVersion(projectId, userId) { await rclient.del(_baseHistoryVersionKey(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 - } +function getDoubleCompilePercentile(projectId) { + return SplitTestHandler.getPercentile(projectId, 'double-compile', 'release') +} +function getNewCompileBackendClass(projectId, compileBackendClass) { + let cfg switch (compileBackendClass) { case 'c3d': - return 'n4' + cfg = Settings.apis.clsi_new.doubleCompileFree + break case 'c4d': - return 'n4' + cfg = Settings.apis.clsi_new.doubleCompilePremium + break default: throw new Error('unknown ?compileBackendClass') } + if (!cfg.backendClass || !cfg.sample) return null + if (getDoubleCompilePercentile(projectId) >= cfg.sample) return null + return cfg.backendClass } /** diff --git a/services/web/test/unit/src/Compile/ClsiManager.test.mjs b/services/web/test/unit/src/Compile/ClsiManager.test.mjs index bcdf503c77..288c923d7c 100644 --- a/services/web/test/unit/src/Compile/ClsiManager.test.mjs +++ b/services/web/test/unit/src/Compile/ClsiManager.test.mjs @@ -170,7 +170,14 @@ describe('ClsiManager', function () { submissionBackendClass: 'c3d', }, clsi_new: { - sample: 100, + doubleCompileFree: { + sample: 100, + backendClass: 'n4', + }, + doubleCompilePremium: { + sample: 100, + backendClass: 'n4d', + }, }, }, enablePdfCaching: true, @@ -1033,9 +1040,93 @@ describe('ClsiManager', function () { }) }) - describe('when a new backend is configured', function () { + describe('when a new backend is configured (free)', function () { beforeEach(async function (ctx) { - ctx.Settings.apis.clsi_new = { url: 'https://compiles.somewhere.test' } + ctx.Settings.apis.clsi_new.url = 'https://compiles.somewhere.test' + await ctx.ClsiManager.promises.sendRequest( + ctx.project._id, + ctx.user_id, + { + compileBackendClass: 'c3d', + compileGroup: 'standard', + } + ) + // wait for the background task to finish + await setTimeout(0) + }) + + it('makes a request to the new backend', function (ctx) { + expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledTwice + expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith( + sinon.match( + url => + url.host === CLSI_HOST && + url.pathname === + `/project/${ctx.project._id}/user/${ctx.user_id}/compile` && + url.searchParams.get('compileBackendClass') === 'c3d' && + url.searchParams.get('compileGroup') === 'standard' + ) + ) + expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith( + sinon.match( + url => + url.toString() === + `${ctx.Settings.apis.clsi_new.url}/project/${ctx.project._id}/user/${ctx.user_id}/compile?compileBackendClass=n4&compileGroup=standard` + ) + ) + }) + it('should record an event', function (ctx) { + expect( + ctx.AnalyticsManager.recordEventForUserInBackground + ).to.have.been.calledWith(ctx.user_id, 'double-compile-result', { + projectId: 'project-id', + compileBackendClass: 'c3d', + newCompileBackendClass: 'n4', + status: 'success', + compileTime: 1337, + newCompileTime: 1337, + clsiServerId: 'newserver', + newClsiServerId: 'clsi-server-id', + pdfSize: 42, + newPdfSize: 42, + }) + }) + }) + + describe('when a new backend is configured with low sample', function () { + beforeEach(async function (ctx) { + ctx.Settings.apis.clsi_new.url = 'https://compiles.somewhere.test' + ctx.Settings.apis.clsi_new.doubleCompileFree.sample = 0 + await ctx.ClsiManager.promises.sendRequest( + ctx.project._id, + ctx.user_id, + { + compileBackendClass: 'c3d', + compileGroup: 'standard', + } + ) + // wait for the background task to finish + await setTimeout(0) + }) + + it('does not make a request to the new backend', function (ctx) { + expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledOnce + expect(ctx.FetchUtils.fetchStringWithResponse).to.have.been.calledWith( + sinon.match( + url => + url.host === CLSI_HOST && + url.pathname === + `/project/${ctx.project._id}/user/${ctx.user_id}/compile` && + url.searchParams.get('compileBackendClass') === 'c3d' && + url.searchParams.get('compileGroup') === 'standard' + ) + ) + }) + }) + + describe('when a new backend is configured (premium)', function () { + beforeEach(async function (ctx) { + ctx.Settings.apis.clsi_new.url = 'https://compiles.somewhere.test' await ctx.ClsiManager.promises.sendRequest( ctx.project._id, ctx.user_id, @@ -1064,7 +1155,7 @@ describe('ClsiManager', function () { sinon.match( url => url.toString() === - `${ctx.Settings.apis.clsi_new.url}/project/${ctx.project._id}/user/${ctx.user_id}/compile?compileBackendClass=n4&compileGroup=priority` + `${ctx.Settings.apis.clsi_new.url}/project/${ctx.project._id}/user/${ctx.user_id}/compile?compileBackendClass=n4d&compileGroup=priority` ) ) }) @@ -1074,7 +1165,7 @@ describe('ClsiManager', function () { ).to.have.been.calledWith(ctx.user_id, 'double-compile-result', { projectId: 'project-id', compileBackendClass: 'c4d', - newCompileBackendClass: 'n4', + newCompileBackendClass: 'n4d', status: 'success', compileTime: 1337, newCompileTime: 1337, @@ -1239,7 +1330,7 @@ describe('ClsiManager', function () { describe('when a new backend is configured', function () { beforeEach(async function (ctx) { - ctx.Settings.apis.clsi_new = { url: 'https://compiles.somewhere.test' } + ctx.Settings.apis.clsi_new.url = 'https://compiles.somewhere.test' await ctx.ClsiManager.promises.deleteAuxFiles( ctx.project._id, ctx.user_id, @@ -1256,7 +1347,7 @@ describe('ClsiManager', function () { ).to.have.been.calledWith(ctx.project._id, ctx.user_id, 'c4d') expect( ctx.ClsiCookieManager.promises.clearServerId - ).to.have.been.calledWith(ctx.project._id, ctx.user_id, 'n4') + ).to.have.been.calledWith(ctx.project._id, ctx.user_id, 'n4d') }) it('should forward delete request', function (ctx) { @@ -1278,7 +1369,7 @@ describe('ClsiManager', function () { url.host === 'compiles.somewhere.test' && url.pathname === `/project/${ctx.project._id}/user/${ctx.user_id}` && - url.searchParams.get('compileBackendClass') === 'n4' && + url.searchParams.get('compileBackendClass') === 'n4d' && url.searchParams.get('compileGroup') === 'priority' && !url.searchParams.has('clsiserverid') ), @@ -1351,7 +1442,7 @@ describe('ClsiManager', function () { describe('when a new backend is configured', function () { beforeEach(async function (ctx) { - ctx.Settings.apis.clsi_new = { url: 'https://compiles.somewhere.test' } + ctx.Settings.apis.clsi_new.url = 'https://compiles.somewhere.test' await ctx.ClsiManager.promises.wordCount( ctx.project._id, ctx.user_id, @@ -1375,7 +1466,7 @@ describe('ClsiManager', function () { sinon.match( url => url.toString() === - `${ctx.Settings.apis.clsi_new.url}/project/${ctx.project._id}/user/${ctx.user_id}/wordcount?compileBackendClass=n4&compileGroup=priority&file=main.tex&image=mock-image-name` + `${ctx.Settings.apis.clsi_new.url}/project/${ctx.project._id}/user/${ctx.user_id}/wordcount?compileBackendClass=n4d&compileGroup=priority&file=main.tex&image=mock-image-name` ) ) })