app.js 9.0 KB

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