diff --git a/services/real-time/app/coffee/HttpApiController.coffee b/services/real-time/app/coffee/HttpApiController.coffee index f99bef6dd6..299d198f57 100644 --- a/services/real-time/app/coffee/HttpApiController.coffee +++ b/services/real-time/app/coffee/HttpApiController.coffee @@ -18,4 +18,18 @@ module.exports = HttpApiController = rate = parseFloat(rate) || 0 logger.log {rate}, "setting client drain rate" DrainManager.startDrain io, rate - res.send 204 \ No newline at end of file + res.send 204 + + disconnectClient: (req, res, next) -> + io = req.app.get("io") + client_id = req.params.client_id + client = io.sockets.sockets[client_id] + + if !client + logger.info({client_id}, "api: client already disconnected") + res.sendStatus(404) + return + logger.warn({client_id}, "api: requesting client disconnect") + client.on "disconnect", () -> + res.sendStatus(204) + client.disconnect() diff --git a/services/real-time/app/coffee/Router.coffee b/services/real-time/app/coffee/Router.coffee index 93cdd07926..49166b8de1 100644 --- a/services/real-time/app/coffee/Router.coffee +++ b/services/real-time/app/coffee/Router.coffee @@ -54,6 +54,7 @@ module.exports = Router = app.post "/project/:project_id/message/:message", httpAuth, bodyParser.json(limit: "5mb"), HttpApiController.sendMessage app.post "/drain", httpAuth, HttpApiController.startDrain + app.post "/client/:client_id/disconnect", httpAuth, HttpApiController.disconnectClient session.on 'connection', (error, client, session) -> client?.on "error", (err) -> diff --git a/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee b/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee new file mode 100644 index 0000000000..b5b192cf88 --- /dev/null +++ b/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee @@ -0,0 +1,82 @@ +RealTimeClient = require "./helpers/RealTimeClient" +FixturesManager = require "./helpers/FixturesManager" + +expect = require("chai").expect + +async = require "async" +request = require "request" + +Settings = require "settings-sharelatex" + +drain = (rate, callback) -> + request.post { + url: "http://localhost:3026/drain?rate=#{rate}" + auth: { + user: Settings.internal.realTime.user, + pass: Settings.internal.realTime.pass + } + }, (error, response, data) -> + callback error, data + return null + +describe "DrainManagerTests", -> + before (done) -> + FixturesManager.setUpProject { + privilegeLevel: "owner" + project: { + name: "Test Project" + } + }, (e, {@project_id, @user_id}) => done() + return null + + before (done) -> + # cleanup to speedup reconnecting + @timeout(10000) + RealTimeClient.disconnectAllClients done + + # trigger and check cleanup + it "should have disconnected all previous clients", (done) -> + RealTimeClient.getConnectedClients (error, data) -> + return done(error) if error + expect(data.length).to.equal(0) + done() + + describe "with two clients in the project", -> + beforeEach (done) -> + async.series [ + (cb) => + @clientA = RealTimeClient.connect() + @clientA.on "connectionAccepted", cb + + (cb) => + @clientB = RealTimeClient.connect() + @clientB.on "connectionAccepted", cb + + (cb) => + @clientA.emit "joinProject", project_id: @project_id, cb + + (cb) => + @clientB.emit "joinProject", project_id: @project_id, cb + ], done + + describe "starting to drain", () -> + beforeEach (done) -> + async.parallel [ + (cb) => + @clientA.on "reconnectGracefully", cb + (cb) => + @clientB.on "reconnectGracefully", cb + + (cb) -> drain(2, cb) + ], done + + afterEach (done) -> + # reset drain + drain(0, done) + + it "should not timeout", -> + expect(true).to.equal(true) + + it "should not have disconnected", -> + expect(@clientA.socket.connected).to.equal true + expect(@clientB.socket.connected).to.equal true diff --git a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee index e3254a79a5..7d54e23b3c 100644 --- a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee +++ b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee @@ -1,5 +1,6 @@ XMLHttpRequest = require("../../libs/XMLHttpRequest").XMLHttpRequest io = require("socket.io-client") +async = require("async") request = require "request" Settings = require "settings-sharelatex" @@ -55,3 +56,20 @@ module.exports = Client = }, (error, response, data) -> callback error, data + + disconnectClient: (client_id, callback) -> + request.post { + url: "http://localhost:3026/client/#{client_id}/disconnect" + auth: { + user: Settings.internal.realTime.user, + pass: Settings.internal.realTime.pass + } + }, (error, response, data) -> + callback error, data + return null + + disconnectAllClients: (callback) -> + Client.getConnectedClients (error, clients) -> + async.each clients, (clientView, cb) -> + Client.disconnectClient clientView.client_id, cb + , callback