From bd841b4795b17d0e6928b5c6a5d6ccdc4d1af391 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 25 Nov 2014 15:26:33 +0000 Subject: [PATCH] coppied the lock manager over from doc updater --- .../coffee/infrastructure/LockManager.coffee | 54 ++++++++++++++ .../LockManager/CheckingTheLock.coffee | 49 +++++++++++++ .../LockManager/ReleasingTheLock.coffee | 27 +++++++ .../LockManager/getLockTests.coffee | 70 +++++++++++++++++++ .../LockManager/tryLockTests.coffee | 38 ++++++++++ 5 files changed, 238 insertions(+) create mode 100644 services/web/app/coffee/infrastructure/LockManager.coffee create mode 100644 services/web/test/UnitTests/coffee/infrastructure/LockManager/CheckingTheLock.coffee create mode 100644 services/web/test/UnitTests/coffee/infrastructure/LockManager/ReleasingTheLock.coffee create mode 100644 services/web/test/UnitTests/coffee/infrastructure/LockManager/getLockTests.coffee create mode 100644 services/web/test/UnitTests/coffee/infrastructure/LockManager/tryLockTests.coffee diff --git a/services/web/app/coffee/infrastructure/LockManager.coffee b/services/web/app/coffee/infrastructure/LockManager.coffee new file mode 100644 index 0000000000..32ae2af765 --- /dev/null +++ b/services/web/app/coffee/infrastructure/LockManager.coffee @@ -0,0 +1,54 @@ +metrics = require('./Metrics') +Settings = require('settings-sharelatex') +redis = require("redis-sharelatex") +rclient = redis.createClient(Settings.redis.web) +logger = require "logger-sharelatex" + +module.exports = LockManager = + LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock + MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock + REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis. + + _blockingKey : (key)-> "Blocking:"+key + + tryLock : (key, callback = (err, isFree)->)-> + rclient.set LockManager._blockingKey(key), "locked", "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)-> + return callback(err) if err? + if gotLock == "OK" + metrics.inc "doc-not-blocking" + callback err, true + else + metrics.inc "doc-blocking" + logger.log key: key, redis_response: gotLock, "doc is locked" + callback err, false + + getLock: (key, callback = (error) ->) -> + startTime = Date.now() + do attempt = () -> + if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME + return callback(new Error("Timeout")) + + LockManager.tryLock key, (error, gotLock) -> + return callback(error) if error? + if gotLock + callback(null) + else + setTimeout attempt, LockManager.LOCK_TEST_INTERVAL + + checkLock: (key, callback = (err, isFree)->)-> + multi = rclient.multi() + multi.exists LockManager._blockingKey(key) + multi.exec (err, replys)-> + return callback(err) if err? + exists = parseInt replys[0] + if exists == 1 + metrics.inc "doc-blocking" + callback err, false + else + metrics.inc "doc-not-blocking" + callback err, true + + releaseLock: (key, callback)-> + rclient.del LockManager._blockingKey(key), callback + + diff --git a/services/web/test/UnitTests/coffee/infrastructure/LockManager/CheckingTheLock.coffee b/services/web/test/UnitTests/coffee/infrastructure/LockManager/CheckingTheLock.coffee new file mode 100644 index 0000000000..2f732bded5 --- /dev/null +++ b/services/web/test/UnitTests/coffee/infrastructure/LockManager/CheckingTheLock.coffee @@ -0,0 +1,49 @@ +require('coffee-script') +sinon = require('sinon') +assert = require('assert') +path = require('path') +modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' +project_id = 1234 +doc_id = 5678 +blockingKey = "Blocking:#{doc_id}" +SandboxedModule = require('sandboxed-module') + +describe 'Lock Manager - checking the lock', ()-> + + existsStub = sinon.stub() + setStub = sinon.stub() + exireStub = sinon.stub() + execStub = sinon.stub() + + mocks = + "logger-sharelatex": log:-> + + "redis-sharelatex": + createClient : ()-> + auth:-> + multi: -> + exists: existsStub + expire: exireStub + set: setStub + exec: execStub + LockManager = SandboxedModule.require(modulePath, requires: mocks) + + it 'should check if lock exists but not set or expire', (done)-> + execStub.callsArgWith(0, null, ["1"]) + LockManager.checkLock doc_id, (err, docIsLocked)-> + existsStub.calledWith(blockingKey).should.equal true + setStub.called.should.equal false + exireStub.called.should.equal false + done() + + it 'should return true if the key does not exists', (done)-> + execStub.callsArgWith(0, null, "0") + LockManager.checkLock doc_id, (err, free)-> + free.should.equal true + done() + + it 'should return false if the key does exists', (done)-> + execStub.callsArgWith(0, null, "1") + LockManager.checkLock doc_id, (err, free)-> + free.should.equal false + done() diff --git a/services/web/test/UnitTests/coffee/infrastructure/LockManager/ReleasingTheLock.coffee b/services/web/test/UnitTests/coffee/infrastructure/LockManager/ReleasingTheLock.coffee new file mode 100644 index 0000000000..42ca0be51b --- /dev/null +++ b/services/web/test/UnitTests/coffee/infrastructure/LockManager/ReleasingTheLock.coffee @@ -0,0 +1,27 @@ +require('coffee-script') +sinon = require('sinon') +assert = require('assert') +path = require('path') +modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' +project_id = 1234 +doc_id = 5678 +SandboxedModule = require('sandboxed-module') + +describe 'LockManager - releasing the lock', ()-> + + deleteStub = sinon.stub().callsArgWith(1) + mocks = + "logger-sharelatex": log:-> + + "redis-sharelatex": + createClient : ()-> + auth:-> + del:deleteStub + + LockManager = SandboxedModule.require(modulePath, requires: mocks) + + it 'should put a all data into memory', (done)-> + LockManager.releaseLock doc_id, -> + deleteStub.calledWith("Blocking:#{doc_id}").should.equal true + done() + diff --git a/services/web/test/UnitTests/coffee/infrastructure/LockManager/getLockTests.coffee b/services/web/test/UnitTests/coffee/infrastructure/LockManager/getLockTests.coffee new file mode 100644 index 0000000000..5441577da6 --- /dev/null +++ b/services/web/test/UnitTests/coffee/infrastructure/LockManager/getLockTests.coffee @@ -0,0 +1,70 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +path = require('path') +modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' +SandboxedModule = require('sandboxed-module') + +describe 'LockManager - getting the lock', -> + beforeEach -> + @LockManager = SandboxedModule.require modulePath, requires: + "logger-sharelatex": log:-> + "redis-sharelatex": + createClient : () => + auth:-> + @callback = sinon.stub() + @doc_id = "doc-id-123" + + describe "when the lock is not set", -> + beforeEach (done) -> + @LockManager.tryLock = sinon.stub().callsArgWith(1, null, true) + @LockManager.getLock @doc_id, (args...) => + @callback(args...) + done() + + it "should try to get the lock", -> + @LockManager.tryLock + .calledWith(@doc_id) + .should.equal true + + it "should only need to try once", -> + @LockManager.tryLock.callCount.should.equal 1 + + it "should return the callback", -> + @callback.calledWith(null).should.equal true + + describe "when the lock is initially set", -> + beforeEach (done) -> + startTime = Date.now() + @LockManager.LOCK_TEST_INTERVAL = 5 + @LockManager.tryLock = (doc_id, callback = (error, isFree) ->) -> + if Date.now() - startTime < 20 + callback null, false + else + callback null, true + sinon.spy @LockManager, "tryLock" + + @LockManager.getLock @doc_id, (args...) => + @callback(args...) + done() + + it "should call tryLock multiple times until free", -> + (@LockManager.tryLock.callCount > 1).should.equal true + + it "should return the callback", -> + @callback.calledWith(null).should.equal true + + describe "when the lock times out", -> + beforeEach (done) -> + time = Date.now() + @LockManager.MAX_LOCK_WAIT_TIME = 5 + @LockManager.tryLock = sinon.stub().callsArgWith(1, null, false) + @LockManager.getLock @doc_id, (args...) => + @callback(args...) + done() + + it "should return the callback with an error", -> + @callback.calledWith(new Error("timeout")).should.equal true + + + diff --git a/services/web/test/UnitTests/coffee/infrastructure/LockManager/tryLockTests.coffee b/services/web/test/UnitTests/coffee/infrastructure/LockManager/tryLockTests.coffee new file mode 100644 index 0000000000..3d5a19f265 --- /dev/null +++ b/services/web/test/UnitTests/coffee/infrastructure/LockManager/tryLockTests.coffee @@ -0,0 +1,38 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +path = require('path') +modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' +SandboxedModule = require('sandboxed-module') + +describe 'LockManager - trying the lock', -> + beforeEach -> + @LockManager = SandboxedModule.require modulePath, requires: + "logger-sharelatex": log:-> + "redis-sharelatex": + createClient : () => + auth:-> + set: @set = sinon.stub() + @callback = sinon.stub() + @doc_id = "doc-id-123" + + describe "when the lock is not set", -> + beforeEach -> + @set.callsArgWith(5, null, "OK") + @LockManager.tryLock @doc_id, @callback + + it "should set the lock key with an expiry if it is not set", -> + @set.calledWith("Blocking:#{@doc_id}", "locked", "EX", 30, "NX") + .should.equal true + + it "should return the callback with true", -> + @callback.calledWith(null, true).should.equal true + + describe "when the lock is already set", -> + beforeEach -> + @set.callsArgWith(5, null, null) + @LockManager.tryLock @doc_id, @callback + + it "should return the callback with false", -> + @callback.calledWith(null, false).should.equal true +