app.js 7.7 KB

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