Просмотр исходного кода

[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
Jakob Ackermann 1 год назад
Родитель
Сommit
8ed650f57a

+ 5 - 0
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

+ 7 - 2
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,

+ 1 - 8
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.

+ 6 - 3
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,

+ 1 - 0
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)

+ 5 - 1
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')

+ 19 - 2
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 => {

+ 6 - 0
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)

+ 9 - 3
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<string, any>
 
@@ -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,

+ 1 - 0
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

+ 1 - 0
services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx

@@ -49,6 +49,7 @@ describe('<PdfPreview/>', function () {
     window.metaAttributesCache.set('ol-splitTestVariants', {
       'initial-compile-from-clsi-cache': 'enabled',
     })
+    window.metaAttributesCache.set('ol-projectOwnerHasPremiumOnPageLoad', true)
     cy.interceptEvents()
   })