mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-27 19:11:56 +02:00
* [web] increase timeout for downloads from clsi * [web] detach request timeout from streaming timeout * [web] align timeout for clsi-cache downloads with clsi downloads * [web] destroy stream when bailing out early from aborted requests GitOrigin-RevId: cc4cad9d684214b2eee0f5fc43513e430ceb0977
206 lines
5.1 KiB
JavaScript
206 lines
5.1 KiB
JavaScript
import { NotFoundError, ResourceGoneError } from '../Errors/Errors.js'
|
|
import {
|
|
fetchStreamWithResponse,
|
|
RequestFailedError,
|
|
} from '@overleaf/fetch-utils'
|
|
import Path from 'node:path'
|
|
import { pipeline } from 'node:stream/promises'
|
|
import logger from '@overleaf/logger'
|
|
import ClsiCacheManager from './ClsiCacheManager.mjs'
|
|
import CompileController from './CompileController.mjs'
|
|
import { expressify } from '@overleaf/promise-utils'
|
|
import ClsiCacheHandler from './ClsiCacheHandler.mjs'
|
|
import ProjectGetter from '../Project/ProjectGetter.mjs'
|
|
import { MeteredStream } from '@overleaf/stream-utils'
|
|
import Metrics from '@overleaf/metrics'
|
|
|
|
/**
|
|
* Download a file from a specific build on the clsi-cache.
|
|
*
|
|
* @param req
|
|
* @param res
|
|
* @return {Promise<*>}
|
|
*/
|
|
async function downloadFromCache(req, res) {
|
|
const { Project_id: projectId, buildId, filename } = req.params
|
|
return await _downloadFromCacheWithParams(
|
|
req,
|
|
res,
|
|
projectId,
|
|
buildId,
|
|
filename
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Download a file from a specific build on the clsi-cache.
|
|
*
|
|
* @param req
|
|
* @param res
|
|
* @param projectId
|
|
* @param buildId
|
|
* @param filename
|
|
* @param projectId
|
|
* @param buildId
|
|
* @param filename
|
|
* @return {Promise<*>}
|
|
*/
|
|
async function _downloadFromCacheWithParams(
|
|
req,
|
|
res,
|
|
projectId,
|
|
buildId,
|
|
filename
|
|
) {
|
|
const userId = CompileController._getUserIdForCompile(req)
|
|
const ac = new AbortController()
|
|
let timer = setTimeout(() => ac.abort(), 10_000)
|
|
let location, projectName
|
|
try {
|
|
;[{ location }, { name: projectName }] = await Promise.all([
|
|
ClsiCacheHandler.getOutputFile(
|
|
projectId,
|
|
userId,
|
|
buildId,
|
|
filename,
|
|
ac.signal
|
|
),
|
|
ProjectGetter.promises.getProject(projectId, { name: 1 }),
|
|
])
|
|
} catch (err) {
|
|
clearTimeout(timer)
|
|
if (err instanceof NotFoundError) {
|
|
// res.sendStatus() sends a description of the status as body.
|
|
// Using res.status().end() avoids sending that fake body.
|
|
return res.status(404).end()
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
|
|
let stream, response
|
|
try {
|
|
;({ stream, response } = await fetchStreamWithResponse(location, {
|
|
signal: ac.signal,
|
|
}))
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
if (req.destroyed) {
|
|
// The client has disconnected already, avoid trying to write into the broken connection.
|
|
stream.destroy(new Error('user aborted the request'))
|
|
return
|
|
}
|
|
|
|
for (const key of ['Content-Length', 'Content-Type']) {
|
|
if (response.headers.has(key)) res.setHeader(key, response.headers.get(key))
|
|
}
|
|
const ext = Path.extname(filename)
|
|
res.attachment(
|
|
ext === '.pdf'
|
|
? `${CompileController._getSafeProjectName({ name: projectName })}.pdf`
|
|
: filename
|
|
)
|
|
// Downloads can take a while on a slow connection, increase timeouts to 10min
|
|
const TEN_MINUTES_IN_MS = 10 * 60 * 1000
|
|
res.setTimeout(TEN_MINUTES_IN_MS)
|
|
timer = setTimeout(() => ac.abort(), TEN_MINUTES_IN_MS)
|
|
try {
|
|
res.writeHead(response.status)
|
|
await pipeline(
|
|
stream,
|
|
new MeteredStream(Metrics, 'clsi_cache_egress', {
|
|
path: ClsiCacheHandler.getEgressLabel(filename),
|
|
}),
|
|
res
|
|
)
|
|
} catch (err) {
|
|
const reqAborted = Boolean(req.destroyed)
|
|
const streamingStarted = Boolean(res.headersSent)
|
|
if (!streamingStarted) {
|
|
if (err instanceof RequestFailedError) {
|
|
res.sendStatus(err.response.status)
|
|
} else {
|
|
res.sendStatus(500)
|
|
}
|
|
}
|
|
if (
|
|
streamingStarted &&
|
|
reqAborted &&
|
|
(err.code === 'ERR_STREAM_PREMATURE_CLOSE' ||
|
|
err.code === 'ERR_STREAM_UNABLE_TO_PIPE')
|
|
) {
|
|
// Ignore noisy spurious error
|
|
return
|
|
}
|
|
logger.warn(
|
|
{
|
|
err,
|
|
projectId,
|
|
location,
|
|
filename,
|
|
reqAborted,
|
|
streamingStarted,
|
|
},
|
|
'CLSI-cache proxy error'
|
|
)
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare a compile response from the clsi-cache.
|
|
*
|
|
* @param req
|
|
* @param res
|
|
* @return {Promise<void>}
|
|
*/
|
|
async function getLatestBuildFromCache(req, res) {
|
|
const { Project_id: projectId } = req.params
|
|
const userId = CompileController._getUserIdForCompile(req)
|
|
try {
|
|
const {
|
|
zone,
|
|
outputFiles,
|
|
compileGroup,
|
|
clsiServerId,
|
|
clsiCacheShard,
|
|
options,
|
|
stats,
|
|
timings,
|
|
} = await ClsiCacheManager.getLatestCompileResult(projectId, userId)
|
|
|
|
let { pdfCachingMinChunkSize, pdfDownloadDomain } =
|
|
await CompileController._getSplitTestOptions(req, res)
|
|
pdfDownloadDomain += `/zone/${zone}`
|
|
res.json({
|
|
fromCache: true,
|
|
status: 'success',
|
|
outputFiles,
|
|
compileGroup,
|
|
clsiServerId,
|
|
clsiCacheShard,
|
|
pdfDownloadDomain,
|
|
pdfCachingMinChunkSize,
|
|
options,
|
|
stats,
|
|
timings,
|
|
})
|
|
} catch (err) {
|
|
if (err instanceof NotFoundError) {
|
|
res.sendStatus(404)
|
|
} else if (err instanceof ResourceGoneError) {
|
|
res.sendStatus(410)
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
export default {
|
|
_downloadFromCacheWithParams,
|
|
downloadFromCache: expressify(downloadFromCache),
|
|
getLatestBuildFromCache: expressify(getLatestBuildFromCache),
|
|
}
|