Merge pull request #4650 from overleaf/jpa-in-memory-last-access-time

[perf] store the last access time of a project in memory

GitOrigin-RevId: 16e2bee28f58eced18f4c3ec5571ea9d10805cbb
This commit is contained in:
Jakob Ackermann 2021-10-06 10:12:09 +02:00 committed by Copybot
parent 02918e7483
commit 44d0a8d162
2 changed files with 57 additions and 55 deletions

View file

@ -5,7 +5,6 @@
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
const tenMinutes = 10 * 60 * 1000
const Metrics = require('@overleaf/metrics') const Metrics = require('@overleaf/metrics')
Metrics.initialize('clsi') Metrics.initialize('clsi')
@ -32,6 +31,7 @@ const OutputCacheManager = require('./app/js/OutputCacheManager')
const ContentCacheManager = require('./app/js/ContentCacheManager') const ContentCacheManager = require('./app/js/ContentCacheManager')
require('./app/js/db').sync() require('./app/js/db').sync()
ProjectPersistenceManager.init()
const express = require('express') const express = require('express')
const bodyParser = require('body-parser') const bodyParser = require('body-parser')
@ -420,12 +420,6 @@ if (!module.parent) {
module.exports = app module.exports = app
setInterval(() => {
ProjectPersistenceManager.refreshExpiryTimeout(() => {
ProjectPersistenceManager.clearExpiredProjects()
})
}, tenMinutes)
function __guard__(value, transform) { function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null return typeof value !== 'undefined' && value !== null
? transform(value) ? transform(value)

View file

@ -12,17 +12,19 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
let ProjectPersistenceManager let ProjectPersistenceManager
const Metrics = require('./Metrics')
const UrlCache = require('./UrlCache') const UrlCache = require('./UrlCache')
const CompileManager = require('./CompileManager') const CompileManager = require('./CompileManager')
const db = require('./db')
const dbQueue = require('./DbQueue')
const async = require('async') const async = require('async')
const logger = require('logger-sharelatex') const logger = require('logger-sharelatex')
const oneDay = 24 * 60 * 60 * 1000 const oneDay = 24 * 60 * 60 * 1000
const Settings = require('@overleaf/settings') const Settings = require('@overleaf/settings')
const diskusage = require('diskusage') const diskusage = require('diskusage')
const { callbackify } = require('util') const { callbackify } = require('util')
const Path = require('path')
const fs = require('fs')
// projectId -> timestamp mapping.
const LAST_ACCESS = new Map()
async function refreshExpiryTimeout() { async function refreshExpiryTimeout() {
const paths = [ const paths = [
@ -61,26 +63,50 @@ module.exports = ProjectPersistenceManager = {
}, },
refreshExpiryTimeout: callbackify(refreshExpiryTimeout), refreshExpiryTimeout: callbackify(refreshExpiryTimeout),
markProjectAsJustAccessed(project_id, callback) {
if (callback == null) { init() {
callback = function (error) {} fs.readdir(Settings.path.compilesDir, (err, dirs) => {
} if (err) {
const timer = new Metrics.Timer('db-bump-last-accessed') logger.warn({ err }, 'cannot get project listing')
const job = cb => dirs = []
db.Project.findOrCreate({ where: { project_id } }) }
.spread((project, created) =>
project async.eachLimit(
.update({ lastAccessed: new Date() }) dirs,
.then(() => cb()) 10,
.error(cb) (projectAndUserId, cb) => {
) const compileDir = Path.join(
.error(cb) Settings.path.compilesDir,
dbQueue.queue.push(job, error => { projectAndUserId
timer.done() )
callback(error) const projectId = projectAndUserId.slice(0, 24)
fs.stat(compileDir, (err, stats) => {
if (err) {
// Schedule for immediate cleanup
LAST_ACCESS.set(projectId, 0)
} else {
// Cleanup eventually.
LAST_ACCESS.set(projectId, stats.mtime.getTime())
}
cb()
})
},
() => {
setInterval(() => {
ProjectPersistenceManager.refreshExpiryTimeout(() => {
ProjectPersistenceManager.clearExpiredProjects()
})
}, 10 * 60 * 1000)
}
)
}) })
}, },
markProjectAsJustAccessed(project_id, callback) {
LAST_ACCESS.set(project_id, Date.now())
callback()
},
clearExpiredProjects(callback) { clearExpiredProjects(callback) {
if (callback == null) { if (callback == null) {
callback = function (error) {} callback = function (error) {}
@ -166,38 +192,20 @@ module.exports = ProjectPersistenceManager = {
}, },
_clearProjectFromDatabase(project_id, callback) { _clearProjectFromDatabase(project_id, callback) {
if (callback == null) { LAST_ACCESS.delete(project_id)
callback = function (error) {} callback()
}
logger.log({ project_id }, 'clearing project from database')
const job = cb =>
db.Project.destroy({ where: { project_id } })
.then(() => cb())
.error(cb)
return dbQueue.queue.push(job, callback)
}, },
_findExpiredProjectIds(callback) { _findExpiredProjectIds(callback) {
if (callback == null) { const expiredFrom = Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT
callback = function (error, project_ids) {} const expiredProjectsIds = []
for (const [projectId, lastAccess] of LAST_ACCESS.entries()) {
if (lastAccess < expiredFrom) {
expiredProjectsIds.push(projectId)
}
} }
const job = function (cb) { // ^ may be a fairly busy loop, continue detached.
const keepProjectsFrom = new Date( setTimeout(() => callback(null, expiredProjectsIds), 0)
Date.now() - ProjectPersistenceManager.EXPIRY_TIMEOUT
)
const q = {}
q[db.op.lt] = keepProjectsFrom
return db.Project.findAll({ where: { lastAccessed: q } })
.then(projects =>
cb(
null,
projects.map(project => project.project_id)
)
)
.error(cb)
}
return dbQueue.queue.push(job, callback)
}, },
} }