From 8ed650f57aadac21ff26b2b01d6817d2077d2061 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Thu, 24 Apr 2025 16:29:00 +0100 Subject: [PATCH] [web] make clsi-cache a premium feature (#25075) * [web] stopOnFirstError=true does not conflict with =false locally Allow stopOnFirstError to be enabled in the compile from cache and disabled locally. Compiles that passed with stopOnFirstError=true will also pass with stopOnFirstError=false. The inverse does not hold, and we need to recompile. * [web] record event when using compile from cache * [web] record event when falling back to clsi-cache * [web] make clsi-cache a premium feature * [k8s] clsi-cache: increase disk size for beta rollout NOTE: As this is a premium feature and paid servers run in zones c+d, we do not need to scale up clsi-cache in zone b for now. * [web] enable full sampling of compile-result-backend events * [web] fix frontend tests * [web] be more verbose when determining access to clsi-cache feature GitOrigin-RevId: 6fd663e16085187876eb225f7f33eeeaf69d2b2a --- .../src/Features/Compile/ClsiCacheManager.js | 5 +++++ .../app/src/Features/Compile/ClsiManager.js | 9 ++++++-- .../src/Features/Compile/CompileController.js | 9 +------- .../src/Features/Project/ProjectController.js | 9 +++++--- .../web/app/views/project/editor/_meta.pug | 1 + .../pdf-preview/util/pdf-caching-flags.js | 6 +++++- .../pdf-preview/util/pdf-caching-transport.js | 21 +++++++++++++++++-- .../features/pdf-preview/util/pdf-caching.js | 6 ++++++ .../shared/context/local-compile-context.tsx | 12 ++++++++--- services/web/frontend/js/utils/meta.ts | 1 + .../pdf-preview/pdf-preview.spec.tsx | 1 + 11 files changed, 61 insertions(+), 19 deletions(-) diff --git a/services/web/app/src/Features/Compile/ClsiCacheManager.js b/services/web/app/src/Features/Compile/ClsiCacheManager.js index 1090b9f2d0..3fe4b987c5 100644 --- a/services/web/app/src/Features/Compile/ClsiCacheManager.js +++ b/services/web/app/src/Features/Compile/ClsiCacheManager.js @@ -3,6 +3,7 @@ const ClsiCacheHandler = require('./ClsiCacheHandler') const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') const ProjectGetter = require('../Project/ProjectGetter') const SplitTestHandler = require('../SplitTests/SplitTestHandler') +const UserGetter = require('../User/UserGetter') /** * Get the most recent build and metadata @@ -66,6 +67,10 @@ async function prepareClsiCache( 'copy-clsi-cache' ) if (variant !== 'enabled') return + + const features = await UserGetter.promises.getUserFeatures(userId) + if (features.compileGroup !== 'priority') return + const signal = AbortSignal.timeout(5_000) let lastUpdated let zone = 'b' // populate template data on zone b diff --git a/services/web/app/src/Features/Compile/ClsiManager.js b/services/web/app/src/Features/Compile/ClsiManager.js index 021f102324..2e32aa9622 100644 --- a/services/web/app/src/Features/Compile/ClsiManager.js +++ b/services/web/app/src/Features/Compile/ClsiManager.js @@ -784,8 +784,13 @@ function _finaliseRequest(projectId, options, project, docs, files) { syncType: options.syncType, syncState: options.syncState, compileGroup: options.compileGroup, - compileFromClsiCache: options.compileFromClsiCache, - populateClsiCache: options.populateClsiCache, + // Overleaf alpha/staff users get compileGroup=alpha (via getProjectCompileLimits in CompileManager), enroll them into the premium rollout of clsi-cache. + compileFromClsiCache: + ['alpha', 'priority'].includes(options.compileGroup) && + options.compileFromClsiCache, + populateClsiCache: + ['alpha', 'priority'].includes(options.compileGroup) && + options.populateClsiCache, enablePdfCaching: (Settings.enablePdfCaching && options.enablePdfCaching) || false, pdfCachingMinChunkSize: options.pdfCachingMinChunkSize, diff --git a/services/web/app/src/Features/Compile/CompileController.js b/services/web/app/src/Features/Compile/CompileController.js index c981e93dcb..5d2bbcda3e 100644 --- a/services/web/app/src/Features/Compile/CompileController.js +++ b/services/web/app/src/Features/Compile/CompileController.js @@ -203,14 +203,7 @@ module.exports = CompileController = { pdfDownloadDomain += outputUrlPrefix } - if ( - limits && - SplitTestHandler.getPercentile( - AnalyticsManager.getIdsFromSession(req.session).analyticsId, - 'compile-result-backend', - 'release' - ) === 1 - ) { + if (limits) { // For a compile request to be sent to clsi we need limits. // If we get here without having the limits object populated, it is // a reasonable assumption to make that nothing was compiled. diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 5a01df534f..791df18390 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -558,10 +558,11 @@ const _ProjectController = { .catch(err => logger.error({ err }, 'failed to update split test info in session') ) + + const ownerFeatures = await UserGetter.promises.getUserFeatures( + project.owner_ref + ) if (userId) { - const ownerFeatures = await UserGetter.promises.getUserFeatures( - project.owner_ref - ) const planLimit = ownerFeatures?.collaborators || 0 const namedEditors = project.collaberator_refs?.length || 0 const pendingEditors = project.pendingEditor_refs?.length || 0 @@ -798,6 +799,8 @@ const _ProjectController = { bodyClasses: ['editor'], project_id: project._id, projectName: project.name, + projectOwnerHasPremiumOnPageLoad: + ownerFeatures?.compileGroup === 'priority', user: { id: userId, email: user.email, diff --git a/services/web/app/views/project/editor/_meta.pug b/services/web/app/views/project/editor/_meta.pug index 61b7dd7d67..ab31cef89e 100644 --- a/services/web/app/views/project/editor/_meta.pug +++ b/services/web/app/views/project/editor/_meta.pug @@ -1,5 +1,6 @@ meta(name="ol-project_id" content=project_id) meta(name="ol-projectName" content=projectName) +meta(name="ol-projectOwnerHasPremiumOnPageLoad" data-type="boolean" content=projectOwnerHasPremiumOnPageLoad) meta(name="ol-userSettings" data-type="json" content=userSettings) meta(name="ol-user" data-type="json" content=user) meta(name="ol-labsExperiments" data-type="json" content=labsExperiments) diff --git a/services/web/frontend/js/features/pdf-preview/util/pdf-caching-flags.js b/services/web/frontend/js/features/pdf-preview/util/pdf-caching-flags.js index d0a5a57ae9..dd7ed2c1b5 100644 --- a/services/web/frontend/js/features/pdf-preview/util/pdf-caching-flags.js +++ b/services/web/frontend/js/features/pdf-preview/util/pdf-caching-flags.js @@ -26,4 +26,8 @@ export const prefetchingEnabled = isFlagEnabled('pdf-caching-prefetching') export const prefetchLargeEnabled = isFlagEnabled('pdf-caching-prefetch-large') export const enablePdfCaching = isFlagEnabled('pdf-caching-mode') export const trackPdfDownloadEnabled = isFlagEnabled('track-pdf-download') -export const useClsiCache = isFlagEnabled('fall-back-to-clsi-cache') +export const projectOwnerHasPremiumOnPageLoad = getMeta( + 'ol-projectOwnerHasPremiumOnPageLoad' +) +export const fallBackToClsiCache = + projectOwnerHasPremiumOnPageLoad && isFlagEnabled('fall-back-to-clsi-cache') diff --git a/services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js b/services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js index 166a2b0d94..f5c95e531b 100644 --- a/services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js +++ b/services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js @@ -8,11 +8,13 @@ import { prefetchingEnabled, prefetchLargeEnabled, trackPdfDownloadEnabled, - useClsiCache, + fallBackToClsiCache, } from './pdf-caching-flags' import { isNetworkError } from '@/utils/is-network-error' import { debugConsole } from '@/utils/debugging' import { PDFJS } from './pdf-js' +import { sendMB } from '@/infrastructure/event-tracking' +import getMeta from '@/utils/meta' // 30 seconds: The shutdown grace period of a clsi pre-emp instance. const STALE_OUTPUT_REQUEST_THRESHOLD_MS = 30 * 1000 @@ -22,6 +24,7 @@ export function generatePdfCachingTransportFactory() { if (!enablePdfCaching && !trackPdfDownloadEnabled) { return () => undefined } + const projectId = getMeta('ol-project_id') const usageScore = new Map() const cachedUrls = new Map() const metrics = Object.assign(getPdfCachingMetrics(), { @@ -59,6 +62,7 @@ export function generatePdfCachingTransportFactory() { this.handleFetchError = handleFetchError this.abortController = abortController this.startTime = performance.now() + this.sentEventFallbackToClsiCache = false const params = new URL(url).searchParams // drop no needed params @@ -72,6 +76,7 @@ export function generatePdfCachingTransportFactory() { } requestDataRange(start, end) { + let recordFallbackToClsiCache = false const abortSignal = this.abortController.signal const getDebugInfo = () => ({ // Sentry does not serialize objects in twice nested objects. @@ -115,7 +120,7 @@ export function generatePdfCachingTransportFactory() { ) } const canTryFromCache = err => { - if (!useClsiCache) return false + if (!fallBackToClsiCache) return false if (!is404(err)) return false return !usesCache(OError.getFullInfo(err).url) } @@ -146,6 +151,7 @@ export function generatePdfCachingTransportFactory() { this.pdfFile.ranges = this.pdfFile.ranges.filter(r => cachedUrls.has(r.hash) ) + recordFallbackToClsiCache = true return blob }) .catch(err => { @@ -177,6 +183,9 @@ export function generatePdfCachingTransportFactory() { abortSignal, canTryFromCache, fallbackToCacheURL: getOutputPDFURLFromCache(), + recordFallbackToClsiCache: () => { + recordFallbackToClsiCache = true + }, }) .catch(err => { if (abortSignal.aborted) return @@ -228,6 +237,14 @@ export function generatePdfCachingTransportFactory() { }) .then(blob => { if (abortSignal.aborted) return + if (recordFallbackToClsiCache && !this.sentEventFallbackToClsiCache) { + // Record once per PDF preview. Technically we should record once per 90min (output cache age), but keep it simple for now. + this.sentEventFallbackToClsiCache = true + sendMB('fallback-to-clsi-cache', { + projectId, + ageMS: Math.ceil(performance.now() - this.startTime), + }) + } this.onDataRange(start, blob) }) .catch(err => { diff --git a/services/web/frontend/js/features/pdf-preview/util/pdf-caching.js b/services/web/frontend/js/features/pdf-preview/util/pdf-caching.js index a13c74aa2a..7fd17c87bf 100644 --- a/services/web/frontend/js/features/pdf-preview/util/pdf-caching.js +++ b/services/web/frontend/js/features/pdf-preview/util/pdf-caching.js @@ -593,6 +593,7 @@ function skipPrefetched(chunks, prefetched, start, end) { * @param {() => boolean} canTryFromCache * @param {string} fallbackToCacheURL * @param {Object} file + * @param {() => void} recordFallbackToClsiCache */ async function fetchChunk({ chunk, @@ -604,6 +605,7 @@ async function fetchChunk({ canTryFromCache, fallbackToCacheURL, file, + recordFallbackToClsiCache, }) { const estimatedSize = Array.isArray(chunk) ? estimateSizeOfMultipartResponse(chunk) @@ -667,6 +669,7 @@ async function fetchChunk({ try { response = await fetchWithBrowserCacheFallback(url, init) checkChunkResponse(response, estimatedSize, init) + recordFallbackToClsiCache() } catch (err2) { throw err1 } @@ -824,6 +827,7 @@ class Timer { * @param {AbortSignal} abortSignal * @param {() => boolean} canTryFromCache * @param {string} fallbackToCacheURL + * @param {() => void} recordFallbackToClsiCache */ export async function fetchRange({ url, @@ -841,6 +845,7 @@ export async function fetchRange({ abortSignal, canTryFromCache, fallbackToCacheURL, + recordFallbackToClsiCache, }) { const timer = new Timer() timer.startBlockingCompute() @@ -985,6 +990,7 @@ export async function fetchRange({ canTryFromCache, fallbackToCacheURL, file, + recordFallbackToClsiCache, }) timer.startBlockingCompute() const boundary = getMultipartBoundary(response, chunk) diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx index 7b237a441d..59c0fc8886 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -49,6 +49,7 @@ import { PdfFileDataList } from '@/features/pdf-preview/util/types' import { isSplitTestEnabled } from '@/utils/splitTestUtils' import { captureException } from '@/infrastructure/error-reporter' import OError from '@overleaf/o-error' +import getMeta from '@/utils/meta' type PdfFile = Record @@ -200,7 +201,8 @@ export const LocalCompileProvider: FC = ({ children }) => { const [compiledOnce, setCompiledOnce] = useState(false) // fetch initial compile response from cache const [initialCompileFromCache, setInitialCompileFromCache] = useState( - isSplitTestEnabled('initial-compile-from-clsi-cache') && + getMeta('ol-projectOwnerHasPremiumOnPageLoad') && + isSplitTestEnabled('initial-compile-from-clsi-cache') && // Avoid fetching the initial compile from cache in PDF detach tab role !== 'detached' ) @@ -385,8 +387,10 @@ export const LocalCompileProvider: FC = ({ children }) => { rootDocOverride === dataFromCache.rootDocId && dataFromCache.options.imageName === imageName && dataFromCache.options.compiler === compilerName && - dataFromCache.options.stopOnFirstError === stopOnFirstError && - dataFromCache.options.draft === draft + dataFromCache.options.draft === draft && + // Allow stopOnFirstError to be enabled in the compile from cache and disabled locally. + // Compiles that passed with stopOnFirstError=true will also pass with stopOnFirstError=false. The inverse does not hold, and we need to recompile. + !!dataFromCache.options.stopOnFirstError >= stopOnFirstError } catch (err) { captureException( OError.tag(err as unknown as Error, 'validate compile options', { @@ -396,6 +400,7 @@ export const LocalCompileProvider: FC = ({ children }) => { } if (settingsUpToDate) { + sendMB('compile-from-cache', { projectId }) setData(dataFromCache) setCompiledOnce(true) } @@ -403,6 +408,7 @@ export const LocalCompileProvider: FC = ({ children }) => { setInitialCompileFromCache(false) setPendingInitialCompileFromCache(false) }, [ + projectId, dataFromCache, joinedOnce, currentDocument, diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index 15ff6db6a7..6f2fe864f9 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -183,6 +183,7 @@ export interface Meta { 'ol-project': any // TODO 'ol-projectHistoryBlobsEnabled': boolean 'ol-projectName': string + 'ol-projectOwnerHasPremiumOnPageLoad': boolean 'ol-projectSyncSuccessMessage': string 'ol-projectTags': Tag[] 'ol-project_id': string diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx index a1cbed1fe3..db637c1f77 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx @@ -49,6 +49,7 @@ describe('', function () { window.metaAttributesCache.set('ol-splitTestVariants', { 'initial-compile-from-clsi-cache': 'enabled', }) + window.metaAttributesCache.set('ol-projectOwnerHasPremiumOnPageLoad', true) cy.interceptEvents() })