mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Add in an absolute size limit on the ranges JSON object
This commit is contained in:
parent
f211c282b2
commit
f544814dda
3 changed files with 113 additions and 41 deletions
|
@ -22,6 +22,9 @@ logHashErrors = Settings.documentupdater?.logHashErrors
|
||||||
logHashReadErrors = logHashErrors?.read
|
logHashReadErrors = logHashErrors?.read
|
||||||
logHashWriteErrors = logHashErrors?.write
|
logHashWriteErrors = logHashErrors?.write
|
||||||
|
|
||||||
|
MEGABYTES = 1024 * 1024
|
||||||
|
MAX_RANGES_SIZE = 3 * MEGABYTES
|
||||||
|
|
||||||
module.exports = RedisManager =
|
module.exports = RedisManager =
|
||||||
rclient: rclient
|
rclient: rclient
|
||||||
|
|
||||||
|
@ -37,7 +40,11 @@ module.exports = RedisManager =
|
||||||
return callback(error)
|
return callback(error)
|
||||||
docHash = RedisManager._computeHash(docLines)
|
docHash = RedisManager._computeHash(docLines)
|
||||||
logger.log project_id:project_id, doc_id:doc_id, version: version, hash:docHash, "putting doc in redis"
|
logger.log project_id:project_id, doc_id:doc_id, version: version, hash:docHash, "putting doc in redis"
|
||||||
ranges = RedisManager._serializeRanges(ranges)
|
|
||||||
|
RedisManager._serializeRanges ranges, (error, ranges) ->
|
||||||
|
if error?
|
||||||
|
logger.error {err: error, doc_id, project_id}, error.message
|
||||||
|
return callback(error)
|
||||||
multi = rclient.multi()
|
multi = rclient.multi()
|
||||||
multi.eval setScript, 1, keys.docLines(doc_id:doc_id), docLines
|
multi.eval setScript, 1, keys.docLines(doc_id:doc_id), docLines
|
||||||
multi.set keys.projectKey({doc_id:doc_id}), project_id
|
multi.set keys.projectKey({doc_id:doc_id}), project_id
|
||||||
|
@ -163,6 +170,10 @@ module.exports = RedisManager =
|
||||||
|
|
||||||
logger.log doc_id: doc_id, version: newVersion, hash: newHash, "updating doc in redis"
|
logger.log doc_id: doc_id, version: newVersion, hash: newHash, "updating doc in redis"
|
||||||
|
|
||||||
|
RedisManager._serializeRanges ranges, (error, ranges) ->
|
||||||
|
if error?
|
||||||
|
logger.error {err: error, doc_id}, error.message
|
||||||
|
return callback(error)
|
||||||
multi = rclient.multi()
|
multi = rclient.multi()
|
||||||
multi.eval setScript, 1, keys.docLines(doc_id:doc_id), newDocLines
|
multi.eval setScript, 1, keys.docLines(doc_id:doc_id), newDocLines
|
||||||
multi.set keys.docVersion(doc_id:doc_id), newVersion
|
multi.set keys.docVersion(doc_id:doc_id), newVersion
|
||||||
|
@ -171,7 +182,6 @@ module.exports = RedisManager =
|
||||||
multi.rpush keys.docOps(doc_id: doc_id), jsonOps...
|
multi.rpush keys.docOps(doc_id: doc_id), jsonOps...
|
||||||
multi.expire keys.docOps(doc_id: doc_id), RedisManager.DOC_OPS_TTL
|
multi.expire keys.docOps(doc_id: doc_id), RedisManager.DOC_OPS_TTL
|
||||||
multi.ltrim keys.docOps(doc_id: doc_id), -RedisManager.DOC_OPS_MAX_LENGTH, -1
|
multi.ltrim keys.docOps(doc_id: doc_id), -RedisManager.DOC_OPS_MAX_LENGTH, -1
|
||||||
ranges = RedisManager._serializeRanges(ranges)
|
|
||||||
if ranges?
|
if ranges?
|
||||||
multi.set keys.ranges(doc_id:doc_id), ranges
|
multi.set keys.ranges(doc_id:doc_id), ranges
|
||||||
else
|
else
|
||||||
|
@ -187,12 +197,14 @@ module.exports = RedisManager =
|
||||||
getDocIdsInProject: (project_id, callback = (error, doc_ids) ->) ->
|
getDocIdsInProject: (project_id, callback = (error, doc_ids) ->) ->
|
||||||
rclient.smembers keys.docsInProject(project_id: project_id), callback
|
rclient.smembers keys.docsInProject(project_id: project_id), callback
|
||||||
|
|
||||||
_serializeRanges: (ranges) ->
|
_serializeRanges: (ranges, callback = (error, serializedRanges) ->) ->
|
||||||
jsonRanges = JSON.stringify(ranges)
|
jsonRanges = JSON.stringify(ranges)
|
||||||
|
if jsonRanges? and jsonRanges.length > MAX_RANGES_SIZE
|
||||||
|
return callback new Error("ranges are too large")
|
||||||
if jsonRanges == '{}'
|
if jsonRanges == '{}'
|
||||||
# Most doc will have empty ranges so don't fill redis with lots of '{}' keys
|
# Most doc will have empty ranges so don't fill redis with lots of '{}' keys
|
||||||
jsonRanges = null
|
jsonRanges = null
|
||||||
return jsonRanges
|
return callback null, jsonRanges
|
||||||
|
|
||||||
_deserializeRanges: (ranges) ->
|
_deserializeRanges: (ranges) ->
|
||||||
if !ranges? or ranges == ""
|
if !ranges? or ranges == ""
|
||||||
|
|
|
@ -265,3 +265,40 @@ describe "Ranges", ->
|
||||||
throw error if error?
|
throw error if error?
|
||||||
expect(data.ranges.comments).to.be.undefined
|
expect(data.ranges.comments).to.be.undefined
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
describe "tripping range size limit", ->
|
||||||
|
before (done) ->
|
||||||
|
@project_id = DocUpdaterClient.randomId()
|
||||||
|
@user_id = DocUpdaterClient.randomId()
|
||||||
|
@id_seed = DocUpdaterClient.randomId()
|
||||||
|
@doc = {
|
||||||
|
id: DocUpdaterClient.randomId()
|
||||||
|
lines: ["aaa"]
|
||||||
|
}
|
||||||
|
@i = new Array(3 * 1024 * 1024).join("a")
|
||||||
|
@updates = [{
|
||||||
|
doc: @doc.id
|
||||||
|
op: [{ i: @i, p: 1 }]
|
||||||
|
v: 0
|
||||||
|
meta: { user_id: @user_id, tc: @id_seed }
|
||||||
|
}]
|
||||||
|
MockWebApi.insertDoc @project_id, @doc.id, {
|
||||||
|
lines: @doc.lines
|
||||||
|
version: 0
|
||||||
|
}
|
||||||
|
jobs = []
|
||||||
|
for update in @updates
|
||||||
|
do (update) =>
|
||||||
|
jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
|
||||||
|
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
|
||||||
|
throw error if error?
|
||||||
|
async.series jobs, (error) ->
|
||||||
|
throw error if error?
|
||||||
|
setTimeout done, 200
|
||||||
|
|
||||||
|
it "should not update the ranges", (done) ->
|
||||||
|
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
|
||||||
|
throw error if error?
|
||||||
|
ranges = data.ranges
|
||||||
|
expect(ranges.changes).to.be.undefined
|
||||||
|
done()
|
|
@ -338,6 +338,18 @@ describe "RedisManager", ->
|
||||||
it "should call the callback with an error", ->
|
it "should call the callback with an error", ->
|
||||||
@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
|
@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
|
||||||
|
|
||||||
|
describe "with ranges that are too big", ->
|
||||||
|
beforeEach ->
|
||||||
|
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
|
||||||
|
@RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"))
|
||||||
|
@RedisManager.updateDocument @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||||
|
|
||||||
|
it 'should log an error', ->
|
||||||
|
@logger.error.called.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the error", ->
|
||||||
|
@callback.calledWith(new Error("ranges are too large")).should.equal true
|
||||||
|
|
||||||
describe "putDocInMemory", ->
|
describe "putDocInMemory", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@rclient.set = sinon.stub()
|
@rclient.set = sinon.stub()
|
||||||
|
@ -426,6 +438,17 @@ describe "RedisManager", ->
|
||||||
it "should call the callback with an error", ->
|
it "should call the callback with an error", ->
|
||||||
@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
|
@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
|
||||||
|
|
||||||
|
describe "with ranges that are too big", ->
|
||||||
|
beforeEach ->
|
||||||
|
@RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"))
|
||||||
|
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @callback
|
||||||
|
|
||||||
|
it 'should log an error', ->
|
||||||
|
@logger.error.called.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the error", ->
|
||||||
|
@callback.calledWith(new Error("ranges are too large")).should.equal true
|
||||||
|
|
||||||
describe "removeDocFromMemory", ->
|
describe "removeDocFromMemory", ->
|
||||||
beforeEach (done) ->
|
beforeEach (done) ->
|
||||||
@rclient.del = sinon.stub()
|
@rclient.del = sinon.stub()
|
||||||
|
|
Loading…
Reference in a new issue