mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01:00
[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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user