ClsiCookieManager.js 6.6 KB

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