mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
shouldFlushHistoryOps has a default value for 'threshold', which keeps the exports simpler and still lets the unit tests override it. GitOrigin-RevId: 1c6d4a2778052b5af40e2e338589a230ac2f4646
255 lines
6.8 KiB
JavaScript
255 lines
6.8 KiB
JavaScript
const RedisManager = require('./RedisManager')
|
|
const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
|
|
const DocumentManager = require('./DocumentManager')
|
|
const HistoryManager = require('./HistoryManager')
|
|
const logger = require('@overleaf/logger')
|
|
const Metrics = require('./Metrics')
|
|
const Errors = require('./Errors')
|
|
const { callbackifyAll } = require('@overleaf/promise-utils')
|
|
|
|
async function flushProjectWithLocks(projectId) {
|
|
const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
|
|
const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
|
|
|
|
logger.debug({ projectId, docIds }, 'flushing docs')
|
|
const errors = []
|
|
for (const docId of docIds) {
|
|
try {
|
|
await DocumentManager.promises.flushDocIfLoadedWithLock(projectId, docId)
|
|
} catch (error) {
|
|
if (error instanceof Errors.NotFoundError) {
|
|
logger.warn(
|
|
{ err: error, projectId, docId },
|
|
'found deleted doc when flushing'
|
|
)
|
|
} else {
|
|
logger.error({ err: error, projectId, docId }, 'error flushing doc')
|
|
errors.push(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
timer.done()
|
|
if (errors.length > 0) {
|
|
throw new Error('Errors flushing docs. See log for details')
|
|
}
|
|
}
|
|
|
|
async function flushAndDeleteProjectWithLocks(projectId, options) {
|
|
const timer = new Metrics.Timer(
|
|
'projectManager.flushAndDeleteProjectWithLocks'
|
|
)
|
|
|
|
const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
|
|
|
|
logger.debug({ projectId, docIds }, 'deleting docs')
|
|
const errors = []
|
|
for (const docId of docIds) {
|
|
try {
|
|
await DocumentManager.promises.flushAndDeleteDocWithLock(
|
|
projectId,
|
|
docId,
|
|
{}
|
|
)
|
|
} catch (error) {
|
|
logger.error({ err: error, projectId, docId }, 'error deleting doc')
|
|
errors.push(error)
|
|
}
|
|
}
|
|
|
|
// When deleting the project here we want to ensure that project
|
|
// history is completely flushed because the project may be
|
|
// deleted in web after this call completes, and so further
|
|
// attempts to flush would fail after that.
|
|
await HistoryManager.promises.flushProjectChanges(projectId, options)
|
|
timer.done()
|
|
if (errors.length > 0) {
|
|
throw new Error('Errors deleting docs. See log for details')
|
|
}
|
|
}
|
|
|
|
async function queueFlushAndDeleteProject(projectId) {
|
|
await RedisManager.promises.queueFlushAndDeleteProject(projectId)
|
|
Metrics.inc('queued-delete')
|
|
}
|
|
|
|
async function getProjectDocsTimestamps(projectId, callback) {
|
|
const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
|
|
if (docIds.length === 0) {
|
|
return []
|
|
}
|
|
|
|
const timestamps = await RedisManager.promises.getDocTimestamps(docIds)
|
|
return timestamps
|
|
}
|
|
|
|
async function getProjectDocsAndFlushIfOld(
|
|
projectId,
|
|
projectStateHash,
|
|
excludeVersions
|
|
) {
|
|
const timer = new Metrics.Timer('projectManager.getProjectDocsAndFlushIfOld')
|
|
|
|
const projectStateChanged =
|
|
await RedisManager.promises.checkOrSetProjectState(
|
|
projectId,
|
|
projectStateHash
|
|
)
|
|
|
|
// we can't return docs if project structure has changed
|
|
if (projectStateChanged) {
|
|
timer.done()
|
|
throw new Errors.ProjectStateChangedError('project state changed')
|
|
}
|
|
|
|
// project structure hasn't changed, return doc content from redis
|
|
const docs = []
|
|
const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
|
|
for (const docId of docIds) {
|
|
const { lines, version } =
|
|
await DocumentManager.promises.getDocAndFlushIfOldWithLock(
|
|
projectId,
|
|
docId
|
|
)
|
|
docs.push({ _id: docId, lines, v: version })
|
|
}
|
|
|
|
timer.done()
|
|
return docs
|
|
}
|
|
|
|
async function getProjectRanges(projectId) {
|
|
const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
|
|
const docs = []
|
|
for (const docId of docIds) {
|
|
const ranges = await RedisManager.promises.getDocRanges(docId)
|
|
docs.push({ id: docId, ranges })
|
|
}
|
|
return docs
|
|
}
|
|
|
|
async function clearProjectState(projectId) {
|
|
await RedisManager.promises.clearProjectState(projectId)
|
|
}
|
|
|
|
async function updateProjectWithLocks(
|
|
projectId,
|
|
projectHistoryId,
|
|
userId,
|
|
updates,
|
|
projectVersion,
|
|
source
|
|
) {
|
|
const timer = new Metrics.Timer('projectManager.updateProject')
|
|
|
|
let projectSubversion = 0 // project versions can have multiple operations
|
|
let projectOpsLength = 0
|
|
|
|
for (const update of updates) {
|
|
update.version = `${projectVersion}.${projectSubversion++}`
|
|
switch (update.type) {
|
|
case 'add-doc':
|
|
projectOpsLength =
|
|
await ProjectHistoryRedisManager.promises.queueAddEntity(
|
|
projectId,
|
|
projectHistoryId,
|
|
'doc',
|
|
update.id,
|
|
userId,
|
|
update,
|
|
source
|
|
)
|
|
break
|
|
case 'rename-doc':
|
|
if (!update.newPathname) {
|
|
// an empty newPathname signifies a delete, so there is no need to
|
|
// update the pathname in redis
|
|
projectOpsLength =
|
|
await ProjectHistoryRedisManager.promises.queueRenameEntity(
|
|
projectId,
|
|
projectHistoryId,
|
|
'doc',
|
|
update.id,
|
|
userId,
|
|
update,
|
|
source
|
|
)
|
|
} else {
|
|
// rename the doc in redis before queuing the update
|
|
await DocumentManager.promises.renameDocWithLock(
|
|
projectId,
|
|
update.id,
|
|
userId,
|
|
update,
|
|
projectHistoryId
|
|
)
|
|
projectOpsLength =
|
|
await ProjectHistoryRedisManager.promises.queueRenameEntity(
|
|
projectId,
|
|
projectHistoryId,
|
|
'doc',
|
|
update.id,
|
|
userId,
|
|
update,
|
|
source
|
|
)
|
|
}
|
|
break
|
|
case 'add-file':
|
|
projectOpsLength =
|
|
await ProjectHistoryRedisManager.promises.queueAddEntity(
|
|
projectId,
|
|
projectHistoryId,
|
|
'file',
|
|
update.id,
|
|
userId,
|
|
update,
|
|
source
|
|
)
|
|
break
|
|
case 'rename-file':
|
|
projectOpsLength =
|
|
await ProjectHistoryRedisManager.promises.queueRenameEntity(
|
|
projectId,
|
|
projectHistoryId,
|
|
'file',
|
|
update.id,
|
|
userId,
|
|
update,
|
|
source
|
|
)
|
|
break
|
|
default:
|
|
throw new Error(`Unknown update type: ${update.type}`)
|
|
}
|
|
}
|
|
|
|
if (
|
|
HistoryManager.shouldFlushHistoryOps(
|
|
projectId,
|
|
projectOpsLength,
|
|
updates.length
|
|
)
|
|
) {
|
|
HistoryManager.flushProjectChangesAsync(projectId)
|
|
}
|
|
|
|
timer.done()
|
|
}
|
|
|
|
const ProjectManager = {
|
|
flushProjectWithLocks,
|
|
flushAndDeleteProjectWithLocks,
|
|
queueFlushAndDeleteProject,
|
|
getProjectDocsTimestamps,
|
|
getProjectDocsAndFlushIfOld,
|
|
getProjectRanges,
|
|
clearProjectState,
|
|
updateProjectWithLocks,
|
|
}
|
|
|
|
module.exports = {
|
|
...callbackifyAll(ProjectManager),
|
|
promises: ProjectManager,
|
|
}
|