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}" 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}}" 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() @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 = {} "ioredis": @ioredis = Cluster: Cluster @client = @RedisBackend.createClient() @doc_id = "mock-doc-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 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 log out the difference", -> @logger.warn .calledWith({ results: [ "primary-result", "secondary-result" ] }, "redis return values do not 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 "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 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 log out the difference", -> @logger.warn .calledWith({ results: [ [@doclines, @version], ["different-doc-lines", @version] ] }, "redis return values do not match") .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] it "should log out the secondary error", -> @logger.error .calledWith({ err: @error }, "error in redis backend") .should.equal true describe "when the secondary 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 it "should log out the error", -> @logger.error .calledWith({ err: @error }, "error in redis backend") .should.equal true describe "monitorAndReconnect", -> beforeEach -> @client._monitorCluster = sinon.stub() @client.monitorAndReconnect() it "should monitor the cluster client", -> @client._monitorCluster .calledWith(@rclient_ioredis) .should.equal true describe "_monitorCluster", -> beforeEach -> @client.HEARTBEAT_TIMEOUT = 10 @client.HEARTBEAT_INTERVAL = 100 @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 "successfully", -> beforeEach -> @nodes[0].ping = (cb) -> cb() @nodes[1].ping = (cb) -> cb() @client._monitorCluster(@rclient_ioredis) it "should get all nodes", -> setTimeout () => @rclient_ioredis.nodes .calledWith("all") .should.equal true , 200 it "should not reset the node connections", (done) -> setTimeout () => @nodes[0].stream.destroy.called.should.equal false @nodes[1].stream.destroy.called.should.equal false done() , 200 describe "when ping fails to a node", -> beforeEach -> @nodes[0].ping = (cb) -> cb() @nodes[1].ping = (cb) -> # Just hang @client._monitorCluster(@rclient_ioredis) it "should reset the failing node connection", (done) -> setTimeout () => @nodes[0].stream.destroy.called.should.equal false @nodes[1].stream.destroy.called.should.equal true done() , 200