app.js 9.4 KB

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