app.coffee 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. Metrics = require("metrics-sharelatex")
  2. Settings = require "settings-sharelatex"
  3. Metrics.initialize(Settings.appName or "real-time")
  4. async = require("async")
  5. _ = require "underscore"
  6. logger = require "logger-sharelatex"
  7. logger.initialize("real-time")
  8. Metrics.event_loop.monitor(logger)
  9. express = require("express")
  10. session = require("express-session")
  11. redis = require("redis-sharelatex")
  12. if Settings.sentry?.dsn?
  13. logger.initializeErrorReporting(Settings.sentry.dsn)
  14. sessionRedisClient = redis.createClient(Settings.redis.websessions)
  15. RedisStore = require('connect-redis')(session)
  16. SessionSockets = require('session.socket.io')
  17. CookieParser = require("cookie-parser")
  18. DrainManager = require("./app/js/DrainManager")
  19. HealthCheckManager = require("./app/js/HealthCheckManager")
  20. # work around frame handler bug in socket.io v0.9.16
  21. require("./socket.io.patch.js")
  22. # Set up socket.io server
  23. app = express()
  24. server = require('http').createServer(app)
  25. io = require('socket.io').listen(server)
  26. # Bind to sessions
  27. sessionStore = new RedisStore(client: sessionRedisClient)
  28. cookieParser = CookieParser(Settings.security.sessionSecret)
  29. sessionSockets = new SessionSockets(io, sessionStore, cookieParser, Settings.cookieName)
  30. Metrics.injectMetricsRoute(app)
  31. app.use(Metrics.http.monitor(logger))
  32. io.configure ->
  33. io.enable('browser client minification')
  34. io.enable('browser client etag')
  35. # Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch"
  36. # See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with
  37. io.set('match origin protocol', true)
  38. # gzip uses a Node 0.8.x method of calling the gzip program which
  39. # doesn't work with 0.6.x
  40. #io.enable('browser client gzip')
  41. io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling'])
  42. io.set('log level', 1)
  43. app.get "/", (req, res, next) ->
  44. res.send "real-time-sharelatex is alive"
  45. app.get "/status", (req, res, next) ->
  46. if Settings.shutDownInProgress
  47. res.send 503 # Service unavailable
  48. else
  49. res.send "real-time-sharelatex is alive"
  50. app.get "/debug/events", (req, res, next) ->
  51. Settings.debugEvents = parseInt(req.query?.count,10) || 20
  52. logger.log {count: Settings.debugEvents}, "starting debug mode"
  53. res.send "debug mode will log next #{Settings.debugEvents} events"
  54. rclient = require("redis-sharelatex").createClient(Settings.redis.realtime)
  55. healthCheck = (req, res, next)->
  56. rclient.healthCheck (error) ->
  57. if error?
  58. logger.err {err: error}, "failed redis health check"
  59. res.sendStatus 500
  60. else if HealthCheckManager.isFailing()
  61. status = HealthCheckManager.status()
  62. logger.err {pubSubErrors: status}, "failed pubsub health check"
  63. res.sendStatus 500
  64. else
  65. res.sendStatus 200
  66. app.get "/health_check", healthCheck
  67. app.get "/health_check/redis", healthCheck
  68. Router = require "./app/js/Router"
  69. Router.configure(app, io, sessionSockets)
  70. WebsocketLoadBalancer = require "./app/js/WebsocketLoadBalancer"
  71. WebsocketLoadBalancer.listenForEditorEvents(io)
  72. DocumentUpdaterController = require "./app/js/DocumentUpdaterController"
  73. DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io)
  74. port = Settings.internal.realTime.port
  75. host = Settings.internal.realTime.host
  76. server.listen port, host, (error) ->
  77. throw error if error?
  78. logger.info "realtime starting up, listening on #{host}:#{port}"
  79. # Stop huge stack traces in logs from all the socket.io parsing steps.
  80. Error.stackTraceLimit = 10
  81. shutdownCleanly = (signal) ->
  82. connectedClients = io.sockets.clients()?.length
  83. if connectedClients == 0
  84. logger.warn("no clients connected, exiting")
  85. process.exit()
  86. else
  87. logger.warn {connectedClients}, "clients still connected, not shutting down yet"
  88. setTimeout () ->
  89. shutdownCleanly(signal)
  90. , 30 * 1000
  91. drainAndShutdown = (signal) ->
  92. if Settings.shutDownInProgress
  93. logger.warn signal: signal, "shutdown already in progress, ignoring signal"
  94. return
  95. else
  96. Settings.shutDownInProgress = true
  97. logger.warn signal: signal, "received interrupt, starting drain over #{shutdownDrainTimeWindow} mins"
  98. DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow)
  99. shutdownCleanly(signal)
  100. Settings.shutDownInProgress = false
  101. if Settings.shutdownDrainTimeWindow?
  102. shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10)
  103. logger.log shutdownDrainTimeWindow: shutdownDrainTimeWindow,"shutdownDrainTimeWindow enabled"
  104. for signal in ['SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGABRT']
  105. process.on signal, drainAndShutdown # signal is passed as argument to event handler
  106. # global exception handler
  107. if Settings.errors?.catchUncaughtErrors
  108. process.removeAllListeners('uncaughtException')
  109. process.on 'uncaughtException', (error) ->
  110. if error.code == 'EPIPE'
  111. return logger.warn err: error, 'attempted to write to disconnected client'
  112. logger.error err: error, 'uncaught exception'
  113. if Settings.errors?.shutdownOnUncaughtError
  114. drainAndShutdown('SIGABRT')
  115. if Settings.continualPubsubTraffic
  116. console.log "continualPubsubTraffic enabled"
  117. pubsubClient = redis.createClient(Settings.redis.pubsub)
  118. clusterClient = redis.createClient(Settings.redis.websessions)
  119. publishJob = (channel, callback)->
  120. checker = new HealthCheckManager(channel)
  121. logger.debug {channel:channel}, "sending pub to keep connection alive"
  122. json = JSON.stringify({health_check:true, key: checker.id, date: new Date().toString()})
  123. pubsubClient.publish channel, json, (err)->
  124. if err?
  125. logger.err {err, channel}, "error publishing pubsub traffic to redis"
  126. clusterClient.publish "cluster-continual-traffic", {keep: "alive"}, callback
  127. runPubSubTraffic = ->
  128. async.map ["applied-ops", "editor-events"], publishJob, (err)->
  129. setTimeout(runPubSubTraffic, 1000 * 20)
  130. runPubSubTraffic()