2020-06-23 13:29:44 -04:00
|
|
|
const request = require('request')
|
2024-01-08 08:55:30 -05:00
|
|
|
const _ = require('lodash')
|
2020-08-20 09:05:50 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
2021-12-14 08:00:35 -05:00
|
|
|
const logger = require('@overleaf/logger')
|
2021-07-12 12:47:18 -04:00
|
|
|
const settings = require('@overleaf/settings')
|
2020-11-25 06:57:22 -05:00
|
|
|
const metrics = require('@overleaf/metrics')
|
2020-08-20 07:52:59 -04:00
|
|
|
const {
|
2020-08-20 07:59:52 -04:00
|
|
|
ClientRequestedMissingOpsError,
|
2020-08-20 07:52:59 -04:00
|
|
|
DocumentUpdaterRequestFailedError,
|
|
|
|
NullBytesInOpError,
|
2021-07-13 07:04:45 -04:00
|
|
|
UpdateTooLargeError,
|
2020-08-20 07:52:59 -04:00
|
|
|
} = require('./Errors')
|
2014-11-12 10:54:55 -05:00
|
|
|
|
2020-11-10 06:32:06 -05:00
|
|
|
const rclient = require('@overleaf/redis-wrapper').createClient(
|
2020-06-23 13:29:44 -04:00
|
|
|
settings.redis.documentupdater
|
|
|
|
)
|
|
|
|
const Keys = settings.redis.documentupdater.key_schema
|
2014-11-13 12:07:05 -05:00
|
|
|
|
2020-07-13 05:42:50 -04:00
|
|
|
const DocumentUpdaterManager = {
|
2023-03-20 10:10:40 -04:00
|
|
|
getDocument(projectId, docId, fromVersion, callback) {
|
2020-06-23 13:29:44 -04:00
|
|
|
const timer = new metrics.Timer('get-document')
|
2023-03-20 10:10:40 -04:00
|
|
|
const url = `${settings.apis.documentupdater.url}/project/${projectId}/doc/${docId}?fromVersion=${fromVersion}`
|
2021-09-14 04:36:24 -04:00
|
|
|
logger.debug(
|
2023-03-20 10:10:40 -04:00
|
|
|
{ projectId, docId, fromVersion },
|
2020-06-23 13:29:44 -04:00
|
|
|
'getting doc from document updater'
|
|
|
|
)
|
2020-07-07 06:06:02 -04:00
|
|
|
request.get(url, function (err, res, body) {
|
2020-06-23 13:29:44 -04:00
|
|
|
timer.done()
|
2020-07-07 06:06:02 -04:00
|
|
|
if (err) {
|
2020-08-20 09:05:50 -04:00
|
|
|
OError.tag(err, 'error getting doc from doc updater')
|
2020-06-23 13:29:44 -04:00
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
2021-09-14 04:36:24 -04:00
|
|
|
logger.debug(
|
2023-03-20 10:10:40 -04:00
|
|
|
{ projectId, docId },
|
2020-06-23 13:29:44 -04:00
|
|
|
'got doc from document document updater'
|
|
|
|
)
|
|
|
|
try {
|
|
|
|
body = JSON.parse(body)
|
|
|
|
} catch (error) {
|
2020-08-20 09:05:50 -04:00
|
|
|
OError.tag(error, 'error parsing doc updater response')
|
2020-06-23 13:29:44 -04:00
|
|
|
return callback(error)
|
|
|
|
}
|
2020-07-07 06:06:02 -04:00
|
|
|
body = body || {}
|
2024-07-18 09:01:09 -04:00
|
|
|
callback(
|
|
|
|
null,
|
|
|
|
body.lines,
|
|
|
|
body.version,
|
|
|
|
body.ranges,
|
|
|
|
body.ops,
|
|
|
|
body.ttlInS
|
|
|
|
)
|
|
|
|
} else if (res.statusCode === 422 && body?.firstVersionInRedis) {
|
|
|
|
callback(new ClientRequestedMissingOpsError(422, body))
|
2020-06-23 13:29:44 -04:00
|
|
|
} else if ([404, 422].includes(res.statusCode)) {
|
2020-08-20 07:59:52 -04:00
|
|
|
callback(new ClientRequestedMissingOpsError(res.statusCode))
|
2020-06-23 13:29:44 -04:00
|
|
|
} else {
|
2020-08-20 07:52:59 -04:00
|
|
|
callback(
|
|
|
|
new DocumentUpdaterRequestFailedError('getDocument', res.statusCode)
|
2020-06-23 13:29:44 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
2014-11-14 10:53:59 -05:00
|
|
|
|
2023-03-20 10:10:40 -04:00
|
|
|
checkDocument(projectId, docId, callback) {
|
2020-07-13 05:42:50 -04:00
|
|
|
// in this call fromVersion = -1 means get document without docOps
|
2023-03-20 10:10:40 -04:00
|
|
|
DocumentUpdaterManager.getDocument(projectId, docId, -1, callback)
|
2020-07-13 05:42:50 -04:00
|
|
|
},
|
|
|
|
|
2023-03-20 10:10:40 -04:00
|
|
|
flushProjectToMongoAndDelete(projectId, callback) {
|
2020-06-23 13:29:44 -04:00
|
|
|
// this method is called when the last connected user leaves the project
|
2023-03-20 10:10:40 -04:00
|
|
|
logger.debug({ projectId }, 'deleting project from document updater')
|
2020-06-23 13:29:44 -04:00
|
|
|
const timer = new metrics.Timer('delete.mongo.project')
|
|
|
|
// flush the project in the background when all users have left
|
|
|
|
const url =
|
2023-03-20 10:10:40 -04:00
|
|
|
`${settings.apis.documentupdater.url}/project/${projectId}?background=true` +
|
2020-06-23 13:29:44 -04:00
|
|
|
(settings.shutDownInProgress ? '&shutdown=true' : '')
|
2020-07-07 06:06:02 -04:00
|
|
|
request.del(url, function (err, res) {
|
2020-06-23 13:29:44 -04:00
|
|
|
timer.done()
|
2020-07-07 06:06:02 -04:00
|
|
|
if (err) {
|
2020-08-20 09:05:50 -04:00
|
|
|
OError.tag(err, 'error deleting project from document updater')
|
2020-07-07 06:06:02 -04:00
|
|
|
callback(err)
|
2020-06-23 13:29:44 -04:00
|
|
|
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
2023-03-20 10:10:40 -04:00
|
|
|
logger.debug({ projectId }, 'deleted project from document updater')
|
2020-07-07 06:06:02 -04:00
|
|
|
callback(null)
|
2020-06-23 13:29:44 -04:00
|
|
|
} else {
|
2020-08-20 07:52:59 -04:00
|
|
|
callback(
|
|
|
|
new DocumentUpdaterRequestFailedError(
|
|
|
|
'flushProjectToMongoAndDelete',
|
|
|
|
res.statusCode
|
|
|
|
)
|
2020-06-23 13:29:44 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
2019-05-31 17:27:05 -04:00
|
|
|
|
2021-02-08 07:19:25 -05:00
|
|
|
_getPendingUpdateListKey() {
|
2021-02-09 05:48:40 -05:00
|
|
|
const shard = _.random(0, settings.pendingUpdateListShardCount - 1)
|
2021-02-08 07:19:25 -05:00
|
|
|
if (shard === 0) {
|
|
|
|
return 'pending-updates-list'
|
|
|
|
} else {
|
|
|
|
return `pending-updates-list-${shard}`
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-03-20 10:10:40 -04:00
|
|
|
queueChange(projectId, docId, change, callback) {
|
2020-06-23 13:29:44 -04:00
|
|
|
const allowedKeys = [
|
|
|
|
'doc',
|
|
|
|
'op',
|
|
|
|
'v',
|
|
|
|
'dupIfSource',
|
|
|
|
'meta',
|
|
|
|
'lastV',
|
2021-07-13 07:04:45 -04:00
|
|
|
'hash',
|
2020-06-23 13:29:44 -04:00
|
|
|
]
|
|
|
|
change = _.pick(change, allowedKeys)
|
|
|
|
const jsonChange = JSON.stringify(change)
|
|
|
|
if (jsonChange.indexOf('\u0000') !== -1) {
|
|
|
|
// memory corruption check
|
2020-08-20 06:30:22 -04:00
|
|
|
return callback(new NullBytesInOpError(jsonChange))
|
2020-06-23 13:29:44 -04:00
|
|
|
}
|
2020-03-24 06:22:28 -04:00
|
|
|
|
2020-06-23 13:29:44 -04:00
|
|
|
const updateSize = jsonChange.length
|
|
|
|
if (updateSize > settings.maxUpdateSize) {
|
2020-08-20 06:16:26 -04:00
|
|
|
return callback(new UpdateTooLargeError(updateSize))
|
2020-06-23 13:29:44 -04:00
|
|
|
}
|
2020-03-24 06:22:28 -04:00
|
|
|
|
2020-06-23 13:29:44 -04:00
|
|
|
// record metric for each update added to queue
|
|
|
|
metrics.summary('redis.pendingUpdates', updateSize, { status: 'push' })
|
2020-04-06 11:24:10 -04:00
|
|
|
|
2023-03-20 10:10:40 -04:00
|
|
|
const docKey = `${projectId}:${docId}`
|
2020-06-23 13:29:44 -04:00
|
|
|
// Push onto pendingUpdates for doc_id first, because once the doc updater
|
|
|
|
// gets an entry on pending-updates-list, it starts processing.
|
2021-07-13 07:04:45 -04:00
|
|
|
rclient.rpush(
|
2023-03-20 10:10:40 -04:00
|
|
|
Keys.pendingUpdates({ doc_id: docId }),
|
2021-07-13 07:04:45 -04:00
|
|
|
jsonChange,
|
|
|
|
function (error) {
|
2021-02-09 07:42:58 -05:00
|
|
|
if (error) {
|
2021-07-13 07:04:45 -04:00
|
|
|
error = new OError('error pushing update into redis').withCause(error)
|
|
|
|
return callback(error)
|
2020-08-20 09:05:50 -04:00
|
|
|
}
|
2021-07-13 07:04:45 -04:00
|
|
|
const queueKey = DocumentUpdaterManager._getPendingUpdateListKey()
|
2023-03-20 10:10:40 -04:00
|
|
|
rclient.rpush(queueKey, docKey, function (error) {
|
2021-07-13 07:04:45 -04:00
|
|
|
if (error) {
|
|
|
|
error = new OError('error pushing doc_id into redis')
|
|
|
|
.withInfo({ queueKey })
|
|
|
|
.withCause(error)
|
|
|
|
}
|
|
|
|
callback(error)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
2020-06-23 13:29:44 -04:00
|
|
|
}
|
2020-07-13 05:42:50 -04:00
|
|
|
|
|
|
|
module.exports = DocumentUpdaterManager
|