ClsiCookieManager.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /* eslint-disable
  2. camelcase,
  3. node/handle-callback-err,
  4. max-len,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. let rclient_secondary
  15. const { URL, URLSearchParams } = require('url')
  16. const OError = require('@overleaf/o-error')
  17. const Settings = require('@overleaf/settings')
  18. const request = require('request').defaults({ timeout: 30 * 1000 })
  19. const RedisWrapper = require('../../infrastructure/RedisWrapper')
  20. const rclient = RedisWrapper.client('clsi_cookie')
  21. if (Settings.redis.clsi_cookie_secondary != null) {
  22. rclient_secondary = RedisWrapper.client('clsi_cookie_secondary')
  23. }
  24. const Cookie = require('cookie')
  25. const logger = require('@overleaf/logger')
  26. const Metrics = require('@overleaf/metrics')
  27. const clsiCookiesEnabled =
  28. (Settings.clsiCookie != null ? Settings.clsiCookie.key : undefined) != null &&
  29. Settings.clsiCookie.key.length !== 0
  30. module.exports = function (backendGroup) {
  31. return {
  32. buildKey(project_id, user_id) {
  33. if (backendGroup != null) {
  34. return `clsiserver:${backendGroup}:${project_id}:${user_id}`
  35. } else {
  36. return `clsiserver:${project_id}:${user_id}`
  37. }
  38. },
  39. _getServerId(project_id, user_id, compileGroup, callback) {
  40. if (callback == null) {
  41. callback = function () {}
  42. }
  43. return rclient.get(
  44. this.buildKey(project_id, user_id),
  45. (err, serverId) => {
  46. if (err != null) {
  47. return callback(err)
  48. }
  49. if (serverId == null || serverId === '') {
  50. return this._populateServerIdViaRequest(
  51. project_id,
  52. user_id,
  53. compileGroup,
  54. callback
  55. )
  56. } else {
  57. return callback(null, serverId)
  58. }
  59. }
  60. )
  61. },
  62. _populateServerIdViaRequest(project_id, user_id, compileGroup, callback) {
  63. if (callback == null) {
  64. callback = function () {}
  65. }
  66. const u = new URL(
  67. `${Settings.apis.clsi.url}/project/${project_id}/status`
  68. )
  69. u.search = new URLSearchParams({ compileGroup }).toString()
  70. request.post(u.href, (err, res, body) => {
  71. if (err != null) {
  72. OError.tag(err, 'error getting initial server id for project', {
  73. project_id,
  74. })
  75. return callback(err)
  76. }
  77. this.setServerId(
  78. project_id,
  79. user_id,
  80. compileGroup,
  81. res,
  82. null,
  83. function (err, serverId) {
  84. if (err != null) {
  85. logger.warn(
  86. { err, project_id },
  87. 'error setting server id via populate request'
  88. )
  89. }
  90. return callback(err, serverId)
  91. }
  92. )
  93. })
  94. },
  95. _parseServerIdFromResponse(response) {
  96. const cookies = Cookie.parse(
  97. (response.headers['set-cookie'] != null
  98. ? response.headers['set-cookie'][0]
  99. : undefined) || ''
  100. )
  101. return cookies != null ? cookies[Settings.clsiCookie.key] : undefined
  102. },
  103. checkIsLoadSheddingEvent(clsiserverid, compileGroup) {
  104. request.get(
  105. {
  106. url: `${Settings.apis.clsi.url}/instance-state`,
  107. qs: { clsiserverid, compileGroup },
  108. },
  109. (err, res, body) => {
  110. if (err) {
  111. Metrics.inc('clsi-lb-switch-backend', 1, {
  112. status: 'error',
  113. })
  114. logger.warn({ err, clsiserverid }, 'cannot probe clsi VM')
  115. return
  116. }
  117. const isStillRunning =
  118. res.statusCode === 200 && body === `${clsiserverid},UP\n`
  119. Metrics.inc('clsi-lb-switch-backend', 1, {
  120. status: isStillRunning ? 'load-shedding' : 'cycle',
  121. })
  122. }
  123. )
  124. },
  125. _getTTLInSeconds(clsiServerId) {
  126. return (clsiServerId || '').includes('-reg-')
  127. ? Settings.clsiCookie.ttlInSecondsRegular
  128. : Settings.clsiCookie.ttlInSeconds
  129. },
  130. setServerId(
  131. project_id,
  132. user_id,
  133. compileGroup,
  134. response,
  135. previous,
  136. callback
  137. ) {
  138. if (callback == null) {
  139. callback = function () {}
  140. }
  141. if (!clsiCookiesEnabled) {
  142. return callback()
  143. }
  144. const serverId = this._parseServerIdFromResponse(response)
  145. if (serverId == null) {
  146. // We don't get a cookie back if it hasn't changed
  147. return rclient.expire(
  148. this.buildKey(project_id, user_id),
  149. this._getTTLInSeconds(previous),
  150. err => callback(err, undefined)
  151. )
  152. }
  153. if (!previous) {
  154. // Initial assignment of a user+project or after clearing cache.
  155. Metrics.inc('clsi-lb-assign-initial-backend')
  156. } else {
  157. this.checkIsLoadSheddingEvent(previous, compileGroup)
  158. }
  159. if (rclient_secondary != null) {
  160. this._setServerIdInRedis(
  161. rclient_secondary,
  162. project_id,
  163. user_id,
  164. serverId,
  165. () => {}
  166. )
  167. }
  168. this._setServerIdInRedis(rclient, project_id, user_id, serverId, err =>
  169. callback(err, serverId)
  170. )
  171. },
  172. _setServerIdInRedis(rclient, project_id, user_id, serverId, callback) {
  173. if (callback == null) {
  174. callback = function () {}
  175. }
  176. rclient.setex(
  177. this.buildKey(project_id, user_id),
  178. this._getTTLInSeconds(serverId),
  179. serverId,
  180. callback
  181. )
  182. },
  183. clearServerId(project_id, user_id, callback) {
  184. if (callback == null) {
  185. callback = function () {}
  186. }
  187. if (!clsiCookiesEnabled) {
  188. return callback()
  189. }
  190. return rclient.del(this.buildKey(project_id, user_id), callback)
  191. },
  192. getCookieJar(project_id, user_id, compileGroup, callback) {
  193. if (callback == null) {
  194. callback = function () {}
  195. }
  196. if (!clsiCookiesEnabled) {
  197. return callback(null, request.jar(), undefined)
  198. }
  199. return this._getServerId(
  200. project_id,
  201. user_id,
  202. compileGroup,
  203. (err, serverId) => {
  204. if (err != null) {
  205. OError.tag(err, 'error getting server id', {
  206. project_id,
  207. })
  208. return callback(err)
  209. }
  210. const serverCookie = request.cookie(
  211. `${Settings.clsiCookie.key}=${serverId}`
  212. )
  213. const jar = request.jar()
  214. jar.setCookie(serverCookie, Settings.apis.clsi.url)
  215. return callback(null, jar, serverId)
  216. }
  217. )
  218. },
  219. }
  220. }