RedisManager.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. const Settings = require('@overleaf/settings')
  2. const RedisWrapper = require('@overleaf/redis-wrapper')
  3. const logger = require('@overleaf/logger')
  4. const OError = require('@overleaf/o-error')
  5. const { callbackifyAll } = require('@overleaf/promise-utils')
  6. const metrics = require('./Metrics')
  7. const Errors = require('./Errors')
  8. const crypto = require('node:crypto')
  9. const { docIsTooLarge } = require('./Limits')
  10. const rclient = RedisWrapper.createClient(Settings.redis.documentupdater)
  11. // Sometimes Redis calls take an unexpectedly long time. We have to be
  12. // quick with Redis calls because we're holding a lock that expires
  13. // after 30 seconds. We can't let any errors in the rest of the stack
  14. // hold us up, and need to bail out quickly if there is a problem.
  15. const MAX_REDIS_REQUEST_LENGTH = 5000 // 5 seconds
  16. const PROJECT_BLOCK_TTL_SECS = 30
  17. // Make times easy to read
  18. const minutes = 60 // seconds for Redis expire
  19. const logHashReadErrors = Settings.documentupdater?.logHashErrors?.read
  20. const MEGABYTES = 1024 * 1024
  21. const MAX_RANGES_SIZE = 3 * MEGABYTES
  22. const keys = Settings.redis.documentupdater.key_schema
  23. const RedisManager = {
  24. async putDocInMemory(
  25. projectId,
  26. docId,
  27. docLines,
  28. version,
  29. ranges,
  30. resolvedCommentIds,
  31. pathname,
  32. projectHistoryId,
  33. historyRangesSupport
  34. ) {
  35. const timer = new metrics.Timer('redis.put-doc')
  36. const shareJSTextOT = Array.isArray(docLines)
  37. const docLinesArray = docLines
  38. docLines = JSON.stringify(docLines)
  39. if (docLines.indexOf('\u0000') !== -1) {
  40. // this check was added to catch memory corruption in JSON.stringify.
  41. // It sometimes returned null bytes at the end of the string.
  42. throw new OError('null bytes found in doc lines', { docId })
  43. }
  44. // Do an optimised size check on the docLines using the serialised
  45. // length as an upper bound
  46. const sizeBound = docLines.length
  47. if (
  48. shareJSTextOT && // editor-core has a size check in TextOperation.apply and TextOperation.applyToLength.
  49. docIsTooLarge(sizeBound, docLinesArray, Settings.max_doc_length)
  50. ) {
  51. const docSize = docLines.length
  52. throw new OError('blocking doc insert into redis: doc is too large', {
  53. projectId,
  54. docId,
  55. docSize,
  56. })
  57. }
  58. const docHash = RedisManager._computeHash(docLines)
  59. // record bytes sent to redis
  60. metrics.summary('redis.docLines', docLines.length, { status: 'set' })
  61. logger.debug(
  62. { projectId, docId, version, docHash, pathname, projectHistoryId },
  63. 'putting doc in redis'
  64. )
  65. ranges = RedisManager._serializeRanges(ranges)
  66. // update docsInProject set before writing doc contents
  67. const projectBlockMulti = rclient.multi()
  68. projectBlockMulti.exists(keys.projectBlock({ project_id: projectId }))
  69. projectBlockMulti.sadd(keys.docsInProject({ project_id: projectId }), docId)
  70. const reply = await projectBlockMulti.exec()
  71. const projectBlocked = reply[0] === 1
  72. if (projectBlocked) {
  73. // We don't clean up the spurious docId added in the docsInProject
  74. // set. There is a risk that the docId was successfully added by a
  75. // concurrent process. This set is used when unloading projects. An
  76. // extra docId will not prevent the project from being uploaded, but
  77. // a missing docId means that the doc might stay in Redis forever.
  78. throw new OError('Project blocked from loading docs', { projectId })
  79. }
  80. await RedisManager.setHistoryRangesSupportFlag(docId, historyRangesSupport)
  81. if (!pathname) {
  82. metrics.inc('pathname', 1, {
  83. path: 'RedisManager.setDoc',
  84. status: pathname === '' ? 'zero-length' : 'undefined',
  85. })
  86. }
  87. // Make sure that this MULTI operation only operates on doc
  88. // specific keys, i.e. keys that have the doc id in curly braces.
  89. // The curly braces identify a hash key for Redis and ensures that
  90. // the MULTI's operations are all done on the same node in a
  91. // cluster environment.
  92. const multi = rclient.multi()
  93. multi.mset({
  94. [keys.docLines({ doc_id: docId })]: docLines,
  95. [keys.projectKey({ doc_id: docId })]: projectId,
  96. [keys.docVersion({ doc_id: docId })]: version,
  97. [keys.docHash({ doc_id: docId })]: docHash,
  98. [keys.ranges({ doc_id: docId })]: ranges,
  99. [keys.pathname({ doc_id: docId })]: pathname,
  100. [keys.projectHistoryId({ doc_id: docId })]: projectHistoryId,
  101. })
  102. if (historyRangesSupport) {
  103. multi.del(keys.resolvedCommentIds({ doc_id: docId }))
  104. if (resolvedCommentIds.length > 0) {
  105. multi.sadd(
  106. keys.resolvedCommentIds({ doc_id: docId }),
  107. ...resolvedCommentIds
  108. )
  109. }
  110. }
  111. try {
  112. await multi.exec()
  113. } catch (err) {
  114. throw OError.tag(err, 'failed to write doc to Redis in MULTI', {
  115. previousErrors: err.previousErrors.map(e => ({
  116. name: e.name,
  117. message: e.message,
  118. command: e.command,
  119. })),
  120. })
  121. }
  122. timer.done()
  123. },
  124. async removeDocFromMemory(projectId, docId) {
  125. logger.debug({ projectId, docId }, 'removing doc from redis')
  126. // Make sure that this MULTI operation only operates on doc
  127. // specific keys, i.e. keys that have the doc id in curly braces.
  128. // The curly braces identify a hash key for Redis and ensures that
  129. // the MULTI's operations are all done on the same node in a
  130. // cluster environment.
  131. let multi = rclient.multi()
  132. multi.strlen(keys.docLines({ doc_id: docId }))
  133. multi.del(
  134. keys.docLines({ doc_id: docId }),
  135. keys.projectKey({ doc_id: docId }),
  136. keys.docVersion({ doc_id: docId }),
  137. keys.docHash({ doc_id: docId }),
  138. keys.ranges({ doc_id: docId }),
  139. keys.pathname({ doc_id: docId }),
  140. keys.projectHistoryId({ doc_id: docId }),
  141. keys.unflushedTime({ doc_id: docId }),
  142. keys.lastUpdatedAt({ doc_id: docId }),
  143. keys.lastUpdatedBy({ doc_id: docId }),
  144. keys.resolvedCommentIds({ doc_id: docId })
  145. )
  146. const response = await multi.exec()
  147. const length = response?.[0]
  148. if (length > 0) {
  149. // record bytes freed in redis
  150. metrics.summary('redis.docLines', length, { status: 'del' })
  151. }
  152. // Make sure that this MULTI operation only operates on project
  153. // specific keys, i.e. keys that have the project id in curly braces.
  154. // The curly braces identify a hash key for Redis and ensures that
  155. // the MULTI's operations are all done on the same node in a
  156. // cluster environment.
  157. multi = rclient.multi()
  158. multi.srem(keys.docsInProject({ project_id: projectId }), docId)
  159. multi.del(keys.projectState({ project_id: projectId }))
  160. await multi.exec()
  161. await rclient.srem(keys.historyRangesSupport(), docId)
  162. },
  163. async checkOrSetProjectState(projectId, newState) {
  164. // Make sure that this MULTI operation only operates on project
  165. // specific keys, i.e. keys that have the project id in curly braces.
  166. // The curly braces identify a hash key for Redis and ensures that
  167. // the MULTI's operations are all done on the same node in a
  168. // cluster environment.
  169. const multi = rclient.multi()
  170. multi.getset(keys.projectState({ project_id: projectId }), newState)
  171. multi.expire(keys.projectState({ project_id: projectId }), 30 * minutes)
  172. const response = await multi.exec()
  173. logger.debug(
  174. { projectId, newState, oldState: response[0] },
  175. 'checking project state'
  176. )
  177. return response[0] !== newState
  178. },
  179. async clearProjectState(projectId) {
  180. await rclient.del(keys.projectState({ project_id: projectId }))
  181. },
  182. async getDoc(projectId, docId) {
  183. const timer = new metrics.Timer('redis.get-doc')
  184. const collectKeys = [
  185. keys.docLines({ doc_id: docId }),
  186. keys.docVersion({ doc_id: docId }),
  187. keys.docHash({ doc_id: docId }),
  188. keys.projectKey({ doc_id: docId }),
  189. keys.ranges({ doc_id: docId }),
  190. keys.pathname({ doc_id: docId }),
  191. keys.projectHistoryId({ doc_id: docId }),
  192. keys.unflushedTime({ doc_id: docId }),
  193. keys.lastUpdatedAt({ doc_id: docId }),
  194. keys.lastUpdatedBy({ doc_id: docId }),
  195. ]
  196. let [
  197. docLines,
  198. version,
  199. storedHash,
  200. docProjectId,
  201. ranges,
  202. pathname,
  203. projectHistoryId,
  204. unflushedTime,
  205. lastUpdatedAt,
  206. lastUpdatedBy,
  207. ] = await rclient.mget(...collectKeys)
  208. const result = await rclient.sismember(keys.historyRangesSupport(), docId)
  209. const historyRangesSupport = result === 1
  210. const resolvedCommentIds = await rclient.smembers(
  211. keys.resolvedCommentIds({ doc_id: docId })
  212. )
  213. const timeSpan = timer.done()
  214. // check if request took too long and bail out. only do this for
  215. // get, because it is the first call in each update, so if this
  216. // passes we'll assume others have a reasonable chance to succeed.
  217. if (timeSpan > MAX_REDIS_REQUEST_LENGTH) {
  218. throw new OError('redis getDoc exceeded timeout', { projectId, docId })
  219. }
  220. // record bytes loaded from redis
  221. if (docLines != null) {
  222. metrics.summary('redis.docLines', docLines.length, {
  223. status: 'get',
  224. })
  225. }
  226. // check sha1 hash value if present
  227. if (docLines != null && storedHash != null) {
  228. const computedHash = RedisManager._computeHash(docLines)
  229. if (logHashReadErrors && computedHash !== storedHash) {
  230. logger.error(
  231. {
  232. projectId,
  233. docId,
  234. docProjectId,
  235. computedHash,
  236. storedHash,
  237. docLines,
  238. },
  239. 'hash mismatch on retrieved document'
  240. )
  241. }
  242. }
  243. docLines = JSON.parse(docLines)
  244. ranges = RedisManager._deserializeRanges(ranges)
  245. version = parseInt(version || 0, 10)
  246. // check doc is in requested project
  247. if (docProjectId != null && docProjectId !== projectId) {
  248. throw new Errors.NotFoundError('document not found', {
  249. projectId,
  250. docId,
  251. docProjectId,
  252. })
  253. }
  254. if (docLines && version && !pathname) {
  255. metrics.inc('pathname', 1, {
  256. path: 'RedisManager.getDoc',
  257. status: pathname === '' ? 'zero-length' : 'undefined',
  258. })
  259. }
  260. return {
  261. lines: docLines,
  262. version,
  263. ranges,
  264. pathname,
  265. projectHistoryId,
  266. unflushedTime,
  267. lastUpdatedAt,
  268. lastUpdatedBy,
  269. historyRangesSupport,
  270. resolvedCommentIds,
  271. }
  272. },
  273. async getDocRanges(docId) {
  274. const json = await rclient.get(keys.ranges({ doc_id: docId }))
  275. const ranges = RedisManager._deserializeRanges(json)
  276. return ranges
  277. },
  278. async getDocVersion(docId) {
  279. const result = await rclient.mget(keys.docVersion({ doc_id: docId }))
  280. let [version] = result || []
  281. version = parseInt(version, 10)
  282. return version
  283. },
  284. async getDocLines(docId) {
  285. const docLines = await rclient.get(keys.docLines({ doc_id: docId }))
  286. return docLines
  287. },
  288. async getPreviousDocOps(docId, start, end) {
  289. const timer = new metrics.Timer('redis.get-prev-docops')
  290. const length = await rclient.llen(keys.docOps({ doc_id: docId }))
  291. let version = await rclient.get(keys.docVersion({ doc_id: docId }))
  292. version = parseInt(version, 10)
  293. const firstVersionInRedis = version - length
  294. if (start < firstVersionInRedis || end > version) {
  295. throw new Errors.OpRangeNotAvailableError(
  296. 'doc ops range is not loaded in redis',
  297. { firstVersionInRedis, version, ttlInS: RedisManager.DOC_OPS_TTL }
  298. )
  299. }
  300. start = start - firstVersionInRedis
  301. if (end > -1) {
  302. end = end - firstVersionInRedis
  303. }
  304. if (isNaN(start) || isNaN(end)) {
  305. throw new OError('inconsistent version or lengths', {
  306. docId,
  307. length,
  308. version,
  309. start,
  310. end,
  311. })
  312. }
  313. const jsonOps = await rclient.lrange(
  314. keys.docOps({ doc_id: docId }),
  315. start,
  316. end
  317. )
  318. const ops = jsonOps.map(jsonOp => JSON.parse(jsonOp))
  319. const timeSpan = timer.done()
  320. if (timeSpan > MAX_REDIS_REQUEST_LENGTH) {
  321. throw new Error('redis getPreviousDocOps exceeded timeout')
  322. }
  323. return ops
  324. },
  325. DOC_OPS_TTL: 60 * minutes,
  326. DOC_OPS_MAX_LENGTH: 100,
  327. async updateDocument(
  328. projectId,
  329. docId,
  330. docLines,
  331. newVersion,
  332. appliedOps,
  333. ranges,
  334. updateMeta
  335. ) {
  336. if (appliedOps == null) {
  337. appliedOps = []
  338. }
  339. const shareJSTextOT = Array.isArray(docLines)
  340. const currentVersion = await RedisManager.getDocVersion(docId)
  341. if (currentVersion + appliedOps.length !== newVersion) {
  342. throw new OError('Version mismatch. doc is corrupted', {
  343. docId,
  344. currentVersion,
  345. newVersion,
  346. opsLength: appliedOps.length,
  347. })
  348. }
  349. const jsonOps = appliedOps.map(op => JSON.stringify(op))
  350. for (const op of jsonOps) {
  351. if (op.indexOf('\u0000') !== -1) {
  352. // this check was added to catch memory corruption in JSON.stringify
  353. throw new OError('null bytes found in jsonOps', {
  354. docId,
  355. jsonOps,
  356. })
  357. }
  358. }
  359. const newDocLines = JSON.stringify(docLines)
  360. if (newDocLines.indexOf('\u0000') !== -1) {
  361. // this check was added to catch memory corruption in JSON.stringify
  362. throw new OError('null bytes found in doc lines', {
  363. docId,
  364. newDocLines,
  365. })
  366. }
  367. // Do an optimised size check on the docLines using the serialised
  368. // length as an upper bound
  369. const sizeBound = newDocLines.length
  370. if (
  371. shareJSTextOT && // editor-core has a size check in TextOperation.apply and TextOperation.applyToLength.
  372. docIsTooLarge(sizeBound, docLines, Settings.max_doc_length)
  373. ) {
  374. const docSize = newDocLines.length
  375. throw new OError('blocking doc update: doc is too large', {
  376. projectId,
  377. docId,
  378. docSize,
  379. })
  380. }
  381. const newHash = RedisManager._computeHash(newDocLines)
  382. const opVersions = appliedOps.map(op => op?.v)
  383. logger.debug(
  384. {
  385. docId,
  386. version: newVersion,
  387. hash: newHash,
  388. opVersions,
  389. },
  390. 'updating doc in redis'
  391. )
  392. // record bytes sent to redis in update
  393. metrics.summary('redis.docLines', newDocLines.length, {
  394. status: 'update',
  395. })
  396. const jsonRanges = RedisManager._serializeRanges(ranges)
  397. if (jsonRanges && jsonRanges.indexOf('\u0000') !== -1) {
  398. // this check was added to catch memory corruption in JSON.stringify
  399. throw new OError('null bytes found in ranges', { docId })
  400. }
  401. // Make sure that this MULTI operation only operates on doc
  402. // specific keys, i.e. keys that have the doc id in curly braces.
  403. // The curly braces identify a hash key for Redis and ensures that
  404. // the MULTI's operations are all done on the same node in a
  405. // cluster environment.
  406. const multi = rclient.multi()
  407. multi.mset({
  408. [keys.docLines({ doc_id: docId })]: newDocLines,
  409. [keys.docVersion({ doc_id: docId })]: newVersion,
  410. [keys.docHash({ doc_id: docId })]: newHash,
  411. [keys.ranges({ doc_id: docId })]: jsonRanges,
  412. [keys.lastUpdatedAt({ doc_id: docId })]: Date.now(),
  413. [keys.lastUpdatedBy({ doc_id: docId })]: updateMeta && updateMeta.user_id,
  414. })
  415. multi.ltrim(
  416. keys.docOps({ doc_id: docId }),
  417. -RedisManager.DOC_OPS_MAX_LENGTH,
  418. -1
  419. ) // index 3
  420. // push the ops last so we can get the lengths at fixed index position 7
  421. if (jsonOps.length > 0) {
  422. multi.rpush(keys.docOps({ doc_id: docId }), ...jsonOps) // index 5
  423. // expire must come after rpush since before it will be a no-op if the list is empty
  424. multi.expire(keys.docOps({ doc_id: docId }), RedisManager.DOC_OPS_TTL) // index 6
  425. }
  426. // Set the unflushed timestamp to the current time if not set ("NX" flag).
  427. multi.set(keys.unflushedTime({ doc_id: docId }), Date.now(), 'NX')
  428. await multi.exec()
  429. },
  430. async renameDoc(projectId, docId, userId, update, projectHistoryId) {
  431. const { lines, version } = await RedisManager.getDoc(projectId, docId)
  432. if (lines != null && version != null) {
  433. if (!update.newPathname) {
  434. logger.warn(
  435. { projectId, docId, update },
  436. 'missing pathname in RedisManager.renameDoc'
  437. )
  438. metrics.inc('pathname', 1, {
  439. path: 'RedisManager.renameDoc',
  440. status: update.newPathname === '' ? 'zero-length' : 'undefined',
  441. })
  442. }
  443. await rclient.set(keys.pathname({ doc_id: docId }), update.newPathname)
  444. }
  445. },
  446. async clearUnflushedTime(docId) {
  447. await rclient.del(keys.unflushedTime({ doc_id: docId }))
  448. },
  449. async updateCommentState(docId, commentId, resolved) {
  450. if (resolved) {
  451. await rclient.sadd(keys.resolvedCommentIds({ doc_id: docId }), commentId)
  452. } else {
  453. await rclient.srem(keys.resolvedCommentIds({ doc_id: docId }), commentId)
  454. }
  455. },
  456. async getDocIdsInProject(projectId) {
  457. return await rclient.smembers(keys.docsInProject({ project_id: projectId }))
  458. },
  459. /**
  460. * Get lastupdatedat timestamps for an array of docIds
  461. */
  462. async getDocTimestamps(docIds) {
  463. const timestamps = []
  464. for (const docId of docIds) {
  465. const timestamp = await rclient.get(keys.lastUpdatedAt({ doc_id: docId }))
  466. timestamps.push(timestamp)
  467. }
  468. return timestamps
  469. },
  470. /**
  471. * Store the project id in a sorted set ordered by time with a random offset
  472. * to smooth out spikes
  473. */
  474. async queueFlushAndDeleteProject(projectId) {
  475. const SMOOTHING_OFFSET =
  476. Settings.smoothingOffset > 0
  477. ? Math.round(Settings.smoothingOffset * Math.random())
  478. : 0
  479. await rclient.zadd(
  480. keys.flushAndDeleteQueue(),
  481. Date.now() + SMOOTHING_OFFSET,
  482. projectId
  483. )
  484. },
  485. /**
  486. * Find the oldest queued flush that is before the cutoff time
  487. */
  488. async getNextProjectToFlushAndDelete(cutoffTime) {
  489. const projectsReady = await rclient.zrangebyscore(
  490. keys.flushAndDeleteQueue(),
  491. 0,
  492. cutoffTime,
  493. 'WITHSCORES',
  494. 'LIMIT',
  495. 0,
  496. 1
  497. )
  498. // return if no projects ready to be processed
  499. if (!projectsReady || projectsReady.length === 0) {
  500. return {}
  501. }
  502. // pop the oldest entry (get and remove in a multi)
  503. const multi = rclient.multi()
  504. // Poor man's version of ZPOPMIN, which is only available in Redis 5.
  505. multi.zrange(keys.flushAndDeleteQueue(), 0, 0, 'WITHSCORES')
  506. multi.zremrangebyrank(keys.flushAndDeleteQueue(), 0, 0)
  507. multi.zcard(keys.flushAndDeleteQueue()) // the total length of the queue (for metrics)
  508. const reply = await multi.exec()
  509. if (!reply || reply.length === 0) {
  510. return {}
  511. }
  512. const [key, timestamp] = reply[0]
  513. const queueLength = reply[2]
  514. return { projectId: key, flushTimestamp: timestamp, queueLength }
  515. },
  516. async setHistoryRangesSupportFlag(docId, historyRangesSupport) {
  517. if (historyRangesSupport) {
  518. await rclient.sadd(keys.historyRangesSupport(), docId)
  519. } else {
  520. await rclient.srem(keys.historyRangesSupport(), docId)
  521. }
  522. },
  523. async blockProject(projectId) {
  524. // Make sure that this MULTI operation only operates on project
  525. // specific keys, i.e. keys that have the project id in curly braces.
  526. // The curly braces identify a hash key for Redis and ensures that
  527. // the MULTI's operations are all done on the same node in a
  528. // cluster environment.
  529. const multi = rclient.multi()
  530. multi.setex(
  531. keys.projectBlock({ project_id: projectId }),
  532. PROJECT_BLOCK_TTL_SECS,
  533. '1'
  534. )
  535. multi.scard(keys.docsInProject({ project_id: projectId }))
  536. const reply = await multi.exec()
  537. const docsInProject = reply[1]
  538. if (docsInProject > 0) {
  539. // Too late to lock the project
  540. await rclient.del(keys.projectBlock({ project_id: projectId }))
  541. return false
  542. }
  543. return true
  544. },
  545. async unblockProject(projectId) {
  546. const reply = await rclient.del(
  547. keys.projectBlock({ project_id: projectId })
  548. )
  549. const wasBlocked = reply === 1
  550. return wasBlocked
  551. },
  552. _serializeRanges(ranges) {
  553. let jsonRanges = JSON.stringify(ranges)
  554. if (jsonRanges && jsonRanges.length > MAX_RANGES_SIZE) {
  555. throw new Error('ranges are too large')
  556. }
  557. if (jsonRanges === '{}') {
  558. // Most doc will have empty ranges so don't fill redis with lots of '{}' keys
  559. jsonRanges = null
  560. }
  561. return jsonRanges
  562. },
  563. _deserializeRanges(ranges) {
  564. if (ranges == null || ranges === '') {
  565. return {}
  566. } else {
  567. return JSON.parse(ranges)
  568. }
  569. },
  570. _computeHash(docLines) {
  571. // use sha1 checksum of doclines to detect data corruption.
  572. //
  573. // note: must specify 'utf8' encoding explicitly, as the default is
  574. // binary in node < v5
  575. return crypto.createHash('sha1').update(docLines, 'utf8').digest('hex')
  576. },
  577. async cleanupTestRedis() {
  578. await RedisWrapper.cleanupTestRedis(rclient)
  579. },
  580. }
  581. module.exports = {
  582. rclient,
  583. ...callbackifyAll(RedisManager, {
  584. multiResult: {
  585. getDoc: [
  586. 'lines',
  587. 'version',
  588. 'ranges',
  589. 'pathname',
  590. 'projectHistoryId',
  591. 'unflushedTime',
  592. 'lastUpdatedAt',
  593. 'lastUpdatedBy',
  594. 'historyRangesSupport',
  595. 'resolvedCommentIds',
  596. ],
  597. getNextProjectToFlushAndDelete: [
  598. 'projectId',
  599. 'flushTimestamp',
  600. 'queueLength',
  601. ],
  602. },
  603. }),
  604. promises: RedisManager,
  605. }