From 91e296533fa65b9a3779fabdc36c427ead6a41d7 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Fri, 21 Feb 2020 15:11:09 +0000 Subject: [PATCH 1/3] [misc] test/acceptance: add tests for the draining of connections --- .../coffee/DrainManagerTests.coffee | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 services/real-time/test/acceptance/coffee/DrainManagerTests.coffee 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..cffae6fda6 --- /dev/null +++ b/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee @@ -0,0 +1,72 @@ +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 + + 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", () -> + # there is an internal delay of 1000ms, shift accordingly + @timeout(5000) + 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 From de35fc5ecf6e91b8e0e39a0e3e3020428def4718 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Mon, 24 Feb 2020 13:28:22 +0100 Subject: [PATCH 2/3] [HttpApiController] implement the disconnection of a single client The http route returns as soon as the client has fully disconnected. --- .../app/coffee/HttpApiController.coffee | 16 +++++++++++++++- services/real-time/app/coffee/Router.coffee | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) 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 276c64e2dd..99aacce3bc 100644 --- a/services/real-time/app/coffee/Router.coffee +++ b/services/real-time/app/coffee/Router.coffee @@ -53,6 +53,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) -> From eabff1d6b2f777f5fa2a2e0077b13aa0bdca6486 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Mon, 24 Feb 2020 12:30:24 +0000 Subject: [PATCH 3/3] [perf] test/acceptance: DrainManagerTests: cleanup previous clients ...before starting to drain. --- .../acceptance/coffee/DrainManagerTests.coffee | 14 ++++++++++++-- .../coffee/helpers/RealTimeClient.coffee | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee b/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee index cffae6fda6..b5b192cf88 100644 --- a/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee +++ b/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee @@ -29,6 +29,18 @@ describe "DrainManagerTests", -> }, (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 [ @@ -48,8 +60,6 @@ describe "DrainManagerTests", -> ], done describe "starting to drain", () -> - # there is an internal delay of 1000ms, shift accordingly - @timeout(5000) beforeEach (done) -> async.parallel [ (cb) => 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