app.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. const Metrics = require('metrics-sharelatex')
  2. const Settings = require('settings-sharelatex')
  3. Metrics.initialize(Settings.appName || 'real-time')
  4. const async = require('async')
  5. const logger = require('logger-sharelatex')
  6. logger.initialize('real-time')
  7. Metrics.event_loop.monitor(logger)
  8. const express = require('express')
  9. const session = require('express-session')
  10. const redis = require('redis-sharelatex')
  11. if (Settings.sentry && Settings.sentry.dsn) {
  12. logger.initializeErrorReporting(Settings.sentry.dsn)
  13. }
  14. const sessionRedisClient = redis.createClient(Settings.redis.websessions)
  15. const RedisStore = require('connect-redis')(session)
  16. const SessionSockets = require('./app/js/SessionSockets')
  17. const CookieParser = require('cookie-parser')
  18. const DrainManager = require('./app/js/DrainManager')
  19. const HealthCheckManager = require('./app/js/HealthCheckManager')
  20. // Set up socket.io server
  21. const app = express()
  22. const server = require('http').createServer(app)
  23. const io = require('socket.io').listen(server)
  24. // Bind to sessions
  25. const sessionStore = new RedisStore({ client: sessionRedisClient })
  26. const cookieParser = CookieParser(Settings.security.sessionSecret)
  27. const sessionSockets = new SessionSockets(
  28. io,
  29. sessionStore,
  30. cookieParser,
  31. Settings.cookieName
  32. )
  33. Metrics.injectMetricsRoute(app)
  34. app.use(Metrics.http.monitor(logger))
  35. io.configure(function () {
  36. io.enable('browser client minification')
  37. io.enable('browser client etag')
  38. // Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch"
  39. // See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with
  40. io.set('match origin protocol', true)
  41. // gzip uses a Node 0.8.x method of calling the gzip program which
  42. // doesn't work with 0.6.x
  43. // io.enable('browser client gzip')
  44. io.set('transports', [
  45. 'websocket',
  46. 'flashsocket',
  47. 'htmlfile',
  48. 'xhr-polling',
  49. 'jsonp-polling'
  50. ])
  51. io.set('log level', 1)
  52. })
  53. app.get('/', (req, res) => res.send('real-time-sharelatex is alive'))
  54. app.get('/status', function (req, res) {
  55. if (Settings.shutDownInProgress) {
  56. res.sendStatus(503) // Service unavailable
  57. } else {
  58. res.send('real-time-sharelatex is alive')
  59. }
  60. })
  61. app.get('/debug/events', function (req, res) {
  62. Settings.debugEvents = parseInt(req.query.count, 10) || 20
  63. logger.log({ count: Settings.debugEvents }, 'starting debug mode')
  64. res.send(`debug mode will log next ${Settings.debugEvents} events`)
  65. })
  66. const rclient = require('redis-sharelatex').createClient(
  67. Settings.redis.realtime
  68. )
  69. function healthCheck(req, res) {
  70. rclient.healthCheck(function (error) {
  71. if (error) {
  72. logger.err({ err: error }, 'failed redis health check')
  73. res.sendStatus(500)
  74. } else if (HealthCheckManager.isFailing()) {
  75. const status = HealthCheckManager.status()
  76. logger.err({ pubSubErrors: status }, 'failed pubsub health check')
  77. res.sendStatus(500)
  78. } else {
  79. res.sendStatus(200)
  80. }
  81. })
  82. }
  83. app.get('/health_check', healthCheck)
  84. app.get('/health_check/redis', healthCheck)
  85. const Router = require('./app/js/Router')
  86. Router.configure(app, io, sessionSockets)
  87. const WebsocketLoadBalancer = require('./app/js/WebsocketLoadBalancer')
  88. WebsocketLoadBalancer.listenForEditorEvents(io)
  89. const DocumentUpdaterController = require('./app/js/DocumentUpdaterController')
  90. DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io)
  91. const { port } = Settings.internal.realTime
  92. const { host } = Settings.internal.realTime
  93. server.listen(port, host, function (error) {
  94. if (error) {
  95. throw error
  96. }
  97. logger.info(`realtime starting up, listening on ${host}:${port}`)
  98. })
  99. // Stop huge stack traces in logs from all the socket.io parsing steps.
  100. Error.stackTraceLimit = 10
  101. function shutdownCleanly(signal) {
  102. const connectedClients = io.sockets.clients().length
  103. if (connectedClients === 0) {
  104. logger.warn('no clients connected, exiting')
  105. process.exit()
  106. } else {
  107. logger.warn(
  108. { connectedClients },
  109. 'clients still connected, not shutting down yet'
  110. )
  111. setTimeout(() => shutdownCleanly(signal), 30 * 1000)
  112. }
  113. }
  114. function drainAndShutdown(signal) {
  115. if (Settings.shutDownInProgress) {
  116. logger.warn({ signal }, 'shutdown already in progress, ignoring signal')
  117. } else {
  118. Settings.shutDownInProgress = true
  119. const { statusCheckInterval } = Settings
  120. if (statusCheckInterval) {
  121. logger.warn(
  122. { signal },
  123. `received interrupt, delay drain by ${statusCheckInterval}ms`
  124. )
  125. }
  126. setTimeout(function () {
  127. logger.warn(
  128. { signal },
  129. `received interrupt, starting drain over ${shutdownDrainTimeWindow} mins`
  130. )
  131. DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow)
  132. shutdownCleanly(signal)
  133. }, statusCheckInterval)
  134. }
  135. }
  136. Settings.shutDownInProgress = false
  137. const shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10)
  138. if (Settings.shutdownDrainTimeWindow) {
  139. logger.log({ shutdownDrainTimeWindow }, 'shutdownDrainTimeWindow enabled')
  140. for (const signal of [
  141. 'SIGINT',
  142. 'SIGHUP',
  143. 'SIGQUIT',
  144. 'SIGUSR1',
  145. 'SIGUSR2',
  146. 'SIGTERM',
  147. 'SIGABRT'
  148. ]) {
  149. process.on(signal, drainAndShutdown)
  150. } // signal is passed as argument to event handler
  151. // global exception handler
  152. if (Settings.errors && Settings.errors.catchUncaughtErrors) {
  153. process.removeAllListeners('uncaughtException')
  154. process.on('uncaughtException', function (error) {
  155. if (
  156. ['ETIMEDOUT', 'EHOSTUNREACH', 'EPIPE', 'ECONNRESET'].includes(
  157. error.code
  158. )
  159. ) {
  160. Metrics.inc('disconnected_write', 1, { status: error.code })
  161. return logger.warn(
  162. { err: error },
  163. 'attempted to write to disconnected client'
  164. )
  165. }
  166. logger.error({ err: error }, 'uncaught exception')
  167. if (Settings.errors && Settings.errors.shutdownOnUncaughtError) {
  168. drainAndShutdown('SIGABRT')
  169. }
  170. })
  171. }
  172. }
  173. if (Settings.continualPubsubTraffic) {
  174. logger.warn('continualPubsubTraffic enabled')
  175. const pubsubClient = redis.createClient(Settings.redis.pubsub)
  176. const clusterClient = redis.createClient(Settings.redis.websessions)
  177. const publishJob = function (channel, callback) {
  178. const checker = new HealthCheckManager(channel)
  179. logger.debug({ channel }, 'sending pub to keep connection alive')
  180. const json = JSON.stringify({
  181. health_check: true,
  182. key: checker.id,
  183. date: new Date().toString()
  184. })
  185. Metrics.summary(`redis.publish.${channel}`, json.length)
  186. pubsubClient.publish(channel, json, function (err) {
  187. if (err) {
  188. logger.err({ err, channel }, 'error publishing pubsub traffic to redis')
  189. }
  190. const blob = JSON.stringify({ keep: 'alive' })
  191. Metrics.summary('redis.publish.cluster-continual-traffic', blob.length)
  192. clusterClient.publish('cluster-continual-traffic', blob, callback)
  193. })
  194. }
  195. const runPubSubTraffic = () =>
  196. async.map(['applied-ops', 'editor-events'], publishJob, () =>
  197. setTimeout(runPubSubTraffic, 1000 * 20)
  198. )
  199. runPubSubTraffic()
  200. }