mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
lock compile directory
This commit is contained in:
parent
0d613a6480
commit
7f0e6f3eec
9 changed files with 151 additions and 8 deletions
|
@ -15,8 +15,11 @@ module.exports = CompileController =
|
||||||
request.user_id = req.params.user_id if req.params.user_id?
|
request.user_id = req.params.user_id if req.params.user_id?
|
||||||
ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) ->
|
ProjectPersistenceManager.markProjectAsJustAccessed request.project_id, (error) ->
|
||||||
return next(error) if error?
|
return next(error) if error?
|
||||||
CompileManager.doCompile request, (error, outputFiles = []) ->
|
CompileManager.doCompileWithLock request, (error, outputFiles = []) ->
|
||||||
if error instanceof Errors.FilesOutOfSyncError
|
if error instanceof Errors.AlreadyCompilingError
|
||||||
|
code = 423 # Http 443 Locked
|
||||||
|
status = "compile-in-progress"
|
||||||
|
else if error instanceof Errors.FilesOutOfSyncError
|
||||||
code = 409 # Http 409 Conflict
|
code = 409 # Http 409 Conflict
|
||||||
status = "retry"
|
status = "retry"
|
||||||
else if error?.terminated
|
else if error?.terminated
|
||||||
|
|
|
@ -9,6 +9,7 @@ Metrics = require "./Metrics"
|
||||||
child_process = require "child_process"
|
child_process = require "child_process"
|
||||||
DraftModeManager = require "./DraftModeManager"
|
DraftModeManager = require "./DraftModeManager"
|
||||||
TikzManager = require "./TikzManager"
|
TikzManager = require "./TikzManager"
|
||||||
|
LockManager = require "./LockManager"
|
||||||
fs = require("fs")
|
fs = require("fs")
|
||||||
fse = require "fs-extra"
|
fse = require "fs-extra"
|
||||||
os = require("os")
|
os = require("os")
|
||||||
|
@ -26,6 +27,18 @@ getCompileDir = (project_id, user_id) ->
|
||||||
Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id))
|
Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id))
|
||||||
|
|
||||||
module.exports = CompileManager =
|
module.exports = CompileManager =
|
||||||
|
|
||||||
|
doCompileWithLock: (request, callback = (error, outputFiles) ->) ->
|
||||||
|
compileDir = getCompileDir(request.project_id, request.user_id)
|
||||||
|
lockFile = Path.join(compileDir, ".project-lock")
|
||||||
|
# use a .project-lock file in the compile directory to prevent
|
||||||
|
# simultaneous compiles
|
||||||
|
fse.ensureDir compileDir, (error) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
LockManager.runWithLock lockFile, (releaseLock) ->
|
||||||
|
CompileManager.doCompile(request, releaseLock)
|
||||||
|
, callback
|
||||||
|
|
||||||
doCompile: (request, callback = (error, outputFiles) ->) ->
|
doCompile: (request, callback = (error, outputFiles) ->) ->
|
||||||
compileDir = getCompileDir(request.project_id, request.user_id)
|
compileDir = getCompileDir(request.project_id, request.user_id)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,14 @@ FilesOutOfSyncError = (message) ->
|
||||||
return error
|
return error
|
||||||
FilesOutOfSyncError.prototype.__proto__ = Error.prototype
|
FilesOutOfSyncError.prototype.__proto__ = Error.prototype
|
||||||
|
|
||||||
|
AlreadyCompilingError = (message) ->
|
||||||
|
error = new Error(message)
|
||||||
|
error.name = "AlreadyCompilingError"
|
||||||
|
error.__proto__ = AlreadyCompilingError.prototype
|
||||||
|
return error
|
||||||
|
AlreadyCompilingError.prototype.__proto__ = Error.prototype
|
||||||
|
|
||||||
module.exports = Errors =
|
module.exports = Errors =
|
||||||
NotFoundError: NotFoundError
|
NotFoundError: NotFoundError
|
||||||
FilesOutOfSyncError: FilesOutOfSyncError
|
FilesOutOfSyncError: FilesOutOfSyncError
|
||||||
|
AlreadyCompilingError: AlreadyCompilingError
|
||||||
|
|
23
services/clsi/app/coffee/LockManager.coffee
Normal file
23
services/clsi/app/coffee/LockManager.coffee
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
Settings = require('settings-sharelatex')
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
Lockfile = require('lockfile') # from https://github.com/npm/lockfile
|
||||||
|
Errors = require "./Errors"
|
||||||
|
|
||||||
|
module.exports = LockManager =
|
||||||
|
LOCK_TEST_INTERVAL: 1000 # 50ms between each test of the lock
|
||||||
|
MAX_LOCK_WAIT_TIME: 15000 # 10s maximum time to spend trying to get the lock
|
||||||
|
LOCK_STALE: 5*60*1000 # 5 mins time until lock auto expires
|
||||||
|
|
||||||
|
runWithLock: (path, runner = ((releaseLock = (error) ->) ->), callback = ((error) ->)) ->
|
||||||
|
lockOpts =
|
||||||
|
wait: @MAX_LOCK_WAIT_TIME
|
||||||
|
pollPeriod: @LOCK_TEST_INTERVAL
|
||||||
|
stale: @LOCK_STALE
|
||||||
|
Lockfile.lock path, lockOpts, (error) ->
|
||||||
|
return callback new Errors.AlreadyCompilingError("compile in progress") if error?.code is 'EEXIST'
|
||||||
|
return callback(error) if error?
|
||||||
|
runner (error1, args...) ->
|
||||||
|
Lockfile.unlock path, (error2) ->
|
||||||
|
error = error1 or error2
|
||||||
|
return callback(error) if error?
|
||||||
|
callback(null, args...)
|
|
@ -78,7 +78,7 @@ module.exports = ResourceWriter =
|
||||||
should_delete = true
|
should_delete = true
|
||||||
if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache
|
if path.match(/^output\./) or path.match(/\.aux$/) or path.match(/^cache\//) # knitr cache
|
||||||
should_delete = false
|
should_delete = false
|
||||||
if path == '.project-sync-state'
|
if path == '.project-sync-state' or path == '.project-lock'
|
||||||
should_delete = false
|
should_delete = false
|
||||||
if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv"
|
if path == "output.pdf" or path == "output.dvi" or path == "output.log" or path == "output.xdv"
|
||||||
should_delete = true
|
should_delete = true
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"fs-extra": "^0.16.3",
|
"fs-extra": "^0.16.3",
|
||||||
"grunt-mkdir": "^1.0.0",
|
"grunt-mkdir": "^1.0.0",
|
||||||
"heapdump": "^0.3.5",
|
"heapdump": "^0.3.5",
|
||||||
|
"lockfile": "^1.0.3",
|
||||||
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4",
|
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.4",
|
||||||
"lynx": "0.0.11",
|
"lynx": "0.0.11",
|
||||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0",
|
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0",
|
||||||
|
|
|
@ -49,7 +49,7 @@ describe "CompileController", ->
|
||||||
|
|
||||||
describe "successfully", ->
|
describe "successfully", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@CompileManager.doCompile = sinon.stub().callsArgWith(1, null, @output_files)
|
@CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, @output_files)
|
||||||
@CompileController.compile @req, @res
|
@CompileController.compile @req, @res
|
||||||
|
|
||||||
it "should parse the request", ->
|
it "should parse the request", ->
|
||||||
|
@ -58,7 +58,7 @@ describe "CompileController", ->
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
it "should run the compile for the specified project", ->
|
it "should run the compile for the specified project", ->
|
||||||
@CompileManager.doCompile
|
@CompileManager.doCompileWithLock
|
||||||
.calledWith(@request_with_project_id)
|
.calledWith(@request_with_project_id)
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ describe "CompileController", ->
|
||||||
|
|
||||||
describe "with an error", ->
|
describe "with an error", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@CompileManager.doCompile = sinon.stub().callsArgWith(1, new Error(@message = "error message"), null)
|
@CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, new Error(@message = "error message"), null)
|
||||||
@CompileController.compile @req, @res
|
@CompileController.compile @req, @res
|
||||||
|
|
||||||
it "should return the JSON response with the error", ->
|
it "should return the JSON response with the error", ->
|
||||||
|
@ -102,7 +102,7 @@ describe "CompileController", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@error = new Error(@message = "container timed out")
|
@error = new Error(@message = "container timed out")
|
||||||
@error.timedout = true
|
@error.timedout = true
|
||||||
@CompileManager.doCompile = sinon.stub().callsArgWith(1, @error, null)
|
@CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, @error, null)
|
||||||
@CompileController.compile @req, @res
|
@CompileController.compile @req, @res
|
||||||
|
|
||||||
it "should return the JSON response with the timeout status", ->
|
it "should return the JSON response with the timeout status", ->
|
||||||
|
@ -118,7 +118,7 @@ describe "CompileController", ->
|
||||||
|
|
||||||
describe "when the request returns no output files", ->
|
describe "when the request returns no output files", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@CompileManager.doCompile = sinon.stub().callsArgWith(1, null, [])
|
@CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, [])
|
||||||
@CompileController.compile @req, @res
|
@CompileController.compile @req, @res
|
||||||
|
|
||||||
it "should return the JSON response with the failure status", ->
|
it "should return the JSON response with the failure status", ->
|
||||||
|
|
|
@ -19,9 +19,50 @@ describe "CompileManager", ->
|
||||||
"./CommandRunner": @CommandRunner = {}
|
"./CommandRunner": @CommandRunner = {}
|
||||||
"./DraftModeManager": @DraftModeManager = {}
|
"./DraftModeManager": @DraftModeManager = {}
|
||||||
"./TikzManager": @TikzManager = {}
|
"./TikzManager": @TikzManager = {}
|
||||||
|
"./LockManager": @LockManager = {}
|
||||||
"fs": @fs = {}
|
"fs": @fs = {}
|
||||||
@callback = sinon.stub()
|
@callback = sinon.stub()
|
||||||
|
|
||||||
|
describe "doCompileWithLock", ->
|
||||||
|
beforeEach ->
|
||||||
|
@request =
|
||||||
|
resources: @resources = "mock-resources"
|
||||||
|
project_id: @project_id = "project-id-123"
|
||||||
|
user_id: @user_id = "1234"
|
||||||
|
@output_files = ["foo", "bar"]
|
||||||
|
@CompileManager.doCompile = sinon.stub().callsArgWith(1, null, @output_files)
|
||||||
|
@LockManager.runWithLock = (lockFile, runner, callback) ->
|
||||||
|
runner (err, result...) ->
|
||||||
|
callback(err, result...)
|
||||||
|
|
||||||
|
describe "when the project is not locked", ->
|
||||||
|
beforeEach ->
|
||||||
|
@CompileManager.doCompileWithLock @request, @callback
|
||||||
|
|
||||||
|
it "should call doCompile with the request", ->
|
||||||
|
@CompileManager.doCompile
|
||||||
|
.calledWith(@request)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the output files", ->
|
||||||
|
@callback.calledWithExactly(null, @output_files)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "when the project is locked", ->
|
||||||
|
beforeEach ->
|
||||||
|
@error = new Error("locked")
|
||||||
|
@LockManager.runWithLock = (lockFile, runner, callback) =>
|
||||||
|
callback(@error)
|
||||||
|
@CompileManager.doCompileWithLock @request, @callback
|
||||||
|
|
||||||
|
it "should not call doCompile with the request", ->
|
||||||
|
@CompileManager.doCompile
|
||||||
|
.called.should.equal false
|
||||||
|
|
||||||
|
it "should call the callback with the error", ->
|
||||||
|
@callback.calledWithExactly(@error)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
describe "doCompile", ->
|
describe "doCompile", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@output_files = [{
|
@output_files = [{
|
||||||
|
|
54
services/clsi/test/unit/coffee/LockManager.coffee
Normal file
54
services/clsi/test/unit/coffee/LockManager.coffee
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
sinon = require('sinon')
|
||||||
|
require('chai').should()
|
||||||
|
modulePath = require('path').join __dirname, '../../../app/js/LockManager'
|
||||||
|
Path = require "path"
|
||||||
|
Errors = require "../../../app/js/Errors"
|
||||||
|
|
||||||
|
describe "LockManager", ->
|
||||||
|
beforeEach ->
|
||||||
|
@LockManager = SandboxedModule.require modulePath, requires:
|
||||||
|
"settings-sharelatex": {}
|
||||||
|
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
|
||||||
|
"lockfile": @Lockfile = {}
|
||||||
|
@lockFile = "/local/compile/directory/.project-lock"
|
||||||
|
|
||||||
|
describe "runWithLock", ->
|
||||||
|
beforeEach ->
|
||||||
|
@runner = sinon.stub().callsArgWith(0, null, "foo", "bar")
|
||||||
|
@callback = sinon.stub()
|
||||||
|
|
||||||
|
describe "normally", ->
|
||||||
|
beforeEach ->
|
||||||
|
@Lockfile.lock = sinon.stub().callsArgWith(2, null)
|
||||||
|
@Lockfile.unlock = sinon.stub().callsArgWith(1, null)
|
||||||
|
@LockManager.runWithLock @lockFile, @runner, @callback
|
||||||
|
|
||||||
|
it "should run the compile", ->
|
||||||
|
@runner
|
||||||
|
.calledWith()
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the response from the compile", ->
|
||||||
|
@callback
|
||||||
|
.calledWithExactly(null, "foo", "bar")
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "when the project is locked", ->
|
||||||
|
beforeEach ->
|
||||||
|
@error = new Error()
|
||||||
|
@error.code = "EEXIST"
|
||||||
|
@Lockfile.lock = sinon.stub().callsArgWith(2,@error)
|
||||||
|
@Lockfile.unlock = sinon.stub().callsArgWith(1, null)
|
||||||
|
@LockManager.runWithLock @lockFile, @runner, @callback
|
||||||
|
|
||||||
|
it "should not run the compile", ->
|
||||||
|
@runner
|
||||||
|
.called
|
||||||
|
.should.equal false
|
||||||
|
|
||||||
|
it "should return an error", ->
|
||||||
|
error = new Errors.AlreadyCompilingError()
|
||||||
|
@callback
|
||||||
|
.calledWithExactly(error)
|
||||||
|
.should.equal true
|
Loading…
Reference in a new issue