mirror of
https://github.com/overleaf/overleaf.git
synced 2025-03-22 02:04:31 +00:00
Create a RedisWrapper, and use it for rate limiting.
This commit is contained in:
parent
637fcb5784
commit
ef0a5801d5
5 changed files with 98 additions and 4366 deletions
|
@ -1,15 +1,28 @@
|
|||
settings = require("settings-sharelatex")
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(settings.redis.web)
|
||||
redback = require("redback").use(rclient)
|
||||
RedisWrapper = require('./RedisWrapper')
|
||||
rclient = RedisWrapper.client('ratelimiter')
|
||||
|
||||
module.exports =
|
||||
|
||||
module.exports = RateLimiter =
|
||||
|
||||
_buildKey: (endpoint, subject) ->
|
||||
return "RateLimiter:#{endpoint}:{#{subject}}"
|
||||
|
||||
addCount: (opts, callback = (opts, shouldProcess)->)->
|
||||
ratelimit = redback.createRateLimit(opts.endpointName)
|
||||
ratelimit.addCount opts.subjectName, opts.timeInterval, (err, callCount)->
|
||||
shouldProcess = callCount < opts.throttle
|
||||
callback(err, shouldProcess)
|
||||
|
||||
k = RateLimiter._buildKey(opts.endpointName, opts.subjectName)
|
||||
multi = rclient.multi()
|
||||
multi.incr(k)
|
||||
multi.get(k)
|
||||
multi.expire(k, opts.timeInterval)
|
||||
multi.exec (err, results)->
|
||||
console.log ">> results", results
|
||||
count = results[1]
|
||||
# account for the different results from `multi` when using cluster
|
||||
if count instanceof Object
|
||||
count = count[1]
|
||||
allow = count < opts.throttle
|
||||
callback err, allow
|
||||
|
||||
clearRateLimit: (endpointName, subject, callback) ->
|
||||
rclient.del "#{endpointName}:#{subject}", callback
|
||||
k = RateLimiter._buildKey(endpointName, subject)
|
||||
rclient.del k, callback
|
||||
|
|
28
services/web/app/coffee/infrastructure/RedisWrapper.coffee
Normal file
28
services/web/app/coffee/infrastructure/RedisWrapper.coffee
Normal file
|
@ -0,0 +1,28 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
redis = require 'redis-sharelatex'
|
||||
ioredis = require 'ioredis'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
|
||||
# A per-feature interface to Redis,
|
||||
# looks up the feature in `settings.redis`
|
||||
# and returns an appropriate client.
|
||||
# Necessary because we don't want to migrate web over
|
||||
# to redis-cluster all at once.
|
||||
|
||||
# TODO: consider merging into `redis-sharelatex`
|
||||
|
||||
|
||||
module.exports = Redis =
|
||||
|
||||
# feature = 'websessions' | 'ratelimiter' | ...
|
||||
client: (feature) ->
|
||||
redisFeatureSettings = Settings.redis[feature] or Settings.redis.web
|
||||
if redisFeatureSettings?.cluster?
|
||||
logger.log {feature}, "creating redis-cluster client"
|
||||
rclient = new ioredis.Cluster(redisFeatureSettings.cluster)
|
||||
rclient._is_redis_cluster = true
|
||||
else
|
||||
logger.log {feature}, "creating redis client"
|
||||
rclient = redis.createClient(redisFeatureSettings)
|
||||
return rclient
|
4311
services/web/npm-shrinkwrap.json
generated
4311
services/web/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load diff
|
@ -50,7 +50,6 @@
|
|||
"passport": "^0.3.2",
|
||||
"passport-ldapauth": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"redback": "0.4.0",
|
||||
"redis": "0.10.1",
|
||||
"redis-sharelatex": "0.0.9",
|
||||
"request": "^2.69.0",
|
||||
|
|
|
@ -6,7 +6,7 @@ expect = chai.expect
|
|||
modulePath = "../../../../app/js/infrastructure/RateLimiter.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "FileStoreHandler", ->
|
||||
describe "RateLimiter", ->
|
||||
|
||||
beforeEach ->
|
||||
@settings =
|
||||
|
@ -15,23 +15,22 @@ describe "FileStoreHandler", ->
|
|||
port:"1234"
|
||||
host:"somewhere"
|
||||
password: "password"
|
||||
@redbackInstance =
|
||||
addCount: sinon.stub()
|
||||
|
||||
@redback =
|
||||
createRateLimit: sinon.stub().returns(@redbackInstance)
|
||||
@redis =
|
||||
createClient: ->
|
||||
return auth:->
|
||||
@rclient =
|
||||
incr: sinon.stub()
|
||||
get: sinon.stub()
|
||||
expire: sinon.stub()
|
||||
exec: sinon.stub()
|
||||
@rclient.multi = sinon.stub().returns(@rclient)
|
||||
@RedisWrapper =
|
||||
client: sinon.stub().returns(@rclient)
|
||||
|
||||
@limiter = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex":@settings
|
||||
"logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()}
|
||||
"redis-sharelatex": @redis
|
||||
"redback": use: => @redback
|
||||
"./RedisWrapper": @RedisWrapper
|
||||
|
||||
@endpointName = "compiles"
|
||||
@subject = "some project id"
|
||||
@subject = "some-project-id"
|
||||
@timeInterval = 20
|
||||
@throttleLimit = 5
|
||||
|
||||
|
@ -40,43 +39,47 @@ describe "FileStoreHandler", ->
|
|||
subjectName: @subject
|
||||
throttle: @throttleLimit
|
||||
timeInterval: @timeInterval
|
||||
@key = "RateLimiter:#{@endpointName}:{#{@subject}}"
|
||||
|
||||
for redisType, resultSet of {
|
||||
normal:[10, '10', 10],
|
||||
cluster:[[null,10], [null,'10'], [null,10]]
|
||||
}
|
||||
do (redisType, resultSet) ->
|
||||
|
||||
describe "addCount", ->
|
||||
describe "addCount with #{redisType} redis", ->
|
||||
|
||||
beforeEach ->
|
||||
@redbackInstance.addCount.callsArgWith(2, null, 10)
|
||||
beforeEach ->
|
||||
@results = resultSet
|
||||
@rclient.incr = sinon.stub()
|
||||
@rclient.get = sinon.stub()
|
||||
@rclient.expire = sinon.stub()
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, @results)
|
||||
|
||||
it "should use correct namespace", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@redback.createRateLimit.calledWith(@endpointName).should.equal true
|
||||
done()
|
||||
it "should use correct key", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@rclient.incr.calledWith(@key).should.equal true
|
||||
done()
|
||||
|
||||
it "should only call it once", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@redbackInstance.addCount.callCount.should.equal 1
|
||||
done()
|
||||
it "should only call it once", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@rclient.exec.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it "should use the subjectName", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@redbackInstance.addCount.calledWith(@details.subjectName, @details.timeInterval).should.equal true
|
||||
done()
|
||||
it "should return true if the count is less than throttle", (done)->
|
||||
@details.throttle = 100
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal true
|
||||
done()
|
||||
|
||||
it "should return true if the count is less than throttle", (done)->
|
||||
@details.throttle = 100
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal true
|
||||
done()
|
||||
|
||||
it "should return true if the count is less than throttle", (done)->
|
||||
@details.throttle = 1
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal false
|
||||
done()
|
||||
|
||||
it "should return false if the limit is matched", (done)->
|
||||
@details.throttle = 10
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal false
|
||||
done()
|
||||
it "should return true if the count is less than throttle", (done)->
|
||||
@details.throttle = 1
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal false
|
||||
done()
|
||||
|
||||
it "should return false if the limit is matched", (done)->
|
||||
@details.throttle = 10
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal false
|
||||
done()
|
||||
|
|
Loading…
Reference in a new issue