Add a cooldown mechanism for projects which go over limits

This commit is contained in:
Shane Kilkelly 2017-05-09 11:40:42 +01:00
parent c5c0364d49
commit 149e38855f
6 changed files with 242 additions and 0 deletions

View file

@ -0,0 +1,24 @@
RedisWrapper = require('../../infrastructure/RedisWrapper')
rclient = RedisWrapper.client('cooldown')
logger = require('logger-sharelatex')
COOLDOWN_IN_SECONDS = 60 * 10
module.exports = CooldownManager =
_buildKey: (projectId) ->
"Cooldown:{#{projectId}}"
putProjectOnCooldown: (projectId, callback=(err)->) ->
logger.log {projectId},
"[Cooldown] putting project on cooldown for #{COOLDOWN_IN_SECONDS} seconds"
rclient.set(CooldownManager._buildKey(projectId), '1', 'EX', COOLDOWN_IN_SECONDS, callback)
isProjectOnCooldown: (projectId, callback=(err, isOnCooldown)->) ->
rclient.get CooldownManager._buildKey(projectId), (err, result) ->
if err?
return callback(err)
callback(null, result == "1")

View file

@ -0,0 +1,15 @@
CooldownManager = require('./CooldownManager')
logger = require('logger-sharelatex')
module.exports = CooldownMiddlewear =
freezeProject: (req, res, next) ->
projectId = req.params.Project_id
CooldownManager.isProjectOnCooldown projectId, (err, projectIsOnCooldown) ->
if err?
return next(err)
if projectIsOnCooldown
logger.log {projectId}, "[Cooldown] project is on cooldown, denying request"
return res.sendStatus(429)
next()

View file

@ -32,6 +32,7 @@ ChatController = require("./Features/Chat/ChatController")
BlogController = require("./Features/Blog/BlogController")
Modules = require "./infrastructure/Modules"
RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
CooldownMiddlewear = require('./Features/Cooldown/CooldownMiddlewear')
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
InactiveProjectController = require("./Features/InactiveData/InactiveProjectController")
ContactRouter = require("./Features/Contacts/ContactRouter")

View file

@ -58,6 +58,16 @@ module.exports = settings =
# {host: 'localhost', port: 7005}
# ]
# cooldown:
# cluster: [
# {host: 'localhost', port: 7000}
# {host: 'localhost', port: 7001}
# {host: 'localhost', port: 7002}
# {host: 'localhost', port: 7003}
# {host: 'localhost', port: 7004}
# {host: 'localhost', port: 7005}
# ]
api:
host: "localhost"
port: "6379"

View file

@ -0,0 +1,120 @@
SandboxedModule = require('sandboxed-module')
sinon = require('sinon')
require('chai').should()
expect = require('chai').expect
modulePath = require('path').join __dirname, '../../../../app/js/Features/Cooldown/CooldownManager'
describe "CooldownManager", ->
beforeEach ->
@projectId = 'abcdefg'
@rclient = {set: sinon.stub(), get: sinon.stub()}
@RedisWrapper =
client: () => @rclient
@CooldownManager = SandboxedModule.require modulePath, requires:
'../../infrastructure/RedisWrapper': @RedisWrapper
'logger-sharelatex': {log: sinon.stub()}
describe '_buildKey', ->
it 'should build a properly formatted redis key', ->
expect(@CooldownManager._buildKey('ABC')).to.equal 'Cooldown:{ABC}'
describe 'isProjectOnCooldown', ->
beforeEach ->
@call = (cb) =>
@CooldownManager.isProjectOnCooldown @projectId, cb
describe 'when project is on cooldown', ->
beforeEach ->
@rclient.get = sinon.stub().callsArgWith(1, null, '1')
it 'should fetch key from redis', (done) ->
@call (err, result) =>
@rclient.get.callCount.should.equal 1
@rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true
done()
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.equal null
done()
it 'should produce a true result', (done) ->
@call (err, result) =>
expect(result).to.equal true
done()
describe 'when project is not on cooldown', ->
beforeEach ->
@rclient.get = sinon.stub().callsArgWith(1, null, null)
it 'should fetch key from redis', (done) ->
@call (err, result) =>
@rclient.get.callCount.should.equal 1
@rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true
done()
it 'should not produce an error', (done) ->
@call (err, result) =>
expect(err).to.equal null
done()
it 'should produce a false result', (done) ->
@call (err, result) =>
expect(result).to.equal false
done()
describe 'when rclient.get produces an error', ->
beforeEach ->
@rclient.get = sinon.stub().callsArgWith(1, new Error('woops'))
it 'should fetch key from redis', (done) ->
@call (err, result) =>
@rclient.get.callCount.should.equal 1
@rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true
done()
it 'should produce an error', (done) ->
@call (err, result) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
done()
describe 'putProjectOnCooldown', ->
beforeEach ->
@call = (cb) =>
@CooldownManager.putProjectOnCooldown @projectId, cb
describe 'when rclient.set does not produce an error', ->
beforeEach ->
@rclient.set = sinon.stub().callsArgWith(4, null)
it 'should set a key in redis', (done) ->
@call (err) =>
@rclient.set.callCount.should.equal 1
@rclient.set.calledWith('Cooldown:{abcdefg}').should.equal true
done()
it 'should not produce an error', (done) ->
@call (err) =>
expect(err).to.equal null
done()
describe 'when rclient.set produces an error', ->
beforeEach ->
@rclient.set = sinon.stub().callsArgWith(4, new Error('woops'))
it 'should set a key in redis', (done) ->
@call (err) =>
@rclient.set.callCount.should.equal 1
@rclient.set.calledWith('Cooldown:{abcdefg}').should.equal true
done()
it 'produce an error', (done) ->
@call (err) =>
expect(err).to.not.equal null
expect(err).to.be.instanceof Error
done()

View file

@ -0,0 +1,72 @@
SandboxedModule = require('sandboxed-module')
sinon = require('sinon')
require('chai').should()
expect = require('chai').expect
modulePath = require('path').join __dirname, '../../../../app/js/Features/Cooldown/CooldownMiddlewear'
describe "CooldownMiddlewear", ->
beforeEach ->
@CooldownManager =
isProjectOnCooldown: sinon.stub()
@CooldownMiddlewear = SandboxedModule.require modulePath, requires:
'./CooldownManager': @CooldownManager
'logger-sharelatex': {log: sinon.stub()}
describe 'freezeProject', ->
describe 'when project is on cooldown', ->
beforeEach ->
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, true)
@req = {params: {Project_id: 'abc'}}
@res = {sendStatus: sinon.stub()}
@next = sinon.stub()
it 'should call CooldownManager.isProjectOnCooldown', ->
@CooldownMiddlewear.freezeProject @req, @res, @next
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
@CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true
it 'should not produce an error', ->
@CooldownMiddlewear.freezeProject @req, @res, @next
@next.callCount.should.equal 0
it 'should send a 429 status', ->
@CooldownMiddlewear.freezeProject @req, @res, @next
@res.sendStatus.callCount.should.equal 1
@res.sendStatus.calledWith(429).should.equal true
describe 'when project is not on cooldown', ->
beforeEach ->
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, false)
@req = {params: {Project_id: 'abc'}}
@res = {sendStatus: sinon.stub()}
@next = sinon.stub()
it 'should call CooldownManager.isProjectOnCooldown', ->
@CooldownMiddlewear.freezeProject @req, @res, @next
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
@CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true
it 'call next with no arguments', ->
@CooldownMiddlewear.freezeProject @req, @res, @next
@next.callCount.should.equal 1
expect(@next.lastCall.args.length).to.equal 0
describe 'when isProjectOnCooldown produces an error', ->
beforeEach ->
@CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, new Error('woops'))
@req = {params: {Project_id: 'abc'}}
@res = {sendStatus: sinon.stub()}
@next = sinon.stub()
it 'should call CooldownManager.isProjectOnCooldown', ->
@CooldownMiddlewear.freezeProject @req, @res, @next
@CooldownManager.isProjectOnCooldown.callCount.should.equal 1
@CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true
it 'call next with an error', ->
@CooldownMiddlewear.freezeProject @req, @res, @next
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error