app.js 8.0 KB

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