mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Add a cooldown mechanism for projects which go over limits
This commit is contained in:
parent
c5c0364d49
commit
149e38855f
6 changed files with 242 additions and 0 deletions
|
@ -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")
|
||||
|
|
@ -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()
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
|
@ -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
|
Loading…
Reference in a new issue