2018-12-04 09:11:28 -05:00
|
|
|
Metrics = require("metrics-sharelatex")
|
2018-12-04 09:21:05 -05:00
|
|
|
Settings = require "settings-sharelatex"
|
2018-12-04 09:11:28 -05:00
|
|
|
Metrics.initialize(Settings.appName or "real-time")
|
2019-02-15 10:23:59 -05:00
|
|
|
async = require("async")
|
2019-07-02 10:36:17 -04:00
|
|
|
_ = require "underscore"
|
2018-12-04 09:11:28 -05:00
|
|
|
|
2016-11-09 07:06:32 -05:00
|
|
|
logger = require "logger-sharelatex"
|
2019-01-31 10:33:11 -05:00
|
|
|
logger.initialize("real-time")
|
2018-12-04 09:11:28 -05:00
|
|
|
Metrics.event_loop.monitor(logger)
|
2016-11-09 07:06:32 -05:00
|
|
|
|
2014-11-07 12:38:12 -05:00
|
|
|
express = require("express")
|
|
|
|
session = require("express-session")
|
|
|
|
redis = require("redis-sharelatex")
|
2017-06-06 04:49:38 -04:00
|
|
|
if Settings.sentry?.dsn?
|
|
|
|
logger.initializeErrorReporting(Settings.sentry.dsn)
|
2016-11-09 07:06:32 -05:00
|
|
|
|
2017-05-10 10:52:35 -04:00
|
|
|
sessionRedisClient = redis.createClient(Settings.redis.websessions)
|
2016-11-09 07:06:32 -05:00
|
|
|
|
2014-11-07 12:38:12 -05:00
|
|
|
RedisStore = require('connect-redis')(session)
|
2020-06-06 08:37:40 -04:00
|
|
|
SessionSockets = require('./app/js/SessionSockets')
|
2014-11-07 12:38:12 -05:00
|
|
|
CookieParser = require("cookie-parser")
|
|
|
|
|
2018-12-04 09:06:39 -05:00
|
|
|
DrainManager = require("./app/js/DrainManager")
|
2019-04-15 09:05:26 -04:00
|
|
|
HealthCheckManager = require("./app/js/HealthCheckManager")
|
2018-12-04 09:06:39 -05:00
|
|
|
|
2019-07-16 09:02:52 -04:00
|
|
|
# work around frame handler bug in socket.io v0.9.16
|
|
|
|
require("./socket.io.patch.js")
|
2014-11-07 12:38:12 -05:00
|
|
|
# Set up socket.io server
|
|
|
|
app = express()
|
2019-08-14 10:38:02 -04:00
|
|
|
|
2014-11-07 12:38:12 -05:00
|
|
|
server = require('http').createServer(app)
|
|
|
|
io = require('socket.io').listen(server)
|
|
|
|
|
|
|
|
# Bind to sessions
|
2016-11-09 07:09:15 -05:00
|
|
|
sessionStore = new RedisStore(client: sessionRedisClient)
|
2014-11-07 12:38:12 -05:00
|
|
|
cookieParser = CookieParser(Settings.security.sessionSecret)
|
2018-12-04 08:32:37 -05:00
|
|
|
|
2014-11-07 12:38:12 -05:00
|
|
|
sessionSockets = new SessionSockets(io, sessionStore, cookieParser, Settings.cookieName)
|
|
|
|
|
2019-08-14 10:38:02 -04:00
|
|
|
Metrics.injectMetricsRoute(app)
|
|
|
|
app.use(Metrics.http.monitor(logger))
|
|
|
|
|
2014-11-07 12:38:12 -05:00
|
|
|
io.configure ->
|
|
|
|
io.enable('browser client minification')
|
|
|
|
io.enable('browser client etag')
|
|
|
|
|
|
|
|
# Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch"
|
|
|
|
# See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with
|
|
|
|
io.set('match origin protocol', true)
|
|
|
|
|
|
|
|
# gzip uses a Node 0.8.x method of calling the gzip program which
|
|
|
|
# doesn't work with 0.6.x
|
|
|
|
#io.enable('browser client gzip')
|
2019-05-24 10:21:48 -04:00
|
|
|
io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling'])
|
2014-11-07 12:38:12 -05:00
|
|
|
io.set('log level', 1)
|
2014-11-20 11:56:09 -05:00
|
|
|
|
2018-10-19 11:44:40 -04:00
|
|
|
app.get "/", (req, res, next) ->
|
|
|
|
res.send "real-time-sharelatex is alive"
|
|
|
|
|
2014-11-20 11:56:09 -05:00
|
|
|
app.get "/status", (req, res, next) ->
|
2019-08-13 11:56:48 -04:00
|
|
|
if Settings.shutDownInProgress
|
2019-08-13 05:41:35 -04:00
|
|
|
res.send 503 # Service unavailable
|
|
|
|
else
|
|
|
|
res.send "real-time-sharelatex is alive"
|
2016-11-09 07:06:32 -05:00
|
|
|
|
2019-04-11 10:00:25 -04:00
|
|
|
app.get "/debug/events", (req, res, next) ->
|
|
|
|
Settings.debugEvents = parseInt(req.query?.count,10) || 20
|
|
|
|
logger.log {count: Settings.debugEvents}, "starting debug mode"
|
|
|
|
res.send "debug mode will log next #{Settings.debugEvents} events"
|
|
|
|
|
2017-05-02 10:51:17 -04:00
|
|
|
rclient = require("redis-sharelatex").createClient(Settings.redis.realtime)
|
2019-07-08 06:17:08 -04:00
|
|
|
|
2019-08-14 10:38:02 -04:00
|
|
|
healthCheck = (req, res, next)->
|
2017-05-02 10:51:17 -04:00
|
|
|
rclient.healthCheck (error) ->
|
|
|
|
if error?
|
|
|
|
logger.err {err: error}, "failed redis health check"
|
|
|
|
res.sendStatus 500
|
2019-04-15 09:05:26 -04:00
|
|
|
else if HealthCheckManager.isFailing()
|
|
|
|
status = HealthCheckManager.status()
|
|
|
|
logger.err {pubSubErrors: status}, "failed pubsub health check"
|
|
|
|
res.sendStatus 500
|
2017-05-02 10:51:17 -04:00
|
|
|
else
|
|
|
|
res.sendStatus 200
|
2016-11-09 07:06:32 -05:00
|
|
|
|
2019-08-14 10:38:02 -04:00
|
|
|
app.get "/health_check", healthCheck
|
|
|
|
|
|
|
|
app.get "/health_check/redis", healthCheck
|
|
|
|
|
|
|
|
|
2018-12-05 09:01:15 -05:00
|
|
|
|
2014-11-07 12:38:12 -05:00
|
|
|
Router = require "./app/js/Router"
|
|
|
|
Router.configure(app, io, sessionSockets)
|
2014-11-13 11:03:37 -05:00
|
|
|
|
|
|
|
WebsocketLoadBalancer = require "./app/js/WebsocketLoadBalancer"
|
|
|
|
WebsocketLoadBalancer.listenForEditorEvents(io)
|
2014-11-14 10:30:18 -05:00
|
|
|
|
|
|
|
DocumentUpdaterController = require "./app/js/DocumentUpdaterController"
|
|
|
|
DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io)
|
2016-11-09 07:06:32 -05:00
|
|
|
|
2014-11-07 12:38:12 -05:00
|
|
|
port = Settings.internal.realTime.port
|
|
|
|
host = Settings.internal.realTime.host
|
|
|
|
|
|
|
|
server.listen port, host, (error) ->
|
|
|
|
throw error if error?
|
2015-04-30 10:05:31 -04:00
|
|
|
logger.info "realtime starting up, listening on #{host}:#{port}"
|
2016-11-09 07:06:32 -05:00
|
|
|
|
2014-11-25 04:17:26 -05:00
|
|
|
# Stop huge stack traces in logs from all the socket.io parsing steps.
|
2016-11-09 07:06:32 -05:00
|
|
|
Error.stackTraceLimit = 10
|
2018-12-04 08:47:04 -05:00
|
|
|
|
|
|
|
|
|
|
|
shutdownCleanly = (signal) ->
|
2018-12-04 09:06:39 -05:00
|
|
|
connectedClients = io.sockets.clients()?.length
|
|
|
|
if connectedClients == 0
|
2020-02-04 06:14:14 -05:00
|
|
|
logger.warn("no clients connected, exiting")
|
2018-12-04 09:06:39 -05:00
|
|
|
process.exit()
|
|
|
|
else
|
2020-02-04 06:14:14 -05:00
|
|
|
logger.warn {connectedClients}, "clients still connected, not shutting down yet"
|
2018-12-04 09:06:39 -05:00
|
|
|
setTimeout () ->
|
|
|
|
shutdownCleanly(signal)
|
2020-02-04 06:14:14 -05:00
|
|
|
, 30 * 1000
|
2018-12-04 09:06:39 -05:00
|
|
|
|
2019-10-17 07:45:56 -04:00
|
|
|
drainAndShutdown = (signal) ->
|
|
|
|
if Settings.shutDownInProgress
|
2020-02-04 06:14:14 -05:00
|
|
|
logger.warn signal: signal, "shutdown already in progress, ignoring signal"
|
2019-10-17 07:45:56 -04:00
|
|
|
return
|
|
|
|
else
|
|
|
|
Settings.shutDownInProgress = true
|
2020-02-20 15:44:21 -05:00
|
|
|
statusCheckInterval = Settings.statusCheckInterval
|
|
|
|
if statusCheckInterval
|
|
|
|
logger.warn signal: signal, "received interrupt, delay drain by #{statusCheckInterval}ms"
|
|
|
|
setTimeout () ->
|
|
|
|
logger.warn signal: signal, "received interrupt, starting drain over #{shutdownDrainTimeWindow} mins"
|
|
|
|
DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow)
|
|
|
|
shutdownCleanly(signal)
|
|
|
|
, statusCheckInterval
|
2019-10-17 07:45:56 -04:00
|
|
|
|
|
|
|
|
2019-08-13 11:56:48 -04:00
|
|
|
Settings.shutDownInProgress = false
|
2019-08-14 06:51:25 -04:00
|
|
|
if Settings.shutdownDrainTimeWindow?
|
2019-08-15 04:48:42 -04:00
|
|
|
shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10)
|
|
|
|
logger.log shutdownDrainTimeWindow: shutdownDrainTimeWindow,"shutdownDrainTimeWindow enabled"
|
2018-12-04 09:06:39 -05:00
|
|
|
for signal in ['SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGABRT']
|
2020-02-04 06:14:53 -05:00
|
|
|
process.on signal, drainAndShutdown # signal is passed as argument to event handler
|
2019-10-17 07:45:56 -04:00
|
|
|
|
|
|
|
# global exception handler
|
|
|
|
if Settings.errors?.catchUncaughtErrors
|
|
|
|
process.removeAllListeners('uncaughtException')
|
|
|
|
process.on 'uncaughtException', (error) ->
|
2020-02-04 08:58:45 -05:00
|
|
|
if ['EPIPE', 'ECONNRESET'].includes(error.code)
|
2020-02-04 09:03:56 -05:00
|
|
|
Metrics.inc('disconnected_write', 1, {status: error.code})
|
2020-02-04 07:56:43 -05:00
|
|
|
return logger.warn err: error, 'attempted to write to disconnected client'
|
2019-10-17 07:45:56 -04:00
|
|
|
logger.error err: error, 'uncaught exception'
|
|
|
|
if Settings.errors?.shutdownOnUncaughtError
|
|
|
|
drainAndShutdown('SIGABRT')
|
2019-02-15 10:23:59 -05:00
|
|
|
|
|
|
|
if Settings.continualPubsubTraffic
|
|
|
|
console.log "continualPubsubTraffic enabled"
|
|
|
|
|
2019-07-08 07:07:28 -04:00
|
|
|
pubsubClient = redis.createClient(Settings.redis.pubsub)
|
|
|
|
clusterClient = redis.createClient(Settings.redis.websessions)
|
2019-02-15 10:23:59 -05:00
|
|
|
|
2019-07-02 09:56:50 -04:00
|
|
|
publishJob = (channel, callback)->
|
2019-04-15 09:05:26 -04:00
|
|
|
checker = new HealthCheckManager(channel)
|
2019-02-15 10:23:59 -05:00
|
|
|
logger.debug {channel:channel}, "sending pub to keep connection alive"
|
2019-04-15 09:05:26 -04:00
|
|
|
json = JSON.stringify({health_check:true, key: checker.id, date: new Date().toString()})
|
2020-03-30 05:31:44 -04:00
|
|
|
Metrics.summary "redis.publish.#{channel}", json.length
|
2019-07-08 07:07:28 -04:00
|
|
|
pubsubClient.publish channel, json, (err)->
|
|
|
|
if err?
|
|
|
|
logger.err {err, channel}, "error publishing pubsub traffic to redis"
|
2020-03-30 05:31:44 -04:00
|
|
|
blob = JSON.stringify({keep: "alive"})
|
|
|
|
Metrics.summary "redis.publish.cluster-continual-traffic", blob.length
|
|
|
|
clusterClient.publish "cluster-continual-traffic", blob, callback
|
2019-07-08 07:07:28 -04:00
|
|
|
|
2019-02-15 10:23:59 -05:00
|
|
|
|
|
|
|
runPubSubTraffic = ->
|
|
|
|
async.map ["applied-ops", "editor-events"], publishJob, (err)->
|
2019-04-15 09:05:26 -04:00
|
|
|
setTimeout(runPubSubTraffic, 1000 * 20)
|
2019-02-15 10:23:59 -05:00
|
|
|
|
|
|
|
runPubSubTraffic()
|
|
|
|
|
|
|
|
|
|
|
|
|