RedisManager.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  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(keys.docVersion({ doc_id }), function (
  309. error,
  310. version
  311. ) {
  312. if (error != null) {
  313. return callback(error)
  314. }
  315. version = parseInt(version, 10)
  316. const first_version_in_redis = version - length
  317. if (start < first_version_in_redis || end > version) {
  318. error = new Errors.OpRangeNotAvailableError(
  319. 'doc ops range is not loaded in redis'
  320. )
  321. logger.warn(
  322. { err: error, doc_id, length, version, start, end },
  323. 'doc ops range is not loaded in redis'
  324. )
  325. return callback(error)
  326. }
  327. start = start - first_version_in_redis
  328. if (end > -1) {
  329. end = end - first_version_in_redis
  330. }
  331. if (isNaN(start) || isNaN(end)) {
  332. error = new Error('inconsistent version or lengths')
  333. logger.error(
  334. { err: error, doc_id, length, version, start, end },
  335. 'inconsistent version or length'
  336. )
  337. return callback(error)
  338. }
  339. return rclient.lrange(keys.docOps({ doc_id }), start, end, function (
  340. error,
  341. jsonOps
  342. ) {
  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. getHistoryType(doc_id, callback) {
  363. if (callback == null) {
  364. callback = function (error, projectHistoryType) {}
  365. }
  366. return rclient.get(keys.projectHistoryType({ doc_id }), function (
  367. error,
  368. projectHistoryType
  369. ) {
  370. if (error != null) {
  371. return callback(error)
  372. }
  373. return callback(null, projectHistoryType)
  374. })
  375. },
  376. setHistoryType(doc_id, projectHistoryType, callback) {
  377. if (callback == null) {
  378. callback = function (error) {}
  379. }
  380. return rclient.set(
  381. keys.projectHistoryType({ doc_id }),
  382. projectHistoryType,
  383. callback
  384. )
  385. },
  386. DOC_OPS_TTL: 60 * minutes,
  387. DOC_OPS_MAX_LENGTH: 100,
  388. updateDocument(
  389. project_id,
  390. doc_id,
  391. docLines,
  392. newVersion,
  393. appliedOps,
  394. ranges,
  395. updateMeta,
  396. callback
  397. ) {
  398. if (appliedOps == null) {
  399. appliedOps = []
  400. }
  401. if (callback == null) {
  402. callback = function (error) {}
  403. }
  404. return RedisManager.getDocVersion(doc_id, function (
  405. error,
  406. currentVersion,
  407. projectHistoryType
  408. ) {
  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. { doc_id, version: newVersion, hash: newHash, op_versions: opVersions },
  453. 'updating doc in redis'
  454. )
  455. // record bytes sent to redis in update
  456. metrics.summary('redis.docLines', newDocLines.length, {
  457. status: 'update'
  458. })
  459. return RedisManager._serializeRanges(ranges, function (error, ranges) {
  460. if (error != null) {
  461. logger.error({ err: error, doc_id }, error.message)
  462. return callback(error)
  463. }
  464. if (ranges != null && ranges.indexOf('\u0000') !== -1) {
  465. error = new Error('null bytes found in ranges')
  466. // this check was added to catch memory corruption in JSON.stringify
  467. logger.error({ err: error, doc_id, ranges }, error.message)
  468. return callback(error)
  469. }
  470. const multi = rclient.multi()
  471. multi.mset({
  472. [keys.docLines({ doc_id })]: newDocLines,
  473. [keys.docVersion({ doc_id })]: newVersion,
  474. [keys.docHash({ doc_id })]: newHash,
  475. [keys.ranges({ doc_id })]: ranges,
  476. [keys.lastUpdatedAt({ doc_id })]: Date.now(),
  477. [keys.lastUpdatedBy({ doc_id })]: updateMeta && updateMeta.user_id
  478. })
  479. multi.ltrim(
  480. keys.docOps({ doc_id }),
  481. -RedisManager.DOC_OPS_MAX_LENGTH,
  482. -1
  483. ) // index 3
  484. // push the ops last so we can get the lengths at fixed index position 7
  485. if (jsonOps.length > 0) {
  486. multi.rpush(keys.docOps({ doc_id }), ...Array.from(jsonOps)) // index 5
  487. // expire must come after rpush since before it will be a no-op if the list is empty
  488. multi.expire(keys.docOps({ doc_id }), RedisManager.DOC_OPS_TTL) // index 6
  489. if (projectHistoryType === 'project-history') {
  490. metrics.inc('history-queue', 1, { status: 'skip-track-changes' })
  491. logger.log(
  492. { doc_id },
  493. 'skipping push of uncompressed ops for project using project-history'
  494. )
  495. } else {
  496. // project is using old track-changes history service
  497. metrics.inc('history-queue', 1, { status: 'track-changes' })
  498. multi.rpush(
  499. historyKeys.uncompressedHistoryOps({ doc_id }),
  500. ...Array.from(jsonOps)
  501. ) // index 7
  502. }
  503. // Set the unflushed timestamp to the current time if the doc
  504. // hasn't been modified before (the content in mongo has been
  505. // valid up to this point). Otherwise leave it alone ("NX" flag).
  506. multi.set(keys.unflushedTime({ doc_id }), Date.now(), 'NX')
  507. }
  508. return multi.exec(function (error, result) {
  509. let docUpdateCount
  510. if (error != null) {
  511. return callback(error)
  512. }
  513. if (projectHistoryType === 'project-history') {
  514. docUpdateCount = undefined // only using project history, don't bother with track-changes
  515. } else {
  516. // project is using old track-changes history service
  517. docUpdateCount = result[4]
  518. }
  519. if (
  520. jsonOps.length > 0 &&
  521. __guard__(
  522. Settings.apis != null ? Settings.apis.project_history : undefined,
  523. (x) => x.enabled
  524. )
  525. ) {
  526. metrics.inc('history-queue', 1, { status: 'project-history' })
  527. return ProjectHistoryRedisManager.queueOps(
  528. project_id,
  529. ...Array.from(jsonOps),
  530. (error, projectUpdateCount) =>
  531. callback(null, docUpdateCount, projectUpdateCount)
  532. )
  533. } else {
  534. return callback(null, docUpdateCount)
  535. }
  536. })
  537. })
  538. })
  539. },
  540. renameDoc(project_id, doc_id, user_id, update, projectHistoryId, callback) {
  541. if (callback == null) {
  542. callback = function (error) {}
  543. }
  544. return RedisManager.getDoc(project_id, doc_id, function (
  545. error,
  546. lines,
  547. version
  548. ) {
  549. if (error != null) {
  550. return callback(error)
  551. }
  552. if (lines != null && version != null) {
  553. return rclient.set(
  554. keys.pathname({ doc_id }),
  555. update.newPathname,
  556. function (error) {
  557. if (error != null) {
  558. return callback(error)
  559. }
  560. return ProjectHistoryRedisManager.queueRenameEntity(
  561. project_id,
  562. projectHistoryId,
  563. 'doc',
  564. doc_id,
  565. user_id,
  566. update,
  567. callback
  568. )
  569. }
  570. )
  571. } else {
  572. return ProjectHistoryRedisManager.queueRenameEntity(
  573. project_id,
  574. projectHistoryId,
  575. 'doc',
  576. doc_id,
  577. user_id,
  578. update,
  579. callback
  580. )
  581. }
  582. })
  583. },
  584. clearUnflushedTime(doc_id, callback) {
  585. if (callback == null) {
  586. callback = function (error) {}
  587. }
  588. return rclient.del(keys.unflushedTime({ doc_id }), callback)
  589. },
  590. getDocIdsInProject(project_id, callback) {
  591. if (callback == null) {
  592. callback = function (error, doc_ids) {}
  593. }
  594. return rclient.smembers(keys.docsInProject({ project_id }), callback)
  595. },
  596. getDocTimestamps(doc_ids, callback) {
  597. // get lastupdatedat timestamps for an array of doc_ids
  598. if (callback == null) {
  599. callback = function (error, result) {}
  600. }
  601. return async.mapSeries(
  602. doc_ids,
  603. (doc_id, cb) => rclient.get(keys.lastUpdatedAt({ doc_id }), cb),
  604. callback
  605. )
  606. },
  607. queueFlushAndDeleteProject(project_id, callback) {
  608. // store the project id in a sorted set ordered by time with a random offset to smooth out spikes
  609. const SMOOTHING_OFFSET =
  610. Settings.smoothingOffset > 0
  611. ? Math.round(Settings.smoothingOffset * Math.random())
  612. : 0
  613. return rclient.zadd(
  614. keys.flushAndDeleteQueue(),
  615. Date.now() + SMOOTHING_OFFSET,
  616. project_id,
  617. callback
  618. )
  619. },
  620. getNextProjectToFlushAndDelete(cutoffTime, callback) {
  621. // find the oldest queued flush that is before the cutoff time
  622. if (callback == null) {
  623. callback = function (error, key, timestamp) {}
  624. }
  625. return rclient.zrangebyscore(
  626. keys.flushAndDeleteQueue(),
  627. 0,
  628. cutoffTime,
  629. 'WITHSCORES',
  630. 'LIMIT',
  631. 0,
  632. 1,
  633. function (err, reply) {
  634. if (err != null) {
  635. return callback(err)
  636. }
  637. if (!(reply != null ? reply.length : undefined)) {
  638. return callback()
  639. } // return if no projects ready to be processed
  640. // pop the oldest entry (get and remove in a multi)
  641. const multi = rclient.multi()
  642. // Poor man's version of ZPOPMIN, which is only available in Redis 5.
  643. multi.zrange(keys.flushAndDeleteQueue(), 0, 0, 'WITHSCORES')
  644. multi.zremrangebyrank(keys.flushAndDeleteQueue(), 0, 0)
  645. multi.zcard(keys.flushAndDeleteQueue()) // the total length of the queue (for metrics)
  646. return multi.exec(function (err, reply) {
  647. if (err != null) {
  648. return callback(err)
  649. }
  650. if (!(reply != null ? reply.length : undefined)) {
  651. return callback()
  652. }
  653. const [key, timestamp] = Array.from(reply[0])
  654. const queueLength = reply[2]
  655. return callback(null, key, timestamp, queueLength)
  656. })
  657. }
  658. )
  659. },
  660. _serializeRanges(ranges, callback) {
  661. if (callback == null) {
  662. callback = function (error, serializedRanges) {}
  663. }
  664. let jsonRanges = JSON.stringify(ranges)
  665. if (jsonRanges != null && jsonRanges.length > MAX_RANGES_SIZE) {
  666. return callback(new Error('ranges are too large'))
  667. }
  668. if (jsonRanges === '{}') {
  669. // Most doc will have empty ranges so don't fill redis with lots of '{}' keys
  670. jsonRanges = null
  671. }
  672. return callback(null, jsonRanges)
  673. },
  674. _deserializeRanges(ranges) {
  675. if (ranges == null || ranges === '') {
  676. return {}
  677. } else {
  678. return JSON.parse(ranges)
  679. }
  680. },
  681. _computeHash(docLines) {
  682. // use sha1 checksum of doclines to detect data corruption.
  683. //
  684. // note: must specify 'utf8' encoding explicitly, as the default is
  685. // binary in node < v5
  686. return crypto.createHash('sha1').update(docLines, 'utf8').digest('hex')
  687. }
  688. }
  689. function __guard__(value, transform) {
  690. return typeof value !== 'undefined' && value !== null
  691. ? transform(value)
  692. : undefined
  693. }