RedisManager.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS103: Rewrite code to no longer use __guard__
  12. * DS201: Simplify complex destructure assignments
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. let RedisManager
  17. const Settings = require('@overleaf/settings')
  18. const rclient = require('@overleaf/redis-wrapper').createClient(
  19. Settings.redis.documentupdater
  20. )
  21. const logger = require('logger-sharelatex')
  22. const metrics = require('./Metrics')
  23. const Errors = require('./Errors')
  24. const crypto = require('crypto')
  25. const async = require('async')
  26. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  27. // Sometimes Redis calls take an unexpectedly long time. We have to be
  28. // quick with Redis calls because we're holding a lock that expires
  29. // after 30 seconds. We can't let any errors in the rest of the stack
  30. // hold us up, and need to bail out quickly if there is a problem.
  31. const MAX_REDIS_REQUEST_LENGTH = 5000 // 5 seconds
  32. // Make times easy to read
  33. const minutes = 60 // seconds for Redis expire
  34. const logHashErrors =
  35. Settings.documentupdater != null
  36. ? Settings.documentupdater.logHashErrors
  37. : undefined
  38. const logHashReadErrors = logHashErrors != null ? logHashErrors.read : undefined
  39. const MEGABYTES = 1024 * 1024
  40. const MAX_RANGES_SIZE = 3 * MEGABYTES
  41. const keys = Settings.redis.documentupdater.key_schema
  42. const historyKeys = Settings.redis.history.key_schema // note: this is track changes, not project-history
  43. module.exports = RedisManager = {
  44. rclient,
  45. putDocInMemory(
  46. project_id,
  47. doc_id,
  48. docLines,
  49. version,
  50. ranges,
  51. pathname,
  52. projectHistoryId,
  53. _callback
  54. ) {
  55. const timer = new metrics.Timer('redis.put-doc')
  56. const callback = function (error) {
  57. timer.done()
  58. return _callback(error)
  59. }
  60. docLines = JSON.stringify(docLines)
  61. if (docLines.indexOf('\u0000') !== -1) {
  62. const error = new Error('null bytes found in doc lines')
  63. // this check was added to catch memory corruption in JSON.stringify.
  64. // It sometimes returned null bytes at the end of the string.
  65. logger.error({ err: error, doc_id, docLines }, error.message)
  66. return callback(error)
  67. }
  68. // Do a cheap size check on the serialized blob.
  69. if (docLines.length > Settings.max_doc_length) {
  70. const docSize = docLines.length
  71. const err = new Error('blocking doc insert into redis: doc is too large')
  72. logger.error({ project_id, doc_id, err, docSize }, err.message)
  73. return callback(err)
  74. }
  75. const docHash = RedisManager._computeHash(docLines)
  76. // record bytes sent to redis
  77. metrics.summary('redis.docLines', docLines.length, { status: 'set' })
  78. logger.log(
  79. { project_id, doc_id, version, docHash, pathname, projectHistoryId },
  80. 'putting doc in redis'
  81. )
  82. return RedisManager._serializeRanges(ranges, function (error, ranges) {
  83. if (error != null) {
  84. logger.error({ err: error, doc_id, project_id }, error.message)
  85. return callback(error)
  86. }
  87. // update docsInProject set before writing doc contents
  88. rclient.sadd(keys.docsInProject({ project_id }), doc_id, error => {
  89. if (error) return callback(error)
  90. rclient.mset(
  91. {
  92. [keys.docLines({ doc_id })]: docLines,
  93. [keys.projectKey({ doc_id })]: project_id,
  94. [keys.docVersion({ doc_id })]: version,
  95. [keys.docHash({ doc_id })]: docHash,
  96. [keys.ranges({ doc_id })]: ranges,
  97. [keys.pathname({ doc_id })]: pathname,
  98. [keys.projectHistoryId({ doc_id })]: projectHistoryId,
  99. },
  100. callback
  101. )
  102. })
  103. })
  104. },
  105. removeDocFromMemory(project_id, doc_id, _callback) {
  106. logger.log({ project_id, doc_id }, 'removing doc from redis')
  107. const callback = function (err) {
  108. if (err != null) {
  109. logger.err({ project_id, doc_id, err }, 'error removing doc from redis')
  110. return _callback(err)
  111. } else {
  112. logger.log({ project_id, doc_id }, 'removed doc from redis')
  113. return _callback()
  114. }
  115. }
  116. let multi = rclient.multi()
  117. multi.strlen(keys.docLines({ doc_id }))
  118. multi.del(
  119. keys.docLines({ doc_id }),
  120. keys.projectKey({ doc_id }),
  121. keys.docVersion({ doc_id }),
  122. keys.docHash({ doc_id }),
  123. keys.ranges({ doc_id }),
  124. keys.pathname({ doc_id }),
  125. keys.projectHistoryId({ doc_id }),
  126. keys.projectHistoryType({ doc_id }),
  127. keys.unflushedTime({ doc_id }),
  128. keys.lastUpdatedAt({ doc_id }),
  129. keys.lastUpdatedBy({ doc_id })
  130. )
  131. return multi.exec(function (error, response) {
  132. if (error != null) {
  133. return callback(error)
  134. }
  135. const length = response != null ? response[0] : undefined
  136. if (length > 0) {
  137. // record bytes freed in redis
  138. metrics.summary('redis.docLines', length, { status: 'del' })
  139. }
  140. multi = rclient.multi()
  141. multi.srem(keys.docsInProject({ project_id }), doc_id)
  142. multi.del(keys.projectState({ project_id }))
  143. return multi.exec(callback)
  144. })
  145. },
  146. checkOrSetProjectState(project_id, newState, callback) {
  147. if (callback == null) {
  148. callback = function (error, stateChanged) {}
  149. }
  150. const multi = rclient.multi()
  151. multi.getset(keys.projectState({ project_id }), newState)
  152. multi.expire(keys.projectState({ project_id }), 30 * minutes)
  153. return multi.exec(function (error, response) {
  154. if (error != null) {
  155. return callback(error)
  156. }
  157. logger.log(
  158. { project_id, newState, oldState: response[0] },
  159. 'checking project state'
  160. )
  161. return callback(null, response[0] !== newState)
  162. })
  163. },
  164. clearProjectState(project_id, callback) {
  165. if (callback == null) {
  166. callback = function (error) {}
  167. }
  168. return rclient.del(keys.projectState({ project_id }), callback)
  169. },
  170. getDoc(project_id, doc_id, callback) {
  171. if (callback == null) {
  172. callback = function (
  173. error,
  174. lines,
  175. version,
  176. ranges,
  177. pathname,
  178. projectHistoryId,
  179. unflushedTime
  180. ) {}
  181. }
  182. const timer = new metrics.Timer('redis.get-doc')
  183. const collectKeys = [
  184. keys.docLines({ doc_id }),
  185. keys.docVersion({ doc_id }),
  186. keys.docHash({ doc_id }),
  187. keys.projectKey({ doc_id }),
  188. keys.ranges({ doc_id }),
  189. keys.pathname({ doc_id }),
  190. keys.projectHistoryId({ doc_id }),
  191. keys.unflushedTime({ doc_id }),
  192. keys.lastUpdatedAt({ doc_id }),
  193. keys.lastUpdatedBy({ doc_id }),
  194. ]
  195. rclient.mget(...collectKeys, (error, ...rest) => {
  196. let [
  197. docLines,
  198. version,
  199. storedHash,
  200. doc_project_id,
  201. ranges,
  202. pathname,
  203. projectHistoryId,
  204. unflushedTime,
  205. lastUpdatedAt,
  206. lastUpdatedBy,
  207. ] = Array.from(rest[0])
  208. const timeSpan = timer.done()
  209. if (error != null) {
  210. return callback(error)
  211. }
  212. // check if request took too long and bail out. only do this for
  213. // get, because it is the first call in each update, so if this
  214. // passes we'll assume others have a reasonable chance to succeed.
  215. if (timeSpan > MAX_REDIS_REQUEST_LENGTH) {
  216. error = new Error('redis getDoc exceeded timeout')
  217. return callback(error)
  218. }
  219. // record bytes loaded from redis
  220. if (docLines != null) {
  221. metrics.summary('redis.docLines', docLines.length, { status: 'get' })
  222. }
  223. // check sha1 hash value if present
  224. if (docLines != null && storedHash != null) {
  225. const computedHash = RedisManager._computeHash(docLines)
  226. if (logHashReadErrors && computedHash !== storedHash) {
  227. logger.error(
  228. {
  229. project_id,
  230. doc_id,
  231. doc_project_id,
  232. computedHash,
  233. storedHash,
  234. docLines,
  235. },
  236. 'hash mismatch on retrieved document'
  237. )
  238. }
  239. }
  240. try {
  241. docLines = JSON.parse(docLines)
  242. ranges = RedisManager._deserializeRanges(ranges)
  243. } catch (e) {
  244. return callback(e)
  245. }
  246. version = parseInt(version || 0, 10)
  247. // check doc is in requested project
  248. if (doc_project_id != null && doc_project_id !== project_id) {
  249. logger.error(
  250. { project_id, doc_id, doc_project_id },
  251. 'doc not in project'
  252. )
  253. return callback(new Errors.NotFoundError('document not found'))
  254. }
  255. if (projectHistoryId != null) {
  256. projectHistoryId = parseInt(projectHistoryId)
  257. }
  258. callback(
  259. null,
  260. docLines,
  261. version,
  262. ranges,
  263. pathname,
  264. projectHistoryId,
  265. unflushedTime,
  266. lastUpdatedAt,
  267. lastUpdatedBy
  268. )
  269. })
  270. },
  271. getDocVersion(doc_id, callback) {
  272. if (callback == null) {
  273. callback = function (error, version, projectHistoryType) {}
  274. }
  275. return rclient.mget(
  276. keys.docVersion({ doc_id }),
  277. keys.projectHistoryType({ doc_id }),
  278. function (error, result) {
  279. if (error != null) {
  280. return callback(error)
  281. }
  282. let [version, projectHistoryType] = Array.from(result || [])
  283. version = parseInt(version, 10)
  284. return callback(null, version, projectHistoryType)
  285. }
  286. )
  287. },
  288. getDocLines(doc_id, callback) {
  289. if (callback == null) {
  290. callback = function (error, version) {}
  291. }
  292. return rclient.get(keys.docLines({ doc_id }), function (error, docLines) {
  293. if (error != null) {
  294. return callback(error)
  295. }
  296. return callback(null, docLines)
  297. })
  298. },
  299. getPreviousDocOps(doc_id, start, end, callback) {
  300. if (callback == null) {
  301. callback = function (error, jsonOps) {}
  302. }
  303. const timer = new metrics.Timer('redis.get-prev-docops')
  304. return rclient.llen(keys.docOps({ doc_id }), function (error, length) {
  305. if (error != null) {
  306. return callback(error)
  307. }
  308. return rclient.get(
  309. keys.docVersion({ doc_id }),
  310. function (error, version) {
  311. if (error != null) {
  312. return callback(error)
  313. }
  314. version = parseInt(version, 10)
  315. const first_version_in_redis = version - length
  316. if (start < first_version_in_redis || end > version) {
  317. error = new Errors.OpRangeNotAvailableError(
  318. 'doc ops range is not loaded in redis'
  319. )
  320. logger.warn(
  321. { err: error, doc_id, length, version, start, end },
  322. 'doc ops range is not loaded in redis'
  323. )
  324. return callback(error)
  325. }
  326. start = start - first_version_in_redis
  327. if (end > -1) {
  328. end = end - first_version_in_redis
  329. }
  330. if (isNaN(start) || isNaN(end)) {
  331. error = new Error('inconsistent version or lengths')
  332. logger.error(
  333. { err: error, doc_id, length, version, start, end },
  334. 'inconsistent version or length'
  335. )
  336. return callback(error)
  337. }
  338. return rclient.lrange(
  339. keys.docOps({ doc_id }),
  340. start,
  341. end,
  342. function (error, jsonOps) {
  343. let ops
  344. if (error != null) {
  345. return callback(error)
  346. }
  347. try {
  348. ops = jsonOps.map(jsonOp => JSON.parse(jsonOp))
  349. } catch (e) {
  350. return callback(e)
  351. }
  352. const timeSpan = timer.done()
  353. if (timeSpan > MAX_REDIS_REQUEST_LENGTH) {
  354. error = new Error('redis getPreviousDocOps exceeded timeout')
  355. return callback(error)
  356. }
  357. return callback(null, ops)
  358. }
  359. )
  360. }
  361. )
  362. })
  363. },
  364. getHistoryType(doc_id, callback) {
  365. if (callback == null) {
  366. callback = function (error, projectHistoryType) {}
  367. }
  368. return rclient.get(
  369. keys.projectHistoryType({ doc_id }),
  370. function (error, projectHistoryType) {
  371. if (error != null) {
  372. return callback(error)
  373. }
  374. return callback(null, projectHistoryType)
  375. }
  376. )
  377. },
  378. setHistoryType(doc_id, projectHistoryType, callback) {
  379. if (callback == null) {
  380. callback = function (error) {}
  381. }
  382. return rclient.set(
  383. keys.projectHistoryType({ doc_id }),
  384. projectHistoryType,
  385. callback
  386. )
  387. },
  388. DOC_OPS_TTL: 60 * minutes,
  389. DOC_OPS_MAX_LENGTH: 100,
  390. updateDocument(
  391. project_id,
  392. doc_id,
  393. docLines,
  394. newVersion,
  395. appliedOps,
  396. ranges,
  397. updateMeta,
  398. callback
  399. ) {
  400. if (appliedOps == null) {
  401. appliedOps = []
  402. }
  403. if (callback == null) {
  404. callback = function (error) {}
  405. }
  406. return RedisManager.getDocVersion(
  407. doc_id,
  408. function (error, currentVersion, projectHistoryType) {
  409. if (error != null) {
  410. return callback(error)
  411. }
  412. if (currentVersion + appliedOps.length !== newVersion) {
  413. error = new Error(`Version mismatch. '${doc_id}' is corrupted.`)
  414. logger.error(
  415. {
  416. err: error,
  417. doc_id,
  418. currentVersion,
  419. newVersion,
  420. opsLength: appliedOps.length,
  421. },
  422. 'version mismatch'
  423. )
  424. return callback(error)
  425. }
  426. const jsonOps = appliedOps.map(op => JSON.stringify(op))
  427. for (const op of Array.from(jsonOps)) {
  428. if (op.indexOf('\u0000') !== -1) {
  429. error = new Error('null bytes found in jsonOps')
  430. // this check was added to catch memory corruption in JSON.stringify
  431. logger.error({ err: error, doc_id, jsonOps }, error.message)
  432. return callback(error)
  433. }
  434. }
  435. const newDocLines = JSON.stringify(docLines)
  436. if (newDocLines.indexOf('\u0000') !== -1) {
  437. error = new Error('null bytes found in doc lines')
  438. // this check was added to catch memory corruption in JSON.stringify
  439. logger.error({ err: error, doc_id, newDocLines }, error.message)
  440. return callback(error)
  441. }
  442. // Do a cheap size check on the serialized blob.
  443. if (newDocLines.length > Settings.max_doc_length) {
  444. const err = new Error('blocking doc update: doc is too large')
  445. const docSize = newDocLines.length
  446. logger.error({ project_id, doc_id, err, docSize }, err.message)
  447. return callback(err)
  448. }
  449. const newHash = RedisManager._computeHash(newDocLines)
  450. const opVersions = appliedOps.map(op => (op != null ? op.v : undefined))
  451. logger.log(
  452. {
  453. doc_id,
  454. version: newVersion,
  455. hash: newHash,
  456. op_versions: opVersions,
  457. },
  458. 'updating doc in redis'
  459. )
  460. // record bytes sent to redis in update
  461. metrics.summary('redis.docLines', newDocLines.length, {
  462. status: 'update',
  463. })
  464. return RedisManager._serializeRanges(ranges, function (error, ranges) {
  465. if (error != null) {
  466. logger.error({ err: error, doc_id }, error.message)
  467. return callback(error)
  468. }
  469. if (ranges != null && ranges.indexOf('\u0000') !== -1) {
  470. error = new Error('null bytes found in ranges')
  471. // this check was added to catch memory corruption in JSON.stringify
  472. logger.error({ err: error, doc_id, ranges }, error.message)
  473. return callback(error)
  474. }
  475. const multi = rclient.multi()
  476. multi.mset({
  477. [keys.docLines({ doc_id })]: newDocLines,
  478. [keys.docVersion({ doc_id })]: newVersion,
  479. [keys.docHash({ doc_id })]: newHash,
  480. [keys.ranges({ doc_id })]: ranges,
  481. [keys.lastUpdatedAt({ doc_id })]: Date.now(),
  482. [keys.lastUpdatedBy({ doc_id })]: updateMeta && updateMeta.user_id,
  483. })
  484. multi.ltrim(
  485. keys.docOps({ doc_id }),
  486. -RedisManager.DOC_OPS_MAX_LENGTH,
  487. -1
  488. ) // index 3
  489. // push the ops last so we can get the lengths at fixed index position 7
  490. if (jsonOps.length > 0) {
  491. multi.rpush(keys.docOps({ doc_id }), ...Array.from(jsonOps)) // index 5
  492. // expire must come after rpush since before it will be a no-op if the list is empty
  493. multi.expire(keys.docOps({ doc_id }), RedisManager.DOC_OPS_TTL) // index 6
  494. if (projectHistoryType === 'project-history') {
  495. metrics.inc('history-queue', 1, { status: 'skip-track-changes' })
  496. logger.log(
  497. { doc_id },
  498. 'skipping push of uncompressed ops for project using project-history'
  499. )
  500. } else {
  501. // project is using old track-changes history service
  502. metrics.inc('history-queue', 1, { status: 'track-changes' })
  503. multi.rpush(
  504. historyKeys.uncompressedHistoryOps({ doc_id }),
  505. ...Array.from(jsonOps)
  506. ) // index 7
  507. }
  508. // Set the unflushed timestamp to the current time if the doc
  509. // hasn't been modified before (the content in mongo has been
  510. // valid up to this point). Otherwise leave it alone ("NX" flag).
  511. multi.set(keys.unflushedTime({ doc_id }), Date.now(), 'NX')
  512. }
  513. return multi.exec(function (error, result) {
  514. let docUpdateCount
  515. if (error != null) {
  516. return callback(error)
  517. }
  518. if (projectHistoryType === 'project-history') {
  519. docUpdateCount = undefined // only using project history, don't bother with track-changes
  520. } else {
  521. // project is using old track-changes history service
  522. docUpdateCount = result[4]
  523. }
  524. if (
  525. jsonOps.length > 0 &&
  526. __guard__(
  527. Settings.apis != null
  528. ? Settings.apis.project_history
  529. : undefined,
  530. x => x.enabled
  531. )
  532. ) {
  533. metrics.inc('history-queue', 1, { status: 'project-history' })
  534. return ProjectHistoryRedisManager.queueOps(
  535. project_id,
  536. ...Array.from(jsonOps),
  537. (error, projectUpdateCount) =>
  538. callback(null, docUpdateCount, projectUpdateCount)
  539. )
  540. } else {
  541. return callback(null, docUpdateCount)
  542. }
  543. })
  544. })
  545. }
  546. )
  547. },
  548. renameDoc(project_id, doc_id, user_id, update, projectHistoryId, callback) {
  549. if (callback == null) {
  550. callback = function (error) {}
  551. }
  552. return RedisManager.getDoc(
  553. project_id,
  554. doc_id,
  555. function (error, lines, version) {
  556. if (error != null) {
  557. return callback(error)
  558. }
  559. if (lines != null && version != null) {
  560. return rclient.set(
  561. keys.pathname({ doc_id }),
  562. update.newPathname,
  563. function (error) {
  564. if (error != null) {
  565. return callback(error)
  566. }
  567. return ProjectHistoryRedisManager.queueRenameEntity(
  568. project_id,
  569. projectHistoryId,
  570. 'doc',
  571. doc_id,
  572. user_id,
  573. update,
  574. callback
  575. )
  576. }
  577. )
  578. } else {
  579. return ProjectHistoryRedisManager.queueRenameEntity(
  580. project_id,
  581. projectHistoryId,
  582. 'doc',
  583. doc_id,
  584. user_id,
  585. update,
  586. callback
  587. )
  588. }
  589. }
  590. )
  591. },
  592. clearUnflushedTime(doc_id, callback) {
  593. if (callback == null) {
  594. callback = function (error) {}
  595. }
  596. return rclient.del(keys.unflushedTime({ doc_id }), callback)
  597. },
  598. getDocIdsInProject(project_id, callback) {
  599. if (callback == null) {
  600. callback = function (error, doc_ids) {}
  601. }
  602. return rclient.smembers(keys.docsInProject({ project_id }), callback)
  603. },
  604. getDocTimestamps(doc_ids, callback) {
  605. // get lastupdatedat timestamps for an array of doc_ids
  606. if (callback == null) {
  607. callback = function (error, result) {}
  608. }
  609. return async.mapSeries(
  610. doc_ids,
  611. (doc_id, cb) => rclient.get(keys.lastUpdatedAt({ doc_id }), cb),
  612. callback
  613. )
  614. },
  615. queueFlushAndDeleteProject(project_id, callback) {
  616. // store the project id in a sorted set ordered by time with a random offset to smooth out spikes
  617. const SMOOTHING_OFFSET =
  618. Settings.smoothingOffset > 0
  619. ? Math.round(Settings.smoothingOffset * Math.random())
  620. : 0
  621. return rclient.zadd(
  622. keys.flushAndDeleteQueue(),
  623. Date.now() + SMOOTHING_OFFSET,
  624. project_id,
  625. callback
  626. )
  627. },
  628. getNextProjectToFlushAndDelete(cutoffTime, callback) {
  629. // find the oldest queued flush that is before the cutoff time
  630. if (callback == null) {
  631. callback = function (error, key, timestamp) {}
  632. }
  633. return rclient.zrangebyscore(
  634. keys.flushAndDeleteQueue(),
  635. 0,
  636. cutoffTime,
  637. 'WITHSCORES',
  638. 'LIMIT',
  639. 0,
  640. 1,
  641. function (err, reply) {
  642. if (err != null) {
  643. return callback(err)
  644. }
  645. if (!(reply != null ? reply.length : undefined)) {
  646. return callback()
  647. } // return if no projects ready to be processed
  648. // pop the oldest entry (get and remove in a multi)
  649. const multi = rclient.multi()
  650. // Poor man's version of ZPOPMIN, which is only available in Redis 5.
  651. multi.zrange(keys.flushAndDeleteQueue(), 0, 0, 'WITHSCORES')
  652. multi.zremrangebyrank(keys.flushAndDeleteQueue(), 0, 0)
  653. multi.zcard(keys.flushAndDeleteQueue()) // the total length of the queue (for metrics)
  654. return multi.exec(function (err, reply) {
  655. if (err != null) {
  656. return callback(err)
  657. }
  658. if (!(reply != null ? reply.length : undefined)) {
  659. return callback()
  660. }
  661. const [key, timestamp] = Array.from(reply[0])
  662. const queueLength = reply[2]
  663. return callback(null, key, timestamp, queueLength)
  664. })
  665. }
  666. )
  667. },
  668. _serializeRanges(ranges, callback) {
  669. if (callback == null) {
  670. callback = function (error, serializedRanges) {}
  671. }
  672. let jsonRanges = JSON.stringify(ranges)
  673. if (jsonRanges != null && jsonRanges.length > MAX_RANGES_SIZE) {
  674. return callback(new Error('ranges are too large'))
  675. }
  676. if (jsonRanges === '{}') {
  677. // Most doc will have empty ranges so don't fill redis with lots of '{}' keys
  678. jsonRanges = null
  679. }
  680. return callback(null, jsonRanges)
  681. },
  682. _deserializeRanges(ranges) {
  683. if (ranges == null || ranges === '') {
  684. return {}
  685. } else {
  686. return JSON.parse(ranges)
  687. }
  688. },
  689. _computeHash(docLines) {
  690. // use sha1 checksum of doclines to detect data corruption.
  691. //
  692. // note: must specify 'utf8' encoding explicitly, as the default is
  693. // binary in node < v5
  694. return crypto.createHash('sha1').update(docLines, 'utf8').digest('hex')
  695. },
  696. }
  697. function __guard__(value, transform) {
  698. return typeof value !== 'undefined' && value !== null
  699. ? transform(value)
  700. : undefined
  701. }