RedisManager.js 24 KB

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