mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Use new redis-sharelatex instead of RedisBackend for cluster abstraction
This commit is contained in:
parent
fe5711ef07
commit
f21208e841
11 changed files with 63 additions and 808 deletions
|
@ -8,7 +8,6 @@ if Settings.sentry?.dsn?
|
||||||
|
|
||||||
RedisManager = require('./app/js/RedisManager')
|
RedisManager = require('./app/js/RedisManager')
|
||||||
DispatchManager = require('./app/js/DispatchManager')
|
DispatchManager = require('./app/js/DispatchManager')
|
||||||
Keys = require('./app/js/RedisKeyBuilder')
|
|
||||||
Errors = require "./app/js/Errors"
|
Errors = require "./app/js/Errors"
|
||||||
HttpController = require "./app/js/HttpController"
|
HttpController = require "./app/js/HttpController"
|
||||||
|
|
||||||
|
@ -63,15 +62,18 @@ app.get '/status', (req, res)->
|
||||||
else
|
else
|
||||||
res.send('document updater is alive')
|
res.send('document updater is alive')
|
||||||
|
|
||||||
redisCheck = require("redis-sharelatex").activeHealthCheckRedis(Settings.redis.web)
|
webRedisClient = require("redis-sharelatex").createClient(Settings.redis.web)
|
||||||
app.get "/health_check/redis", (req, res, next)->
|
app.get "/health_check/redis", (req, res, next) ->
|
||||||
if redisCheck.isAlive()
|
webRedisClient.healthCheck (error) ->
|
||||||
res.send 200
|
if error?
|
||||||
else
|
logger.err {err: error}, "failed redis health check"
|
||||||
res.send 500
|
res.send 500
|
||||||
|
else
|
||||||
|
res.send 200
|
||||||
|
|
||||||
|
docUpdaterRedisClient = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
|
||||||
app.get "/health_check/redis_cluster", (req, res, next) ->
|
app.get "/health_check/redis_cluster", (req, res, next) ->
|
||||||
RedisManager.rclient.healthCheck (error, alive) ->
|
docUpdaterRedisClient.healthCheck (error) ->
|
||||||
if error?
|
if error?
|
||||||
logger.err {err: error}, "failed redis cluster health check"
|
logger.err {err: error}, "failed redis cluster health check"
|
||||||
res.send 500
|
res.send 500
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
Settings = require "settings-sharelatex"
|
|
||||||
async = require "async"
|
|
||||||
_ = require "underscore"
|
|
||||||
logger = require "logger-sharelatex"
|
|
||||||
Metrics = require "metrics-sharelatex"
|
|
||||||
|
|
||||||
class Client
|
|
||||||
constructor: (@clients) ->
|
|
||||||
@SECONDARY_TIMEOUT = 600
|
|
||||||
@HEARTBEAT_TIMEOUT = 2000
|
|
||||||
|
|
||||||
multi: () ->
|
|
||||||
return new MultiClient(
|
|
||||||
@clients.map (client) -> {
|
|
||||||
rclient: client.rclient.multi()
|
|
||||||
key_schema: client.key_schema
|
|
||||||
primary: client.primary
|
|
||||||
driver: client.driver
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
healthCheck: (callback) ->
|
|
||||||
jobs = @clients.map (client) =>
|
|
||||||
(cb) => @_healthCheckClient(client, cb)
|
|
||||||
async.parallel jobs, callback
|
|
||||||
|
|
||||||
_healthCheckClient: (client, callback) ->
|
|
||||||
if client.driver == "ioredis"
|
|
||||||
@_healthCheckClusterClient(client, callback)
|
|
||||||
else
|
|
||||||
@_healthCheckNodeRedisClient(client, callback)
|
|
||||||
|
|
||||||
_healthCheckNodeRedisClient: (client, callback) ->
|
|
||||||
client.healthCheck ?= require("redis-sharelatex").activeHealthCheckRedis(Settings.redis.web)
|
|
||||||
if client.healthCheck.isAlive()
|
|
||||||
return callback()
|
|
||||||
else
|
|
||||||
return callback(new Error("node-redis client failed health check"))
|
|
||||||
|
|
||||||
_healthCheckClusterClient: (client, callback) ->
|
|
||||||
jobs = client.rclient.nodes("all").map (n) =>
|
|
||||||
(cb) => @_checkNode(n, cb)
|
|
||||||
async.parallel jobs, callback
|
|
||||||
|
|
||||||
_checkNode: (node, _callback) ->
|
|
||||||
callback = (args...) ->
|
|
||||||
_callback(args...)
|
|
||||||
_callback = () ->
|
|
||||||
timer = setTimeout () ->
|
|
||||||
error = new Error("ioredis node ping check timed out")
|
|
||||||
logger.error {err: error, key: node.options.key}, "node timed out"
|
|
||||||
callback(error)
|
|
||||||
, @HEARTBEAT_TIMEOUT
|
|
||||||
node.ping (err) ->
|
|
||||||
clearTimeout timer
|
|
||||||
callback(err)
|
|
||||||
|
|
||||||
class MultiClient
|
|
||||||
constructor: (@clients) ->
|
|
||||||
@SECONDARY_TIMEOUT = 600
|
|
||||||
|
|
||||||
exec: (callback) ->
|
|
||||||
primaryError = null
|
|
||||||
primaryResult = null
|
|
||||||
jobs = @clients.map (client) =>
|
|
||||||
(cb) =>
|
|
||||||
cb = _.once(cb)
|
|
||||||
timer = new Metrics.Timer("redis.#{client.driver}.exec")
|
|
||||||
|
|
||||||
timeout = null
|
|
||||||
if !client.primary
|
|
||||||
timeout = setTimeout () ->
|
|
||||||
logger.error {err: new Error("#{client.driver} backend timed out")}, "backend timed out"
|
|
||||||
cb()
|
|
||||||
, @SECONDARY_TIMEOUT
|
|
||||||
|
|
||||||
client.rclient.exec (error, result) =>
|
|
||||||
timer.done()
|
|
||||||
if client.driver == "ioredis"
|
|
||||||
# ioredis returns an results like:
|
|
||||||
# [ [null, 42], [null, "foo"] ]
|
|
||||||
# where the first entries in each 2-tuple are
|
|
||||||
# presumably errors for each individual command,
|
|
||||||
# and the second entry is the result. We need to transform
|
|
||||||
# this into the same result as the old redis driver:
|
|
||||||
# [ 42, "foo" ]
|
|
||||||
filtered_result = []
|
|
||||||
for entry in result or []
|
|
||||||
if entry[0]?
|
|
||||||
return cb(entry[0])
|
|
||||||
else
|
|
||||||
filtered_result.push entry[1]
|
|
||||||
result = filtered_result
|
|
||||||
|
|
||||||
if client.primary
|
|
||||||
primaryError = error
|
|
||||||
primaryResult = result
|
|
||||||
if timeout?
|
|
||||||
clearTimeout(timeout)
|
|
||||||
cb(error, result)
|
|
||||||
async.parallel jobs, (error, results) ->
|
|
||||||
if error?
|
|
||||||
# suppress logging of errors
|
|
||||||
# logger.error {err: error}, "error in redis backend"
|
|
||||||
else
|
|
||||||
compareResults(results, "exec")
|
|
||||||
callback(primaryError, primaryResult)
|
|
||||||
|
|
||||||
COMMANDS = {
|
|
||||||
"get": 0,
|
|
||||||
"smembers": 0,
|
|
||||||
"set": 0,
|
|
||||||
"srem": 0,
|
|
||||||
"sadd": 0,
|
|
||||||
"del": 0,
|
|
||||||
"lrange": 0,
|
|
||||||
"llen": 0,
|
|
||||||
"rpush": 0,
|
|
||||||
"expire": 0,
|
|
||||||
"ltrim": 0,
|
|
||||||
"incr": 0,
|
|
||||||
"eval": 2
|
|
||||||
}
|
|
||||||
for command, key_pos of COMMANDS
|
|
||||||
do (command, key_pos) ->
|
|
||||||
Client.prototype[command] = (args..., callback) ->
|
|
||||||
primaryError = null
|
|
||||||
primaryResult = []
|
|
||||||
jobs = @clients.map (client) =>
|
|
||||||
(cb) =>
|
|
||||||
cb = _.once(cb)
|
|
||||||
key_builder = args[key_pos]
|
|
||||||
key = key_builder(client.key_schema)
|
|
||||||
args_with_key = args.slice(0)
|
|
||||||
args_with_key[key_pos] = key
|
|
||||||
timer = new Metrics.Timer("redis.#{client.driver}.#{command}")
|
|
||||||
|
|
||||||
timeout = null
|
|
||||||
if !client.primary
|
|
||||||
timeout = setTimeout () ->
|
|
||||||
logger.error {err: new Error("#{client.driver} backend timed out")}, "backend timed out"
|
|
||||||
cb()
|
|
||||||
, @SECONDARY_TIMEOUT
|
|
||||||
|
|
||||||
client.rclient[command] args_with_key..., (error, result...) =>
|
|
||||||
timer.done()
|
|
||||||
if client.primary
|
|
||||||
primaryError = error
|
|
||||||
primaryResult = result
|
|
||||||
if timeout?
|
|
||||||
clearTimeout(timeout)
|
|
||||||
cb(error, result...)
|
|
||||||
async.parallel jobs, (error, results) ->
|
|
||||||
if error?
|
|
||||||
logger.error {err: error}, "error in redis backend"
|
|
||||||
else
|
|
||||||
compareResults(results, command)
|
|
||||||
callback(primaryError, primaryResult...)
|
|
||||||
|
|
||||||
MultiClient.prototype[command] = (args...) ->
|
|
||||||
for client in @clients
|
|
||||||
key_builder = args[key_pos]
|
|
||||||
key = key_builder(client.key_schema)
|
|
||||||
args_with_key = args.slice(0)
|
|
||||||
args_with_key[key_pos] = key
|
|
||||||
client.rclient[command] args_with_key...
|
|
||||||
|
|
||||||
compareResults = (results, command) ->
|
|
||||||
return if results.length < 2
|
|
||||||
first = results[0]
|
|
||||||
if command == "smembers" and first?
|
|
||||||
first = first.slice().sort()
|
|
||||||
for result in results.slice(1)
|
|
||||||
if command == "smembers" and result?
|
|
||||||
result = result.slice().sort()
|
|
||||||
if not _.isEqual(first, result)
|
|
||||||
logger.error results: results, "redis backend conflict"
|
|
||||||
Metrics.inc "backend-conflict"
|
|
||||||
else
|
|
||||||
Metrics.inc "backend-match"
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
createClient: () ->
|
|
||||||
client_configs = Settings.redis.documentupdater
|
|
||||||
unless client_configs instanceof Array
|
|
||||||
client_configs.primary = true
|
|
||||||
client_configs = [client_configs]
|
|
||||||
clients = client_configs.map (config) ->
|
|
||||||
if config.cluster?
|
|
||||||
Redis = require("ioredis")
|
|
||||||
rclient = new Redis.Cluster(config.cluster)
|
|
||||||
driver = "ioredis"
|
|
||||||
else
|
|
||||||
redis_config = {}
|
|
||||||
for key in ["host", "port", "password", "endpoints", "masterName"]
|
|
||||||
if config[key]?
|
|
||||||
redis_config[key] = config[key]
|
|
||||||
rclient = require("redis-sharelatex").createClient(redis_config)
|
|
||||||
driver = "noderedis"
|
|
||||||
return {
|
|
||||||
rclient: rclient
|
|
||||||
key_schema: config.key_schema
|
|
||||||
primary: config.primary
|
|
||||||
driver: driver
|
|
||||||
}
|
|
||||||
return new Client(clients)
|
|
|
@ -1,44 +0,0 @@
|
||||||
# The default key schema looks like:
|
|
||||||
# doclines:foo
|
|
||||||
# DocVersion:foo
|
|
||||||
# but if we use redis cluster, we want all 'foo' keys to map to the same
|
|
||||||
# node, so we must use:
|
|
||||||
# doclines:{foo}
|
|
||||||
# DocVersion:{foo}
|
|
||||||
# since redis hashes on the contents of {...}.
|
|
||||||
#
|
|
||||||
# To transparently support different key schemas for different clients
|
|
||||||
# (potential writing/reading to both a cluster and single instance
|
|
||||||
# while we migrate), instead of keys, we now pass around functions which
|
|
||||||
# will build the key when passed a schema.
|
|
||||||
#
|
|
||||||
# E.g.
|
|
||||||
# key_schema = Settings.redis.keys
|
|
||||||
# key_schema == { docLines: ({doc_id}) -> "doclines:#{doc_id}", ... }
|
|
||||||
# key_builder = RedisKeyBuilder.docLines({doc_id: "foo"})
|
|
||||||
# key_builder == (key_schema) -> key_schema.docLines({doc_id: "foo"})
|
|
||||||
# key = key_builder(key_schema)
|
|
||||||
# key == "doclines:foo"
|
|
||||||
module.exports = RedisKeyBuilder =
|
|
||||||
blockingKey: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.blockingKey({doc_id})
|
|
||||||
docLines: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.docLines({doc_id})
|
|
||||||
docOps: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.docOps({doc_id})
|
|
||||||
docVersion: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.docVersion({doc_id})
|
|
||||||
docHash: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.docHash({doc_id})
|
|
||||||
projectKey: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.projectKey({doc_id})
|
|
||||||
uncompressedHistoryOp: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.uncompressedHistoryOp({doc_id})
|
|
||||||
pendingUpdates: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.pendingUpdates({doc_id})
|
|
||||||
ranges: ({doc_id}) ->
|
|
||||||
return (key_schema) -> key_schema.ranges({doc_id})
|
|
||||||
docsInProject: ({project_id}) ->
|
|
||||||
return (key_schema) -> key_schema.docsInProject({project_id})
|
|
||||||
docsWithHistoryOps: ({project_id}) ->
|
|
||||||
return (key_schema) -> key_schema.docsWithHistoryOps({project_id})
|
|
|
@ -1,8 +1,7 @@
|
||||||
Settings = require('settings-sharelatex')
|
Settings = require('settings-sharelatex')
|
||||||
async = require('async')
|
async = require('async')
|
||||||
rclient = require("./RedisBackend").createClient()
|
rclient = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
|
||||||
_ = require('underscore')
|
_ = require('underscore')
|
||||||
keys = require('./RedisKeyBuilder')
|
|
||||||
logger = require('logger-sharelatex')
|
logger = require('logger-sharelatex')
|
||||||
metrics = require('./Metrics')
|
metrics = require('./Metrics')
|
||||||
Errors = require "./Errors"
|
Errors = require "./Errors"
|
||||||
|
@ -25,6 +24,8 @@ logHashWriteErrors = logHashErrors?.write
|
||||||
MEGABYTES = 1024 * 1024
|
MEGABYTES = 1024 * 1024
|
||||||
MAX_RANGES_SIZE = 3 * MEGABYTES
|
MAX_RANGES_SIZE = 3 * MEGABYTES
|
||||||
|
|
||||||
|
keys = Settings.redis.documentupdater.key_schema
|
||||||
|
|
||||||
module.exports = RedisManager =
|
module.exports = RedisManager =
|
||||||
rclient: rclient
|
rclient: rclient
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,10 @@ module.exports =
|
||||||
port:"6379"
|
port:"6379"
|
||||||
host:"localhost"
|
host:"localhost"
|
||||||
password:""
|
password:""
|
||||||
documentupdater: [{
|
documentupdater:
|
||||||
primary: true
|
port: "6379"
|
||||||
port:"6379"
|
host: "localhost"
|
||||||
host:"localhost"
|
password: ""
|
||||||
password:""
|
|
||||||
key_schema:
|
key_schema:
|
||||||
blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
|
blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
|
||||||
docLines: ({doc_id}) -> "doclines:#{doc_id}"
|
docLines: ({doc_id}) -> "doclines:#{doc_id}"
|
||||||
|
@ -34,20 +33,19 @@ module.exports =
|
||||||
projectKey: ({doc_id}) -> "ProjectId:#{doc_id}"
|
projectKey: ({doc_id}) -> "ProjectId:#{doc_id}"
|
||||||
docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
|
docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
|
||||||
ranges: ({doc_id}) -> "Ranges:#{doc_id}"
|
ranges: ({doc_id}) -> "Ranges:#{doc_id}"
|
||||||
# }, {
|
# cluster: [{
|
||||||
# cluster: [{
|
# port: "7000"
|
||||||
# port: "7000"
|
# host: "localhost"
|
||||||
# host: "localhost"
|
# }]
|
||||||
# }]
|
# key_schema:
|
||||||
# key_schema:
|
# blockingKey: ({doc_id}) -> "Blocking:{#{doc_id}}"
|
||||||
# blockingKey: ({doc_id}) -> "Blocking:{#{doc_id}}"
|
# docLines: ({doc_id}) -> "doclines:{#{doc_id}}"
|
||||||
# docLines: ({doc_id}) -> "doclines:{#{doc_id}}"
|
# docOps: ({doc_id}) -> "DocOps:{#{doc_id}}"
|
||||||
# docOps: ({doc_id}) -> "DocOps:{#{doc_id}}"
|
# docVersion: ({doc_id}) -> "DocVersion:{#{doc_id}}"
|
||||||
# docVersion: ({doc_id}) -> "DocVersion:{#{doc_id}}"
|
# docHash: ({doc_id}) -> "DocHash:{#{doc_id}}"
|
||||||
# projectKey: ({doc_id}) -> "ProjectId:{#{doc_id}}"
|
# projectKey: ({doc_id}) -> "ProjectId:{#{doc_id}}"
|
||||||
# docsInProject: ({project_id}) -> "DocsIn:{#{project_id}}"
|
# docsInProject: ({project_id}) -> "DocsIn:{#{project_id}}"
|
||||||
# ranges: ({doc_id}) -> "Ranges:{#{doc_id}}"
|
# ranges: ({doc_id}) -> "Ranges:{#{doc_id}}"
|
||||||
}]
|
|
||||||
|
|
||||||
max_doc_length: 2 * 1024 * 1024 # 2mb
|
max_doc_length: 2 * 1024 * 1024 # 2mb
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.6",
|
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.5.6",
|
||||||
"lynx": "0.0.11",
|
"lynx": "0.0.11",
|
||||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0",
|
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.5.0",
|
||||||
"redis-sharelatex": "0.0.9",
|
"redis-sharelatex": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.0",
|
||||||
"request": "2.25.0",
|
"request": "2.25.0",
|
||||||
"sandboxed-module": "~0.2.0",
|
"sandboxed-module": "~0.2.0",
|
||||||
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
|
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
|
||||||
|
|
|
@ -4,7 +4,9 @@ chai.should()
|
||||||
expect = chai.expect
|
expect = chai.expect
|
||||||
async = require "async"
|
async = require "async"
|
||||||
Settings = require('settings-sharelatex')
|
Settings = require('settings-sharelatex')
|
||||||
rclient = require("redis-sharelatex").createClient(Settings.redis.web)
|
rclient_web = require("redis-sharelatex").createClient(Settings.redis.web)
|
||||||
|
rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
|
||||||
|
Keys = Settings.redis.documentupdater.key_schema
|
||||||
|
|
||||||
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
||||||
MockWebApi = require "./helpers/MockWebApi"
|
MockWebApi = require "./helpers/MockWebApi"
|
||||||
|
@ -47,10 +49,10 @@ describe "Applying updates to a doc", ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should push the applied updates to the track changes api", (done) ->
|
it "should push the applied updates to the track changes api", (done) ->
|
||||||
rclient.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
|
rclient_web.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
JSON.parse(updates[0]).op.should.deep.equal @update.op
|
JSON.parse(updates[0]).op.should.deep.equal @update.op
|
||||||
rclient.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
|
rclient_web.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
result.should.equal 1
|
result.should.equal 1
|
||||||
done()
|
done()
|
||||||
|
@ -80,9 +82,9 @@ describe "Applying updates to a doc", ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should push the applied updates to the track changes api", (done) ->
|
it "should push the applied updates to the track changes api", (done) ->
|
||||||
rclient.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
|
rclient_web.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
|
||||||
JSON.parse(updates[0]).op.should.deep.equal @update.op
|
JSON.parse(updates[0]).op.should.deep.equal @update.op
|
||||||
rclient.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
|
rclient_web.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
|
||||||
result.should.equal 1
|
result.should.equal 1
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
@ -125,17 +127,17 @@ describe "Applying updates to a doc", ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should push the applied updates to the track changes api", (done) ->
|
it "should push the applied updates to the track changes api", (done) ->
|
||||||
rclient.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
|
rclient_web.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
|
||||||
updates = (JSON.parse(u) for u in updates)
|
updates = (JSON.parse(u) for u in updates)
|
||||||
for appliedUpdate, i in @updates
|
for appliedUpdate, i in @updates
|
||||||
appliedUpdate.op.should.deep.equal updates[i].op
|
appliedUpdate.op.should.deep.equal updates[i].op
|
||||||
|
|
||||||
rclient.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
|
rclient_web.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
|
||||||
result.should.equal 1
|
result.should.equal 1
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should store the doc ops in the correct order", (done) ->
|
it "should store the doc ops in the correct order", (done) ->
|
||||||
rclient.lrange "DocOps:#{@doc_id}", 0, -1, (error, updates) =>
|
rclient_du.lrange Keys.docOps({doc_id: @doc_id}), 0, -1, (error, updates) =>
|
||||||
updates = (JSON.parse(u) for u in updates)
|
updates = (JSON.parse(u) for u in updates)
|
||||||
for appliedUpdate, i in @updates
|
for appliedUpdate, i in @updates
|
||||||
appliedUpdate.op.should.deep.equal updates[i].op
|
appliedUpdate.op.should.deep.equal updates[i].op
|
||||||
|
|
|
@ -3,7 +3,8 @@ chai = require("chai")
|
||||||
chai.should()
|
chai.should()
|
||||||
expect = require("chai").expect
|
expect = require("chai").expect
|
||||||
Settings = require('settings-sharelatex')
|
Settings = require('settings-sharelatex')
|
||||||
rclient = require("redis-sharelatex").createClient(Settings.redis.web)
|
rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
|
||||||
|
Keys = Settings.redis.documentupdater.key_schema
|
||||||
|
|
||||||
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
||||||
MockWebApi = require "./helpers/MockWebApi"
|
MockWebApi = require "./helpers/MockWebApi"
|
||||||
|
@ -65,7 +66,7 @@ describe "Setting a document", ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should leave the document in redis", (done) ->
|
it "should leave the document in redis", (done) ->
|
||||||
rclient.get "doclines:#{@doc_id}", (error, lines) =>
|
rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
expect(JSON.parse(lines)).to.deep.equal @newLines
|
expect(JSON.parse(lines)).to.deep.equal @newLines
|
||||||
done()
|
done()
|
||||||
|
@ -90,7 +91,7 @@ describe "Setting a document", ->
|
||||||
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
|
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
|
||||||
|
|
||||||
it "should remove the document from redis", (done) ->
|
it "should remove the document from redis", (done) ->
|
||||||
rclient.get "doclines:#{@doc_id}", (error, lines) =>
|
rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
expect(lines).to.not.exist
|
expect(lines).to.not.exist
|
||||||
done()
|
done()
|
||||||
|
|
|
@ -1,504 +0,0 @@
|
||||||
sinon = require('sinon')
|
|
||||||
chai = require('chai')
|
|
||||||
should = chai.should()
|
|
||||||
modulePath = "../../../../app/js/RedisBackend.js"
|
|
||||||
SandboxedModule = require('sandboxed-module')
|
|
||||||
RedisKeyBuilder = require "../../../../app/js/RedisKeyBuilder"
|
|
||||||
|
|
||||||
describe "RedisBackend", ->
|
|
||||||
beforeEach ->
|
|
||||||
@Settings =
|
|
||||||
redis:
|
|
||||||
documentupdater: [{
|
|
||||||
primary: true
|
|
||||||
port: "6379"
|
|
||||||
host: "localhost"
|
|
||||||
password: "single-password"
|
|
||||||
key_schema:
|
|
||||||
blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
|
|
||||||
docLines: ({doc_id}) -> "doclines:#{doc_id}"
|
|
||||||
docOps: ({doc_id}) -> "DocOps:#{doc_id}"
|
|
||||||
docVersion: ({doc_id}) -> "DocVersion:#{doc_id}"
|
|
||||||
docHash: ({doc_id}) -> "DocHash:#{doc_id}"
|
|
||||||
projectKey: ({doc_id}) -> "ProjectId:#{doc_id}"
|
|
||||||
pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
|
|
||||||
docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
|
|
||||||
}, {
|
|
||||||
cluster: [{
|
|
||||||
port: "7000"
|
|
||||||
host: "localhost"
|
|
||||||
}]
|
|
||||||
password: "cluster-password"
|
|
||||||
key_schema:
|
|
||||||
blockingKey: ({doc_id}) -> "Blocking:{#{doc_id}}"
|
|
||||||
docLines: ({doc_id}) -> "doclines:{#{doc_id}}"
|
|
||||||
docOps: ({doc_id}) -> "DocOps:{#{doc_id}}"
|
|
||||||
docVersion: ({doc_id}) -> "DocVersion:{#{doc_id}}"
|
|
||||||
docHash: ({doc_id}) -> "DocHash:{#{doc_id}}"
|
|
||||||
projectKey: ({doc_id}) -> "ProjectId:{#{doc_id}}"
|
|
||||||
pendingUpdates: ({doc_id}) -> "PendingUpdates:{#{doc_id}}"
|
|
||||||
docsInProject: ({project_id}) -> "DocsIn:{#{project_id}}"
|
|
||||||
}]
|
|
||||||
|
|
||||||
test_context = @
|
|
||||||
class Cluster
|
|
||||||
constructor: (@config) ->
|
|
||||||
test_context.rclient_ioredis = @
|
|
||||||
|
|
||||||
nodes: sinon.stub()
|
|
||||||
|
|
||||||
@timer = timer = sinon.stub()
|
|
||||||
class Timer
|
|
||||||
constructor: (args...) -> timer(args...)
|
|
||||||
done: () ->
|
|
||||||
|
|
||||||
@RedisBackend = SandboxedModule.require modulePath, requires:
|
|
||||||
"settings-sharelatex": @Settings
|
|
||||||
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
|
|
||||||
"redis-sharelatex": @redis =
|
|
||||||
createClient: sinon.stub().returns @rclient_redis = {}
|
|
||||||
activeHealthCheck: sinon.stub()
|
|
||||||
"ioredis": @ioredis =
|
|
||||||
Cluster: Cluster
|
|
||||||
"metrics-sharelatex":
|
|
||||||
@Metrics =
|
|
||||||
inc: sinon.stub()
|
|
||||||
Timer: Timer
|
|
||||||
|
|
||||||
@client = @RedisBackend.createClient()
|
|
||||||
|
|
||||||
@doc_id = "mock-doc-id"
|
|
||||||
@project_id = "mock-project-id"
|
|
||||||
|
|
||||||
it "should create a redis client", ->
|
|
||||||
@redis.createClient
|
|
||||||
.calledWith({
|
|
||||||
port: "6379"
|
|
||||||
host: "localhost"
|
|
||||||
password: "single-password"
|
|
||||||
})
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should create an ioredis cluster client", ->
|
|
||||||
@rclient_ioredis.config.should.deep.equal [{
|
|
||||||
port: "7000"
|
|
||||||
host: "localhost"
|
|
||||||
}]
|
|
||||||
|
|
||||||
describe "individual commands", ->
|
|
||||||
describe "with the same results", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@content = "bar"
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.get.withArgs("doclines:#{@doc_id}").yields(null, @content)
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.get.withArgs("doclines:{#{@doc_id}}").yields(null, @content)
|
|
||||||
@client.get RedisKeyBuilder.docLines({doc_id: @doc_id}), (error, @result) =>
|
|
||||||
setTimeout () -> # Let all background requests complete
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should return the result", ->
|
|
||||||
@result.should.equal @content
|
|
||||||
|
|
||||||
it "should have called the redis client with the appropriate key", ->
|
|
||||||
@rclient_redis.get
|
|
||||||
.calledWith("doclines:#{@doc_id}")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should have called the ioredis cluster client with the appropriate key", ->
|
|
||||||
@rclient_ioredis.get
|
|
||||||
.calledWith("doclines:{#{@doc_id}}")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should send a metric", ->
|
|
||||||
@Metrics.inc
|
|
||||||
.calledWith("backend-match")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should time the commands", ->
|
|
||||||
@timer
|
|
||||||
.calledWith("redis.ioredis.get")
|
|
||||||
.should.equal true
|
|
||||||
@timer
|
|
||||||
.calledWith("redis.noderedis.get")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "with different results", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.get.withArgs("doclines:#{@doc_id}").yields(null, "primary-result")
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.get.withArgs("doclines:{#{@doc_id}}").yields(null, "secondary-result")
|
|
||||||
@client.get RedisKeyBuilder.docLines({doc_id: @doc_id}), (error, @result) =>
|
|
||||||
setTimeout () -> # Let all background requests complete
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should return the primary result", ->
|
|
||||||
@result.should.equal "primary-result"
|
|
||||||
|
|
||||||
it "should send a metric", ->
|
|
||||||
@Metrics.inc
|
|
||||||
.calledWith("backend-conflict")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "with differently ordered results from smembers", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.smembers = sinon.stub()
|
|
||||||
@rclient_redis.smembers.withArgs("DocsIn:#{@project_id}").yields(null, ["one", "two"])
|
|
||||||
@rclient_ioredis.smembers = sinon.stub()
|
|
||||||
@rclient_ioredis.smembers.withArgs("DocsIn:{#{@project_id}}").yields(null, ["two", "one"])
|
|
||||||
@client.smembers RedisKeyBuilder.docsInProject({project_id: @project_id}), (error, @result) =>
|
|
||||||
setTimeout () -> # Let all background requests complete
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should return the primary result", ->
|
|
||||||
@result.should.deep.equal ["one", "two"]
|
|
||||||
|
|
||||||
it "should send a metric indicating a match", ->
|
|
||||||
@Metrics.inc
|
|
||||||
.calledWith("backend-match")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "when the secondary errors", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.get.withArgs("doclines:#{@doc_id}").yields(null, "primary-result")
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.get.withArgs("doclines:{#{@doc_id}}").yields(@error = new Error("oops"))
|
|
||||||
@client.get RedisKeyBuilder.docLines({doc_id: @doc_id}), (error, @result) =>
|
|
||||||
setTimeout () -> # Let all background requests complete
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should return the primary result", ->
|
|
||||||
@result.should.equal "primary-result"
|
|
||||||
|
|
||||||
it "should log out the secondary error", ->
|
|
||||||
@logger.error
|
|
||||||
.calledWith({
|
|
||||||
err: @error
|
|
||||||
}, "error in redis backend")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "when the primary errors", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.get.withArgs("doclines:#{@doc_id}").yields(@error = new Error("oops"))
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.get.withArgs("doclines:{#{@doc_id}}").yields(null, "secondary-result")
|
|
||||||
@client.get RedisKeyBuilder.docLines({doc_id: @doc_id}), (@returned_error, @result) =>
|
|
||||||
setTimeout () -> # Let all background requests complete
|
|
||||||
done()
|
|
||||||
|
|
||||||
it "should return the error", ->
|
|
||||||
@returned_error.should.equal @error
|
|
||||||
|
|
||||||
it "should log out the error", ->
|
|
||||||
@logger.error
|
|
||||||
.calledWith({
|
|
||||||
err: @error
|
|
||||||
}, "error in redis backend")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "when the command has the key in a non-zero argument index", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@script = "mock-script"
|
|
||||||
@key_count = 1
|
|
||||||
@value = "mock-value"
|
|
||||||
@rclient_redis.eval = sinon.stub()
|
|
||||||
@rclient_redis.eval.withArgs(@script, @key_count, "Blocking:#{@doc_id}", @value).yields(null)
|
|
||||||
@rclient_ioredis.eval = sinon.stub()
|
|
||||||
@rclient_ioredis.eval.withArgs(@script, @key_count, "Blocking:{#{@doc_id}}", @value).yields(null, @content)
|
|
||||||
@client.eval @script, @key_count, RedisKeyBuilder.blockingKey({doc_id: @doc_id}), @value, (error) =>
|
|
||||||
setTimeout () -> # Let all background requests complete
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should have called the redis client with the appropriate key", ->
|
|
||||||
@rclient_redis.eval
|
|
||||||
.calledWith(@script, @key_count, "Blocking:#{@doc_id}", @value)
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should have called the ioredis cluster client with the appropriate key", ->
|
|
||||||
@rclient_ioredis.eval
|
|
||||||
.calledWith(@script, @key_count, "Blocking:{#{@doc_id}}", @value)
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "when the secondary takes longer than SECONDARY_TIMEOUT", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@client.SECONDARY_TIMEOUT = 10
|
|
||||||
@content = "bar"
|
|
||||||
@rclient_redis.get = (key, cb) =>
|
|
||||||
key.should.equal "doclines:#{@doc_id}"
|
|
||||||
setTimeout () =>
|
|
||||||
cb(null, @content)
|
|
||||||
, @client.SECONDARY_TIMEOUT * 3 # If the secondary errors first, don't affect the primary result
|
|
||||||
@rclient_ioredis.get = (key, cb) =>
|
|
||||||
key.should.equal "doclines:{#{@doc_id}}"
|
|
||||||
setTimeout () =>
|
|
||||||
cb(null, @content)
|
|
||||||
, @client.SECONDARY_TIMEOUT * 2
|
|
||||||
@client.get RedisKeyBuilder.docLines({doc_id: @doc_id}), (error, @result) =>
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should log out an error for the backend", ->
|
|
||||||
@logger.error
|
|
||||||
.calledWith({err: new Error("backend timed out")}, "backend timed out")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should return the primary result", ->
|
|
||||||
@result.should.equal @content
|
|
||||||
|
|
||||||
describe "when the primary takes longer than SECONDARY_TIMEOUT", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@client.SECONDARY_TIMEOUT = 10
|
|
||||||
@content = "bar"
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.get.withArgs("doclines:{#{@doc_id}}").yields(null, @content)
|
|
||||||
@rclient_redis.get = (key, cb) =>
|
|
||||||
key.should.equal "doclines:#{@doc_id}"
|
|
||||||
setTimeout () =>
|
|
||||||
cb(null, @content)
|
|
||||||
, @client.SECONDARY_TIMEOUT * 2
|
|
||||||
@client.get RedisKeyBuilder.docLines({doc_id: @doc_id}), (error, @result) =>
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should not consider this an error", ->
|
|
||||||
@logger.error
|
|
||||||
.called
|
|
||||||
.should.equal false
|
|
||||||
|
|
||||||
describe "multi commands", ->
|
|
||||||
beforeEach ->
|
|
||||||
# We will test with:
|
|
||||||
# rclient.multi()
|
|
||||||
# .get("doclines:foo")
|
|
||||||
# .get("DocVersion:foo")
|
|
||||||
# .exec (...) ->
|
|
||||||
@doclines = "mock-doclines"
|
|
||||||
@version = "42"
|
|
||||||
@rclient_redis.multi = sinon.stub().returns @rclient_redis
|
|
||||||
@rclient_ioredis.multi = sinon.stub().returns @rclient_ioredis
|
|
||||||
|
|
||||||
describe "with the same results", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.exec = sinon.stub().yields(null, [@doclines, @version])
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.exec = sinon.stub().yields(null, [ [null, @doclines], [null, @version] ])
|
|
||||||
|
|
||||||
multi = @client.multi()
|
|
||||||
multi.get RedisKeyBuilder.docLines({doc_id: @doc_id})
|
|
||||||
multi.get RedisKeyBuilder.docVersion({doc_id: @doc_id})
|
|
||||||
multi.exec (error, @result) =>
|
|
||||||
setTimeout () ->
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should return the result", ->
|
|
||||||
@result.should.deep.equal [@doclines, @version]
|
|
||||||
|
|
||||||
it "should have called the redis client with the appropriate keys", ->
|
|
||||||
@rclient_redis.get
|
|
||||||
.calledWith("doclines:#{@doc_id}")
|
|
||||||
.should.equal true
|
|
||||||
@rclient_redis.get
|
|
||||||
.calledWith("DocVersion:#{@doc_id}")
|
|
||||||
.should.equal true
|
|
||||||
@rclient_ioredis.exec
|
|
||||||
.called
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should have called the ioredis cluster client with the appropriate keys", ->
|
|
||||||
@rclient_ioredis.get
|
|
||||||
.calledWith("doclines:{#{@doc_id}}")
|
|
||||||
.should.equal true
|
|
||||||
@rclient_ioredis.get
|
|
||||||
.calledWith("DocVersion:{#{@doc_id}}")
|
|
||||||
.should.equal true
|
|
||||||
@rclient_ioredis.exec
|
|
||||||
.called
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should send a metric", ->
|
|
||||||
@Metrics.inc
|
|
||||||
.calledWith("backend-match")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should time the exec", ->
|
|
||||||
@timer
|
|
||||||
.calledWith("redis.ioredis.exec")
|
|
||||||
.should.equal true
|
|
||||||
@timer
|
|
||||||
.calledWith("redis.noderedis.exec")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "with different results", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.exec = sinon.stub().yields(null, [@doclines, @version])
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.exec = sinon.stub().yields(null, [ [null, "different-doc-lines"], [null, @version] ])
|
|
||||||
|
|
||||||
multi = @client.multi()
|
|
||||||
multi.get RedisKeyBuilder.docLines({doc_id: @doc_id})
|
|
||||||
multi.get RedisKeyBuilder.docVersion({doc_id: @doc_id})
|
|
||||||
multi.exec (error, @result) =>
|
|
||||||
setTimeout () ->
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should return the primary result", ->
|
|
||||||
@result.should.deep.equal [@doclines, @version]
|
|
||||||
|
|
||||||
it "should send a metric", ->
|
|
||||||
@Metrics.inc
|
|
||||||
.calledWith("backend-conflict")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
describe "when the secondary errors", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.exec = sinon.stub().yields(null, [@doclines, @version])
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.exec = sinon.stub().yields(@error = new Error("oops"))
|
|
||||||
|
|
||||||
multi = @client.multi()
|
|
||||||
multi.get RedisKeyBuilder.docLines({doc_id: @doc_id})
|
|
||||||
multi.get RedisKeyBuilder.docVersion({doc_id: @doc_id})
|
|
||||||
multi.exec (error, @result) =>
|
|
||||||
setTimeout () ->
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should return the primary result", ->
|
|
||||||
@result.should.deep.equal [@doclines, @version]
|
|
||||||
|
|
||||||
describe "when the primary errors", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.exec = sinon.stub().yields(@error = new Error("oops"))
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.exec = sinon.stub().yields([ [null, @doclines], [null, @version] ])
|
|
||||||
|
|
||||||
multi = @client.multi()
|
|
||||||
multi.get RedisKeyBuilder.docLines({doc_id: @doc_id})
|
|
||||||
multi.get RedisKeyBuilder.docVersion({doc_id: @doc_id})
|
|
||||||
multi.exec (@returned_error) =>
|
|
||||||
setTimeout () -> done()
|
|
||||||
|
|
||||||
it "should return the error", ->
|
|
||||||
@returned_error.should.equal @error
|
|
||||||
|
|
||||||
describe "when the secondary takes longer than SECONDARY_TIMEOUT", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.exec = (cb) =>
|
|
||||||
setTimeout () =>
|
|
||||||
cb(null, [@doclines, @version])
|
|
||||||
, 30 # If secondary errors first, don't affect the primary result
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.exec = (cb) =>
|
|
||||||
setTimeout () =>
|
|
||||||
cb(null, [ [null, @doclines], [null, @version] ])
|
|
||||||
, 20
|
|
||||||
|
|
||||||
multi = @client.multi()
|
|
||||||
multi.SECONDARY_TIMEOUT = 10
|
|
||||||
multi.get RedisKeyBuilder.docLines({doc_id: @doc_id})
|
|
||||||
multi.get RedisKeyBuilder.docVersion({doc_id: @doc_id})
|
|
||||||
multi.exec (error, @result) =>
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should log out an error for the backend", ->
|
|
||||||
@logger.error
|
|
||||||
.calledWith({err: new Error("backend timed out")}, "backend timed out")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should return the primary result", ->
|
|
||||||
@result.should.deep.equal [@doclines, @version]
|
|
||||||
|
|
||||||
describe "when the primary takes longer than SECONDARY_TIMEOUT", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@rclient_redis.get = sinon.stub()
|
|
||||||
@rclient_redis.exec = (cb) =>
|
|
||||||
setTimeout () =>
|
|
||||||
cb(null, [@doclines, @version])
|
|
||||||
, 20
|
|
||||||
@rclient_ioredis.get = sinon.stub()
|
|
||||||
@rclient_ioredis.exec = sinon.stub().yields(null, [ [null, @doclines], [null, @version] ])
|
|
||||||
|
|
||||||
multi = @client.multi()
|
|
||||||
multi.SECONDARY_TIMEOUT = 10
|
|
||||||
multi.get RedisKeyBuilder.docLines({doc_id: @doc_id})
|
|
||||||
multi.get RedisKeyBuilder.docVersion({doc_id: @doc_id})
|
|
||||||
multi.exec (error, @result) =>
|
|
||||||
done(error)
|
|
||||||
|
|
||||||
it "should not consider this an error", ->
|
|
||||||
@logger.error
|
|
||||||
.called
|
|
||||||
.should.equal false
|
|
||||||
|
|
||||||
describe "_healthCheckNodeRedisClient", ->
|
|
||||||
beforeEach ->
|
|
||||||
@redis.activeHealthCheckRedis = sinon.stub().returns @healthCheck = {
|
|
||||||
isAlive: sinon.stub()
|
|
||||||
}
|
|
||||||
|
|
||||||
describe "successfully", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@healthCheck.isAlive.returns true
|
|
||||||
@redis_client = {}
|
|
||||||
@client._healthCheckNodeRedisClient(@redis_client, done)
|
|
||||||
|
|
||||||
it "should check the status of the node redis client", ->
|
|
||||||
@healthCheck.isAlive.called.should.equal true
|
|
||||||
|
|
||||||
it "should only create one health check when called multiple times", (done) ->
|
|
||||||
@client._healthCheckNodeRedisClient @redis_client, () =>
|
|
||||||
@redis.activeHealthCheckRedis.calledOnce.should.equal true
|
|
||||||
@healthCheck.isAlive.calledTwice.should.equal true
|
|
||||||
done()
|
|
||||||
|
|
||||||
describe "when failing", ->
|
|
||||||
beforeEach ->
|
|
||||||
@healthCheck.isAlive.returns false
|
|
||||||
@redis_client = {}
|
|
||||||
|
|
||||||
it "should return an error", (done) ->
|
|
||||||
@client._healthCheckNodeRedisClient @redis_client, (error) ->
|
|
||||||
error.message.should.equal "node-redis client failed health check"
|
|
||||||
done()
|
|
||||||
|
|
||||||
describe "_healthCheckClusterClient", ->
|
|
||||||
beforeEach ->
|
|
||||||
@client.HEARTBEAT_TIMEOUT = 10
|
|
||||||
@nodes = [{
|
|
||||||
options: key: "node-0"
|
|
||||||
stream: destroy: sinon.stub()
|
|
||||||
}, {
|
|
||||||
options: key: "node-1"
|
|
||||||
stream: destroy: sinon.stub()
|
|
||||||
}]
|
|
||||||
@rclient_ioredis.nodes = sinon.stub().returns(@nodes)
|
|
||||||
|
|
||||||
describe "when both clients are successful", ->
|
|
||||||
beforeEach (done) ->
|
|
||||||
@nodes[0].ping = sinon.stub().yields()
|
|
||||||
@nodes[1].ping = sinon.stub().yields()
|
|
||||||
@client._healthCheckClusterClient({ rclient: @rclient_ioredis }, done)
|
|
||||||
|
|
||||||
it "should get all cluster nodes", ->
|
|
||||||
@rclient_ioredis.nodes
|
|
||||||
.calledWith("all")
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should ping each cluster node", ->
|
|
||||||
for node in @nodes
|
|
||||||
node.ping.called.should.equal true
|
|
||||||
|
|
||||||
describe "when ping fails to a node", ->
|
|
||||||
beforeEach ->
|
|
||||||
@nodes[0].ping = (cb) -> cb()
|
|
||||||
@nodes[1].ping = (cb) -> # Just hang
|
|
||||||
|
|
||||||
it "should return an error", (done) ->
|
|
||||||
@client._healthCheckClusterClient { rclient: @rclient_ioredis }, (error) ->
|
|
||||||
error.message.should.equal "ioredis node ping check timed out"
|
|
||||||
done()
|
|
|
@ -14,20 +14,24 @@ describe "RedisManager", ->
|
||||||
@rclient.multi = () => @rclient
|
@rclient.multi = () => @rclient
|
||||||
@RedisManager = SandboxedModule.require modulePath,
|
@RedisManager = SandboxedModule.require modulePath,
|
||||||
requires:
|
requires:
|
||||||
"./RedisBackend":
|
|
||||||
createClient: () => @rclient
|
|
||||||
"./RedisKeyBuilder":
|
|
||||||
blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
|
|
||||||
docLines: ({doc_id}) -> "doclines:#{doc_id}"
|
|
||||||
docOps: ({doc_id}) -> "DocOps:#{doc_id}"
|
|
||||||
docVersion: ({doc_id}) -> "DocVersion:#{doc_id}"
|
|
||||||
docHash: ({doc_id}) -> "DocHash:#{doc_id}"
|
|
||||||
projectKey: ({doc_id}) -> "ProjectId:#{doc_id}"
|
|
||||||
pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
|
|
||||||
docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
|
|
||||||
ranges: ({doc_id}) -> "Ranges:#{doc_id}"
|
|
||||||
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
|
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
|
||||||
"settings-sharelatex": {documentupdater: {logHashErrors: {write:true, read:true}}}
|
"settings-sharelatex": {
|
||||||
|
documentupdater: {logHashErrors: {write:true, read:true}}
|
||||||
|
redis:
|
||||||
|
documentupdater:
|
||||||
|
key_schema:
|
||||||
|
blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
|
||||||
|
docLines: ({doc_id}) -> "doclines:#{doc_id}"
|
||||||
|
docOps: ({doc_id}) -> "DocOps:#{doc_id}"
|
||||||
|
docVersion: ({doc_id}) -> "DocVersion:#{doc_id}"
|
||||||
|
docHash: ({doc_id}) -> "DocHash:#{doc_id}"
|
||||||
|
projectKey: ({doc_id}) -> "ProjectId:#{doc_id}"
|
||||||
|
pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
|
||||||
|
docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
|
||||||
|
ranges: ({doc_id}) -> "Ranges:#{doc_id}"
|
||||||
|
}
|
||||||
|
"redis-sharelatex":
|
||||||
|
createClient: () => @rclient
|
||||||
"./Metrics": @metrics =
|
"./Metrics": @metrics =
|
||||||
inc: sinon.stub()
|
inc: sinon.stub()
|
||||||
Timer: class Timer
|
Timer: class Timer
|
||||||
|
|
|
@ -14,6 +14,7 @@ describe "WebRedisManager", ->
|
||||||
@WebRedisManager = SandboxedModule.require modulePath, requires:
|
@WebRedisManager = SandboxedModule.require modulePath, requires:
|
||||||
"redis-sharelatex": createClient: () => @rclient
|
"redis-sharelatex": createClient: () => @rclient
|
||||||
"settings-sharelatex": redis: web: @settings = {"mock": "settings"}
|
"settings-sharelatex": redis: web: @settings = {"mock": "settings"}
|
||||||
|
"logger-sharelatex": { log: () -> }
|
||||||
@doc_id = "doc-id-123"
|
@doc_id = "doc-id-123"
|
||||||
@project_id = "project-id-123"
|
@project_id = "project-id-123"
|
||||||
@callback = sinon.stub()
|
@callback = sinon.stub()
|
||||||
|
|
Loading…
Reference in a new issue