mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #15414 from overleaf/jpa-server-pro-4-1-4
[server-pro] prepare hotfix release 4.1.4 GitOrigin-RevId: 2301f366f9b7f170a5801c5b74d10b9b7757973e
This commit is contained in:
parent
b8f4ed0e2c
commit
b7c4f3333e
3 changed files with 252 additions and 0 deletions
9
server-ce/hotfix/4.1.4/Dockerfile
Normal file
9
server-ce/hotfix/4.1.4/Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
FROM sharelatex/sharelatex:4.1.3
|
||||||
|
|
||||||
|
# Patch: Make history-v1 http request timeout configurable
|
||||||
|
COPY pr_15409.patch /
|
||||||
|
RUN cd / && patch -p0 < pr_15409.patch
|
||||||
|
|
||||||
|
# Patch: Add verbose logging for I/O in history-v1
|
||||||
|
COPY pr_15410.patch .
|
||||||
|
RUN patch -p0 < pr_15410.patch
|
90
server-ce/hotfix/4.1.4/pr_15409.patch
Normal file
90
server-ce/hotfix/4.1.4/pr_15409.patch
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
--- overleaf/services/history-v1/config/custom-environment-variables.json
|
||||||
|
+++ overleaf/services/history-v1/config/custom-environment-variables.json
|
||||||
|
@@ -43,5 +43,6 @@
|
||||||
|
},
|
||||||
|
"clusterWorkers": "CLUSTER_WORKERS",
|
||||||
|
"maxFileUploadSize": "MAX_FILE_UPLOAD_SIZE",
|
||||||
|
- "httpsOnly": "HTTPS_ONLY"
|
||||||
|
+ "httpsOnly": "HTTPS_ONLY",
|
||||||
|
+ "httpRequestTimeout": "SHARELATEX_HISTORY_V1_HTTP_REQUEST_TIMEOUT"
|
||||||
|
}
|
||||||
|
--- etc/sharelatex/settings.js
|
||||||
|
+++ etc/sharelatex/settings.js
|
||||||
|
@@ -261,6 +261,10 @@ const settings = {
|
||||||
|
url: process.env.V1_HISTORY_URL || 'http://localhost:3100/api',
|
||||||
|
user: 'staging',
|
||||||
|
pass: process.env.STAGING_PASSWORD,
|
||||||
|
+ requestTimeout: parseInt(
|
||||||
|
+ process.env.SHARELATEX_HISTORY_V1_HTTP_REQUEST_TIMEOUT || '300000', // default is 5min
|
||||||
|
+ 10
|
||||||
|
+ ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
references: {},
|
||||||
|
diff --git a/services/history-v1/app.js b/services/history-v1/app.js
|
||||||
|
index 6b3a2ba8f89..2ad490fb6b6 100644
|
||||||
|
--- overleaf/services/history-v1/app.js
|
||||||
|
+++ overleaf/services/history-v1/app.js
|
||||||
|
@@ -5,6 +5,7 @@
|
||||||
|
// Metrics must be initialized before importing anything else
|
||||||
|
require('@overleaf/metrics/initialize')
|
||||||
|
|
||||||
|
+const config = require('config')
|
||||||
|
const Events = require('events')
|
||||||
|
const BPromise = require('bluebird')
|
||||||
|
const express = require('express')
|
||||||
|
@@ -47,9 +48,9 @@ app.use(cors())
|
||||||
|
security.setupSSL(app)
|
||||||
|
security.setupBasicHttpAuthForSwaggerDocs(app)
|
||||||
|
|
||||||
|
+const HTTP_REQUEST_TIMEOUT = parseInt(config.get('httpRequestTimeout'), 10)
|
||||||
|
app.use(function (req, res, next) {
|
||||||
|
- // use a 5 minute timeout on all responses
|
||||||
|
- res.setTimeout(5 * 60 * 1000)
|
||||||
|
+ res.setTimeout(HTTP_REQUEST_TIMEOUT)
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
--- overleaf/services/history-v1/config/default.json
|
||||||
|
+++ overleaf/services/history-v1/config/default.json
|
||||||
|
@@ -25,5 +25,6 @@
|
||||||
|
"maxFileUploadSize": "52428800",
|
||||||
|
"databasePoolMin": "2",
|
||||||
|
"databasePoolMax": "10",
|
||||||
|
- "httpsOnly": "false"
|
||||||
|
+ "httpsOnly": "false",
|
||||||
|
+ "httpRequestTimeout": "300000"
|
||||||
|
}
|
||||||
|
--- overleaf/services/project-history/app/js/HistoryStoreManager.js
|
||||||
|
+++ overleaf/services/project-history/app/js/HistoryStoreManager.js
|
||||||
|
@@ -17,7 +17,7 @@ import * as Errors from './Errors.js'
|
||||||
|
import * as LocalFileWriter from './LocalFileWriter.js'
|
||||||
|
import * as HashManager from './HashManager.js'
|
||||||
|
|
||||||
|
-const HTTP_REQUEST_TIMEOUT = 300 * 1000 // 5 minutes
|
||||||
|
+const HTTP_REQUEST_TIMEOUT = Settings.apis.history_v1.requestTimeout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for functions that need to be mocked in tests
|
||||||
|
--- overleaf/services/project-history/config/settings.defaults.cjs
|
||||||
|
+++ overleaf/services/project-history/config/settings.defaults.cjs
|
||||||
|
@@ -20,6 +20,9 @@ module.exports = {
|
||||||
|
filestore: {
|
||||||
|
url: `http://${process.env.FILESTORE_HOST || 'localhost'}:3009`,
|
||||||
|
},
|
||||||
|
+ history_v1: {
|
||||||
|
+ requestTimeout: parseInt(process.env.V1_REQUEST_TIMEOUT || '300000', 10),
|
||||||
|
+ },
|
||||||
|
web: {
|
||||||
|
url: `http://${
|
||||||
|
process.env.WEB_API_HOST || process.env.WEB_HOST || 'localhost'
|
||||||
|
--- overleaf/services/project-history/test/unit/js/HistoryStoreManager/HistoryStoreManagerTests.js
|
||||||
|
+++ overleaf/services/project-history/test/unit/js/HistoryStoreManager/HistoryStoreManagerTests.js
|
||||||
|
@@ -23,6 +23,7 @@ describe('HistoryStoreManager', function () {
|
||||||
|
filestore: {
|
||||||
|
url: 'http://filestore.sharelatex.production',
|
||||||
|
},
|
||||||
|
+ history_v1: { requestTimeout: 123 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
this.latestChunkRequestArgs = sinon.match({
|
153
server-ce/hotfix/4.1.4/pr_15410.patch
Normal file
153
server-ce/hotfix/4.1.4/pr_15410.patch
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
--- services/history-v1/api/controllers/projects.js
|
||||||
|
+++ services/history-v1/api/controllers/projects.js
|
||||||
|
@@ -194,18 +194,23 @@ async function getProjectBlob(req, res, next) {
|
||||||
|
const hash = req.swagger.params.hash.value
|
||||||
|
|
||||||
|
const blobStore = new BlobStore(projectId)
|
||||||
|
- let stream
|
||||||
|
+ logger.debug({ projectId, hash }, 'getProjectBlob started')
|
||||||
|
try {
|
||||||
|
- stream = await blobStore.getStream(hash)
|
||||||
|
- } catch (err) {
|
||||||
|
- if (err instanceof Blob.NotFoundError) {
|
||||||
|
- return render.notFound(res)
|
||||||
|
- } else {
|
||||||
|
- throw err
|
||||||
|
+ let stream
|
||||||
|
+ try {
|
||||||
|
+ stream = await blobStore.getStream(hash)
|
||||||
|
+ } catch (err) {
|
||||||
|
+ if (err instanceof Blob.NotFoundError) {
|
||||||
|
+ return render.notFound(res)
|
||||||
|
+ } else {
|
||||||
|
+ throw err
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
+ res.set('Content-Type', 'application/octet-stream')
|
||||||
|
+ await pipeline(stream, res)
|
||||||
|
+ } finally {
|
||||||
|
+ logger.debug({ projectId, hash }, 'getProjectBlob finished')
|
||||||
|
}
|
||||||
|
- res.set('Content-Type', 'application/octet-stream')
|
||||||
|
- await pipeline(stream, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSnapshotAtVersion(projectId, version) {
|
||||||
|
--- services/history-v1/storage/lib/blob_store/index.js
|
||||||
|
+++ services/history-v1/storage/lib/blob_store/index.js
|
||||||
|
@@ -20,6 +20,7 @@ const projectKey = require('../project_key')
|
||||||
|
const streams = require('../streams')
|
||||||
|
const postgresBackend = require('./postgres')
|
||||||
|
const mongoBackend = require('./mongo')
|
||||||
|
+const logger = require('@overleaf/logger')
|
||||||
|
|
||||||
|
const GLOBAL_BLOBS = new Map()
|
||||||
|
|
||||||
|
@@ -34,9 +35,14 @@ function makeProjectKey(projectId, hash) {
|
||||||
|
async function uploadBlob(projectId, blob, stream) {
|
||||||
|
const bucket = config.get('blobStore.projectBucket')
|
||||||
|
const key = makeProjectKey(projectId, blob.getHash())
|
||||||
|
- await persistor.sendStream(bucket, key, stream, {
|
||||||
|
- contentType: 'application/octet-stream',
|
||||||
|
- })
|
||||||
|
+ logger.debug({ projectId, blob }, 'uploadBlob started')
|
||||||
|
+ try {
|
||||||
|
+ await persistor.sendStream(bucket, key, stream, {
|
||||||
|
+ contentType: 'application/octet-stream',
|
||||||
|
+ })
|
||||||
|
+ } finally {
|
||||||
|
+ logger.debug({ projectId, blob }, 'uploadBlob finished')
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlobLocation(projectId, hash) {
|
||||||
|
@@ -109,7 +115,12 @@ async function getStringLengthOfFile(byteLength, pathname) {
|
||||||
|
async function deleteBlobsInBucket(projectId) {
|
||||||
|
const bucket = config.get('blobStore.projectBucket')
|
||||||
|
const prefix = `${projectKey.format(projectId)}/`
|
||||||
|
- await persistor.deleteDirectory(bucket, prefix)
|
||||||
|
+ logger.debug({ projectId }, 'deleteBlobsInBucket started')
|
||||||
|
+ try {
|
||||||
|
+ await persistor.deleteDirectory(bucket, prefix)
|
||||||
|
+ } finally {
|
||||||
|
+ logger.debug({ projectId }, 'deleteBlobsInBucket finished')
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGlobalBlobs() {
|
||||||
|
@@ -202,9 +213,15 @@ class BlobStore {
|
||||||
|
async getString(hash) {
|
||||||
|
assert.blobHash(hash, 'bad hash')
|
||||||
|
|
||||||
|
- const stream = await this.getStream(hash)
|
||||||
|
- const buffer = await streams.readStreamToBuffer(stream)
|
||||||
|
- return buffer.toString()
|
||||||
|
+ const projectId = this.projectId
|
||||||
|
+ logger.debug({ projectId, hash }, 'getString started')
|
||||||
|
+ try {
|
||||||
|
+ const stream = await this.getStream(hash)
|
||||||
|
+ const buffer = await streams.readStreamToBuffer(stream)
|
||||||
|
+ return buffer.toString()
|
||||||
|
+ } finally {
|
||||||
|
+ logger.debug({ projectId, hash }, 'getString finished')
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
--- services/history-v1/storage/lib/history_store.js
|
||||||
|
+++ services/history-v1/storage/lib/history_store.js
|
||||||
|
@@ -8,6 +8,7 @@ const path = require('path')
|
||||||
|
|
||||||
|
const OError = require('@overleaf/o-error')
|
||||||
|
const objectPersistor = require('@overleaf/object-persistor')
|
||||||
|
+const logger = require('@overleaf/logger')
|
||||||
|
|
||||||
|
const assert = require('./assert')
|
||||||
|
const persistor = require('./persistor')
|
||||||
|
@@ -70,6 +71,7 @@ HistoryStore.prototype.loadRaw = function historyStoreLoadRaw(
|
||||||
|
|
||||||
|
const key = getKey(projectId, chunkId)
|
||||||
|
|
||||||
|
+ logger.debug({ projectId, chunkId }, 'loadRaw started')
|
||||||
|
return BPromise.resolve()
|
||||||
|
.then(() => persistor.getObjectStream(BUCKET, key))
|
||||||
|
.then(streams.gunzipStreamToBuffer)
|
||||||
|
@@ -80,6 +82,7 @@ HistoryStore.prototype.loadRaw = function historyStoreLoadRaw(
|
||||||
|
}
|
||||||
|
throw new HistoryStore.LoadError(projectId, chunkId).withCause(err)
|
||||||
|
})
|
||||||
|
+ .finally(() => logger.debug({ projectId, chunkId }, 'loadRaw finished'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@@ -102,6 +105,7 @@ HistoryStore.prototype.storeRaw = function historyStoreStoreRaw(
|
||||||
|
const key = getKey(projectId, chunkId)
|
||||||
|
const stream = streams.gzipStringToStream(JSON.stringify(rawHistory))
|
||||||
|
|
||||||
|
+ logger.debug({ projectId, chunkId }, 'storeRaw started')
|
||||||
|
return BPromise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
persistor.sendStream(BUCKET, key, stream, {
|
||||||
|
@@ -112,6 +116,7 @@ HistoryStore.prototype.storeRaw = function historyStoreStoreRaw(
|
||||||
|
.catch(err => {
|
||||||
|
throw new HistoryStore.StoreError(projectId, chunkId).withCause(err)
|
||||||
|
})
|
||||||
|
+ .finally(() => logger.debug({ projectId, chunkId }, 'storeRaw finished'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@@ -121,12 +126,13 @@ HistoryStore.prototype.storeRaw = function historyStoreStoreRaw(
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
HistoryStore.prototype.deleteChunks = function historyDeleteChunks(chunks) {
|
||||||
|
+ logger.debug({ chunks }, 'deleteChunks started')
|
||||||
|
return BPromise.all(
|
||||||
|
chunks.map(chunk => {
|
||||||
|
const key = getKey(chunk.projectId, chunk.chunkId)
|
||||||
|
return persistor.deleteObject(BUCKET, key)
|
||||||
|
})
|
||||||
|
- )
|
||||||
|
+ ).finally(() => logger.debug({ chunks }, 'deleteChunks finished'))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new HistoryStore()
|
Loading…
Reference in a new issue