Sfoglia il codice sorgente

Merge pull request #14358 from overleaf/jpa-abort-check

[web] skip streaming clsi response when the request was aborted

GitOrigin-RevId: 8152399f18328eafd3e93143190a5eb8d1fdff1d
Jakob Ackermann 3 anni fa
parent
commit
33ac9e18f9

+ 70 - 6
services/web/app/src/Features/Compile/CompileController.js

@@ -328,7 +328,15 @@ module.exports = CompileController = {
               req.params.build_id,
               req.params.build_id,
               'output.pdf'
               'output.pdf'
             )
             )
-            CompileController.proxyToClsi(projectId, url, {}, req, res, next)
+            CompileController.proxyToClsi(
+              projectId,
+              'output-file',
+              url,
+              {},
+              req,
+              res,
+              next
+            )
           })
           })
         }
         }
       })
       })
@@ -374,7 +382,15 @@ module.exports = CompileController = {
         return
         return
       }
       }
       const url = `/project/${projectId}/output/output.pdf`
       const url = `/project/${projectId}/output/output.pdf`
-      CompileController.proxyToClsi(projectId, url, {}, req, res, next)
+      CompileController.proxyToClsi(
+        projectId,
+        'output-file',
+        url,
+        {},
+        req,
+        res,
+        next
+      )
     })
     })
   },
   },
 
 
@@ -390,7 +406,15 @@ module.exports = CompileController = {
         req.params.build_id,
         req.params.build_id,
         req.params.file
         req.params.file
       )
       )
-      CompileController.proxyToClsi(projectId, url, {}, req, res, next)
+      CompileController.proxyToClsi(
+        projectId,
+        'output-file',
+        url,
+        {},
+        req,
+        res,
+        next
+      )
     })
     })
   },
   },
 
 
@@ -411,6 +435,7 @@ module.exports = CompileController = {
     }
     }
     CompileController.proxyToClsiWithLimits(
     CompileController.proxyToClsiWithLimits(
       submissionId,
       submissionId,
+      'output-file',
       url,
       url,
       {},
       {},
       limits,
       limits,
@@ -467,6 +492,7 @@ module.exports = CompileController = {
         const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
         const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
         CompileController.proxyToClsi(
         CompileController.proxyToClsi(
           projectId,
           projectId,
+          'sync-to-pdf',
           url,
           url,
           { page, h, v, imageName },
           { page, h, v, imageName },
           req,
           req,
@@ -508,6 +534,7 @@ module.exports = CompileController = {
         const url = CompileController._getUrl(projectId, userId, 'sync/code')
         const url = CompileController._getUrl(projectId, userId, 'sync/code')
         CompileController.proxyToClsi(
         CompileController.proxyToClsi(
           projectId,
           projectId,
+          'sync-to-code',
           url,
           url,
           { file, line, column, imageName },
           { file, line, column, imageName },
           req,
           req,
@@ -518,13 +545,14 @@ module.exports = CompileController = {
     })
     })
   },
   },
 
 
-  proxyToClsi(projectId, url, qs, req, res, next) {
+  proxyToClsi(projectId, action, url, qs, req, res, next) {
     CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
     CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
       if (error) {
       if (error) {
         return next(error)
         return next(error)
       }
       }
       CompileController.proxyToClsiWithLimits(
       CompileController.proxyToClsiWithLimits(
         projectId,
         projectId,
+        action,
         url,
         url,
         qs,
         qs,
         limits,
         limits,
@@ -535,7 +563,7 @@ module.exports = CompileController = {
     })
     })
   },
   },
 
 
-  proxyToClsiWithLimits(projectId, url, qs, limits, req, res, next) {
+  proxyToClsiWithLimits(projectId, action, url, qs, limits, req, res, next) {
     _getPersistenceOptions(
     _getPersistenceOptions(
       req,
       req,
       projectId,
       projectId,
@@ -551,19 +579,43 @@ module.exports = CompileController = {
           ...persistenceOptions.qs,
           ...persistenceOptions.qs,
           ...qs,
           ...qs,
         }).toString()
         }).toString()
+        const timer = new Metrics.Timer(
+          'proxy_to_clsi',
+          1,
+          { path: action },
+          [0, 100, 1000, 2000, 5000, 10000, 15000, 20000, 30000, 45000, 60000]
+        )
+        Metrics.inc('proxy_to_clsi', 1, { path: action, status: 'start' })
         fetchStreamWithResponse(url.href, {
         fetchStreamWithResponse(url.href, {
           method: req.method,
           method: req.method,
           signal: AbortSignal.timeout(60 * 1000),
           signal: AbortSignal.timeout(60 * 1000),
           headers: persistenceOptions.headers,
           headers: persistenceOptions.headers,
         })
         })
           .then(({ stream, response }) => {
           .then(({ stream, response }) => {
+            if (req.destroyed) {
+              // The client has disconnected already, avoid trying to write into the broken connection.
+              Metrics.inc('proxy_to_clsi', 1, {
+                path: action,
+                status: 'req-aborted',
+              })
+              return
+            }
+            Metrics.inc('proxy_to_clsi', 1, {
+              path: action,
+              status: response.status,
+            })
+
             for (const key of ['Content-Length', 'Content-Type']) {
             for (const key of ['Content-Length', 'Content-Type']) {
               res.setHeader(key, response.headers.get(key))
               res.setHeader(key, response.headers.get(key))
             }
             }
             res.writeHead(response.status)
             res.writeHead(response.status)
             return pipeline(stream, res)
             return pipeline(stream, res)
           })
           })
+          .then(() => {
+            timer.done()
+          })
           .catch(err => {
           .catch(err => {
+            const duration = timer.done()
             if (!res.headersSent) {
             if (!res.headersSent) {
               if (err instanceof RequestFailedError) {
               if (err instanceof RequestFailedError) {
                 res.sendStatus(err.response.status)
                 res.sendStatus(err.response.status)
@@ -571,7 +623,19 @@ module.exports = CompileController = {
                 res.sendStatus(500)
                 res.sendStatus(500)
               }
               }
             }
             }
-            logger.warn({ err, projectId, url }, 'CLSI proxy error')
+            const reqAborted = Boolean(req.destroyed)
+            if (reqAborted) {
+              Metrics.inc('proxy_to_clsi', 1, {
+                path: action,
+                status: 'req-aborted-late',
+              })
+            } else {
+              Metrics.inc('proxy_to_clsi', 1, { path: action, status: 'error' })
+            }
+            logger.warn(
+              { err, projectId, url, action, reqAborted, duration },
+              'CLSI proxy error'
+            )
           })
           })
       }
       }
     )
     )

+ 19 - 1
services/web/test/unit/src/Compile/CompileControllerTests.js

@@ -78,7 +78,12 @@ describe('CompileController', function () {
         '@overleaf/fetch-utils': this.fetchUtils,
         '@overleaf/fetch-utils': this.fetchUtils,
         request: (this.request = sinon.stub()),
         request: (this.request = sinon.stub()),
         '../Project/ProjectGetter': (this.ProjectGetter = {}),
         '../Project/ProjectGetter': (this.ProjectGetter = {}),
-        '@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }),
+        '@overleaf/metrics': (this.Metrics = {
+          inc: sinon.stub(),
+          Timer: class {
+            done() {}
+          },
+        }),
         './CompileManager': this.CompileManager,
         './CompileManager': this.CompileManager,
         '../User/UserGetter': this.UserGetter,
         '../User/UserGetter': this.UserGetter,
         './ClsiManager': this.ClsiManager,
         './ClsiManager': this.ClsiManager,
@@ -409,6 +414,7 @@ describe('CompileController', function () {
         this.CompileController.proxyToClsi
         this.CompileController.proxyToClsi
           .calledWith(
           .calledWith(
             this.projectId,
             this.projectId,
+            'output-file',
             `/project/${this.projectId}/user/${this.user_id}/output/output.pdf`,
             `/project/${this.projectId}/user/${this.user_id}/output/output.pdf`,
             {},
             {},
             this.req,
             this.req,
@@ -432,6 +438,7 @@ describe('CompileController', function () {
         this.CompileController.proxyToClsi
         this.CompileController.proxyToClsi
           .calledWith(
           .calledWith(
             this.projectId,
             this.projectId,
+            'output-file',
             `/project/${this.projectId}/user/${this.user_id}/build/${this.buildId}/output/output.pdf`,
             `/project/${this.projectId}/user/${this.user_id}/build/${this.buildId}/output/output.pdf`,
             {},
             {},
             this.req,
             this.req,
@@ -470,6 +477,7 @@ describe('CompileController', function () {
       it('should proxy to CLSI with correct URL and default limits', function () {
       it('should proxy to CLSI with correct URL and default limits', function () {
         this.CompileController.proxyToClsiWithLimits.should.have.been.calledWith(
         this.CompileController.proxyToClsiWithLimits.should.have.been.calledWith(
           this.submission_id,
           this.submission_id,
+          'output-file',
           this.expected_url,
           this.expected_url,
           {},
           {},
           {
           {
@@ -493,6 +501,7 @@ describe('CompileController', function () {
       it('should proxy to CLSI with correct URL and specified limits', function () {
       it('should proxy to CLSI with correct URL and specified limits', function () {
         this.CompileController.proxyToClsiWithLimits.should.have.been.calledWith(
         this.CompileController.proxyToClsiWithLimits.should.have.been.calledWith(
           this.submission_id,
           this.submission_id,
+          'output-file',
           this.expected_url,
           this.expected_url,
           {},
           {},
           {
           {
@@ -526,6 +535,7 @@ describe('CompileController', function () {
     it('should proxy the request with an imageName', function () {
     it('should proxy the request with an imageName', function () {
       expect(this.CompileController.proxyToClsi).to.have.been.calledWith(
       expect(this.CompileController.proxyToClsi).to.have.been.calledWith(
         this.projectId,
         this.projectId,
+        'sync-to-code',
         `/project/${this.projectId}/user/${this.user_id}/sync/code`,
         `/project/${this.projectId}/user/${this.user_id}/sync/code`,
         { file, line, column, imageName },
         { file, line, column, imageName },
         this.req,
         this.req,
@@ -558,6 +568,7 @@ describe('CompileController', function () {
     it('should proxy the request with an imageName', function () {
     it('should proxy the request with an imageName', function () {
       expect(this.CompileController.proxyToClsi).to.have.been.calledWith(
       expect(this.CompileController.proxyToClsi).to.have.been.calledWith(
         this.projectId,
         this.projectId,
+        'sync-to-pdf',
         `/project/${this.projectId}/user/${this.user_id}/sync/pdf`,
         `/project/${this.projectId}/user/${this.user_id}/sync/pdf`,
         { page, h, v, imageName },
         { page, h, v, imageName },
         this.req,
         this.req,
@@ -590,6 +601,7 @@ describe('CompileController', function () {
             })
             })
           this.CompileController.proxyToClsi(
           this.CompileController.proxyToClsi(
             this.projectId,
             this.projectId,
+            'output-file',
             (this.url = '/test'),
             (this.url = '/test'),
             { query: 'foo' },
             { query: 'foo' },
             this.req,
             this.req,
@@ -620,6 +632,7 @@ describe('CompileController', function () {
             })
             })
           this.CompileController.proxyToClsi(
           this.CompileController.proxyToClsi(
             this.projectId,
             this.projectId,
+            'output-file',
             (this.url = '/test'),
             (this.url = '/test'),
             {},
             {},
             this.req,
             this.req,
@@ -647,6 +660,7 @@ describe('CompileController', function () {
             })
             })
           this.CompileController.proxyToClsi(
           this.CompileController.proxyToClsi(
             this.projectId,
             this.projectId,
+            'output-file',
             (this.url = '/test'),
             (this.url = '/test'),
             {},
             {},
             this.req,
             this.req,
@@ -678,6 +692,7 @@ describe('CompileController', function () {
             })
             })
           this.CompileController.proxyToClsi(
           this.CompileController.proxyToClsi(
             this.projectId,
             this.projectId,
+            'output-file',
             (this.url = '/test'),
             (this.url = '/test'),
             {},
             {},
             this.req,
             this.req,
@@ -705,6 +720,7 @@ describe('CompileController', function () {
           this.req.query = { build: 1234 }
           this.req.query = { build: 1234 }
           this.CompileController.proxyToClsi(
           this.CompileController.proxyToClsi(
             this.projectId,
             this.projectId,
+            'output-file',
             (this.url = '/test'),
             (this.url = '/test'),
             {},
             {},
             this.req,
             this.req,
@@ -765,6 +781,7 @@ describe('CompileController', function () {
       sinon.assert.calledWith(
       sinon.assert.calledWith(
         this.CompileController.proxyToClsi,
         this.CompileController.proxyToClsi,
         this.projectId,
         this.projectId,
+        'output-file',
         `/project/${this.projectId}/output/output.pdf`,
         `/project/${this.projectId}/output/output.pdf`,
         {},
         {},
         this.req,
         this.req,
@@ -774,6 +791,7 @@ describe('CompileController', function () {
       this.CompileController.proxyToClsi
       this.CompileController.proxyToClsi
         .calledWith(
         .calledWith(
           this.projectId,
           this.projectId,
+          'output-file',
           `/project/${this.projectId}/output/output.pdf`,
           `/project/${this.projectId}/output/output.pdf`,
           {},
           {},
           this.req,
           this.req,