app.js 7.8 KB

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