RedisManagerTests.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. import { expect } from 'chai'
  2. import sinon from 'sinon'
  3. import { strict as esmock } from 'esmock'
  4. const MODULE_PATH = '../../../../app/js/RedisManager.js'
  5. describe('RedisManager', function () {
  6. beforeEach(async function () {
  7. this.rclient = new FakeRedis()
  8. this.RedisWrapper = {
  9. createClient: sinon.stub().returns(this.rclient),
  10. }
  11. this.Settings = {
  12. redis: {
  13. project_history: {
  14. key_schema: {
  15. projectHistoryOps({ project_id: projectId }) {
  16. return `Project:HistoryOps:{${projectId}}`
  17. },
  18. projectHistoryFirstOpTimestamp({ project_id: projectId }) {
  19. return `ProjectHistory:FirstOpTimestamp:{${projectId}}`
  20. },
  21. },
  22. },
  23. },
  24. }
  25. this.Metrics = {
  26. timing: sinon.stub(),
  27. summary: sinon.stub(),
  28. globalGauge: sinon.stub(),
  29. }
  30. this.RedisManager = await esmock(MODULE_PATH, {
  31. '@overleaf/redis-wrapper': this.RedisWrapper,
  32. '@overleaf/settings': this.Settings,
  33. '@overleaf/metrics': this.Metrics,
  34. })
  35. this.projectId = 'project-id-123'
  36. this.batchSize = 100
  37. this.historyOpsKey = `Project:HistoryOps:{${this.projectId}}`
  38. this.firstOpTimestampKey = `ProjectHistory:FirstOpTimestamp:{${this.projectId}}`
  39. this.updates = [
  40. { v: 42, op: ['a', 'b', 'c', 'd'] },
  41. { v: 45, op: ['e', 'f', 'g', 'h'] },
  42. ]
  43. this.extraUpdates = [{ v: 100, op: ['i', 'j', 'k'] }]
  44. this.rawUpdates = this.updates.map(update => JSON.stringify(update))
  45. this.extraRawUpdates = this.extraUpdates.map(update =>
  46. JSON.stringify(update)
  47. )
  48. })
  49. describe('getRawUpdatesBatch', function () {
  50. it('gets a small number of updates in one batch', async function () {
  51. const updates = makeUpdates(2)
  52. const rawUpdates = makeRawUpdates(updates)
  53. this.rclient.setList(this.historyOpsKey, rawUpdates)
  54. const result = await this.RedisManager.promises.getRawUpdatesBatch(
  55. this.projectId,
  56. 100
  57. )
  58. expect(result).to.deep.equal({ rawUpdates, hasMore: false })
  59. })
  60. it('gets a larger number of updates in several batches', async function () {
  61. const updates = makeUpdates(
  62. this.RedisManager.RAW_UPDATES_BATCH_SIZE * 2 + 12
  63. )
  64. const rawUpdates = makeRawUpdates(updates)
  65. this.rclient.setList(this.historyOpsKey, rawUpdates)
  66. const result = await this.RedisManager.promises.getRawUpdatesBatch(
  67. this.projectId,
  68. 5000
  69. )
  70. expect(result).to.deep.equal({ rawUpdates, hasMore: false })
  71. })
  72. it("doesn't return more than the number of updates requested", async function () {
  73. const updates = makeUpdates(100)
  74. const rawUpdates = makeRawUpdates(updates)
  75. this.rclient.setList(this.historyOpsKey, rawUpdates)
  76. const result = await this.RedisManager.promises.getRawUpdatesBatch(
  77. this.projectId,
  78. 75
  79. )
  80. expect(result).to.deep.equal({
  81. rawUpdates: rawUpdates.slice(0, 75),
  82. hasMore: true,
  83. })
  84. })
  85. })
  86. describe('parseDocUpdates', function () {
  87. it('should return the parsed ops', function () {
  88. const updates = makeUpdates(12)
  89. const rawUpdates = makeRawUpdates(updates)
  90. this.RedisManager.parseDocUpdates(rawUpdates).should.deep.equal(updates)
  91. })
  92. })
  93. describe('getUpdatesInBatches', function () {
  94. beforeEach(function () {
  95. this.runner = sinon.stub().resolves()
  96. })
  97. describe('single batch smaller than batch size', function () {
  98. beforeEach(async function () {
  99. this.updates = makeUpdates(2)
  100. this.rawUpdates = makeRawUpdates(this.updates)
  101. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  102. await this.RedisManager.promises.getUpdatesInBatches(
  103. this.projectId,
  104. 3,
  105. this.runner
  106. )
  107. })
  108. it('calls the runner once', function () {
  109. this.runner.callCount.should.equal(1)
  110. })
  111. it('calls the runner with the updates', function () {
  112. this.runner.should.have.been.calledWith(this.updates)
  113. })
  114. it('deletes the applied updates', function () {
  115. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  116. })
  117. it('deletes the first op timestamp', function () {
  118. expect(this.rclient.del).to.have.been.calledWith(
  119. this.firstOpTimestampKey
  120. )
  121. })
  122. })
  123. describe('single batch at batch size', function () {
  124. beforeEach(async function () {
  125. this.updates = makeUpdates(123)
  126. this.rawUpdates = makeRawUpdates(this.updates)
  127. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  128. await this.RedisManager.promises.getUpdatesInBatches(
  129. this.projectId,
  130. 123,
  131. this.runner
  132. )
  133. })
  134. it('calls the runner once', function () {
  135. this.runner.callCount.should.equal(1)
  136. })
  137. it('calls the runner with the updates', function () {
  138. this.runner.should.have.been.calledWith(this.updates)
  139. })
  140. it('deletes the applied updates', function () {
  141. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  142. })
  143. it('deletes the first op timestamp', function () {
  144. expect(this.rclient.del).to.have.been.calledWith(
  145. this.firstOpTimestampKey
  146. )
  147. })
  148. })
  149. describe('single batch exceeding size limit on updates', function () {
  150. beforeEach(async function () {
  151. this.updates = makeUpdates(2, [
  152. 'x'.repeat(this.RedisManager.RAW_UPDATE_SIZE_THRESHOLD),
  153. ])
  154. this.rawUpdates = makeRawUpdates(this.updates)
  155. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  156. await this.RedisManager.promises.getUpdatesInBatches(
  157. this.projectId,
  158. 123,
  159. this.runner
  160. )
  161. })
  162. it('calls the runner twice', function () {
  163. this.runner.callCount.should.equal(2)
  164. })
  165. it('calls the runner with the first update', function () {
  166. this.runner
  167. .getCall(0)
  168. .should.have.been.calledWith(this.updates.slice(0, 1))
  169. })
  170. it('calls the runner with the second update', function () {
  171. this.runner
  172. .getCall(1)
  173. .should.have.been.calledWith(this.updates.slice(1))
  174. })
  175. it('deletes the applied updates', function () {
  176. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  177. })
  178. })
  179. describe('two batches with first update below and second update above the size limit on updates', function () {
  180. beforeEach(async function () {
  181. this.updates = makeUpdates(2, [
  182. 'x'.repeat(this.RedisManager.RAW_UPDATE_SIZE_THRESHOLD / 2),
  183. ])
  184. this.rawUpdates = makeRawUpdates(this.updates)
  185. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  186. await this.RedisManager.promises.getUpdatesInBatches(
  187. this.projectId,
  188. 123,
  189. this.runner
  190. )
  191. })
  192. it('calls the runner twice', function () {
  193. this.runner.callCount.should.equal(2)
  194. })
  195. it('calls the runner with the first update', function () {
  196. this.runner
  197. .getCall(0)
  198. .should.have.been.calledWith(this.updates.slice(0, 1))
  199. })
  200. it('calls the runner with the second update', function () {
  201. this.runner
  202. .getCall(1)
  203. .should.have.been.calledWith(this.updates.slice(1))
  204. })
  205. it('deletes the applied updates', function () {
  206. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  207. })
  208. })
  209. describe('single batch exceeding op count limit on updates', function () {
  210. beforeEach(async function () {
  211. const ops = Array(this.RedisManager.MAX_UPDATE_OP_LENGTH + 1).fill('op')
  212. this.updates = makeUpdates(2, { op: ops })
  213. this.rawUpdates = makeRawUpdates(this.updates)
  214. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  215. await this.RedisManager.promises.getUpdatesInBatches(
  216. this.projectId,
  217. 123,
  218. this.runner
  219. )
  220. })
  221. it('calls the runner twice', function () {
  222. this.runner.callCount.should.equal(2)
  223. })
  224. it('calls the runner with the first update', function () {
  225. this.runner
  226. .getCall(0)
  227. .should.have.been.calledWith(this.updates.slice(0, 1))
  228. })
  229. it('calls the runner with the second update', function () {
  230. this.runner
  231. .getCall(1)
  232. .should.have.been.calledWith(this.updates.slice(1))
  233. })
  234. it('deletes the applied updates', function () {
  235. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  236. })
  237. })
  238. describe('single batch exceeding doc content count', function () {
  239. beforeEach(async function () {
  240. this.updates = makeUpdates(
  241. this.RedisManager.MAX_NEW_DOC_CONTENT_COUNT + 3,
  242. { resyncDocContent: 123 }
  243. )
  244. this.rawUpdates = makeRawUpdates(this.updates)
  245. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  246. await this.RedisManager.promises.getUpdatesInBatches(
  247. this.projectId,
  248. 123,
  249. this.runner
  250. )
  251. })
  252. it('calls the runner twice', function () {
  253. this.runner.callCount.should.equal(2)
  254. })
  255. it('calls the runner with the first batch of updates', function () {
  256. this.runner.should.have.been.calledWith(
  257. this.updates.slice(0, this.RedisManager.MAX_NEW_DOC_CONTENT_COUNT)
  258. )
  259. })
  260. it('calls the runner with the second batch of updates', function () {
  261. this.runner.should.have.been.calledWith(
  262. this.updates.slice(this.RedisManager.MAX_NEW_DOC_CONTENT_COUNT)
  263. )
  264. })
  265. it('deletes the applied updates', function () {
  266. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  267. })
  268. })
  269. describe('two batches with first update below and second update above the ops length limit on updates', function () {
  270. beforeEach(async function () {
  271. // set the threshold below the size of the first update
  272. this.updates = makeUpdates(2, { op: ['op1', 'op2'] })
  273. this.updates[1].op = Array(
  274. this.RedisManager.MAX_UPDATE_OP_LENGTH + 2
  275. ).fill('op')
  276. this.rawUpdates = makeRawUpdates(this.updates)
  277. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  278. await this.RedisManager.promises.getUpdatesInBatches(
  279. this.projectId,
  280. 123,
  281. this.runner
  282. )
  283. })
  284. it('calls the runner twice', function () {
  285. this.runner.callCount.should.equal(2)
  286. })
  287. it('calls the runner with the first update', function () {
  288. this.runner.should.have.been.calledWith(this.updates.slice(0, 1))
  289. })
  290. it('calls the runner with the second update', function () {
  291. this.runner.should.have.been.calledWith(this.updates.slice(1))
  292. })
  293. it('deletes the applied updates', function () {
  294. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  295. })
  296. })
  297. describe('two batches, one partial', function () {
  298. beforeEach(async function () {
  299. this.updates = makeUpdates(15)
  300. this.rawUpdates = makeRawUpdates(this.updates)
  301. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  302. await this.RedisManager.promises.getUpdatesInBatches(
  303. this.projectId,
  304. 10,
  305. this.runner
  306. )
  307. })
  308. it('calls the runner twice', function () {
  309. this.runner.callCount.should.equal(2)
  310. })
  311. it('calls the runner with the updates', function () {
  312. this.runner
  313. .getCall(0)
  314. .should.have.been.calledWith(this.updates.slice(0, 10))
  315. this.runner
  316. .getCall(1)
  317. .should.have.been.calledWith(this.updates.slice(10))
  318. })
  319. it('deletes the applied updates', function () {
  320. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  321. })
  322. })
  323. describe('two full batches', function () {
  324. beforeEach(async function () {
  325. this.updates = makeUpdates(20)
  326. this.rawUpdates = makeRawUpdates(this.updates)
  327. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  328. await this.RedisManager.promises.getUpdatesInBatches(
  329. this.projectId,
  330. 10,
  331. this.runner
  332. )
  333. })
  334. it('calls the runner twice', function () {
  335. this.runner.callCount.should.equal(2)
  336. })
  337. it('calls the runner with the updates', function () {
  338. this.runner
  339. .getCall(0)
  340. .should.have.been.calledWith(this.updates.slice(0, 10))
  341. this.runner
  342. .getCall(1)
  343. .should.have.been.calledWith(this.updates.slice(10))
  344. })
  345. it('deletes the applied updates', function () {
  346. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  347. })
  348. })
  349. describe('three full bathches, bigger than the Redis read batch size', function () {
  350. beforeEach(async function () {
  351. this.batchSize = this.RedisManager.RAW_UPDATES_BATCH_SIZE * 2
  352. this.updates = makeUpdates(this.batchSize * 3)
  353. this.rawUpdates = makeRawUpdates(this.updates)
  354. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  355. await this.RedisManager.promises.getUpdatesInBatches(
  356. this.projectId,
  357. this.batchSize,
  358. this.runner
  359. )
  360. })
  361. it('calls the runner twice', function () {
  362. this.runner.callCount.should.equal(3)
  363. })
  364. it('calls the runner with the updates', function () {
  365. this.runner
  366. .getCall(0)
  367. .should.have.been.calledWith(this.updates.slice(0, this.batchSize))
  368. this.runner
  369. .getCall(1)
  370. .should.have.been.calledWith(
  371. this.updates.slice(this.batchSize, this.batchSize * 2)
  372. )
  373. this.runner
  374. .getCall(2)
  375. .should.have.been.calledWith(this.updates.slice(this.batchSize * 2))
  376. })
  377. it('deletes the applied updates', function () {
  378. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal([])
  379. })
  380. })
  381. describe('error when first reading updates', function () {
  382. beforeEach(async function () {
  383. this.updates = makeUpdates(10)
  384. this.rawUpdates = makeRawUpdates(this.updates)
  385. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  386. this.rclient.throwErrorOnLrangeCall(0)
  387. await expect(
  388. this.RedisManager.promises.getUpdatesInBatches(
  389. this.projectId,
  390. 2,
  391. this.runner
  392. )
  393. ).to.be.rejected
  394. })
  395. it('does not delete any updates', function () {
  396. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal(
  397. this.rawUpdates
  398. )
  399. })
  400. })
  401. describe('error when reading updates for a second batch', function () {
  402. beforeEach(async function () {
  403. this.batchSize = this.RedisManager.RAW_UPDATES_BATCH_SIZE - 1
  404. this.updates = makeUpdates(this.RedisManager.RAW_UPDATES_BATCH_SIZE * 2)
  405. this.rawUpdates = makeRawUpdates(this.updates)
  406. this.rclient.setList(this.historyOpsKey, this.rawUpdates)
  407. this.rclient.throwErrorOnLrangeCall(1)
  408. await expect(
  409. this.RedisManager.promises.getUpdatesInBatches(
  410. this.projectId,
  411. this.batchSize,
  412. this.runner
  413. )
  414. ).to.be.rejected
  415. })
  416. it('calls the runner with the first batch of updates', function () {
  417. this.runner.should.have.been.calledOnce
  418. this.runner
  419. .getCall(0)
  420. .should.have.been.calledWith(this.updates.slice(0, this.batchSize))
  421. })
  422. it('deletes only the first batch of applied updates', function () {
  423. expect(this.rclient.getList(this.historyOpsKey)).to.deep.equal(
  424. this.rawUpdates.slice(this.batchSize)
  425. )
  426. })
  427. })
  428. })
  429. })
  430. class FakeRedis {
  431. constructor() {
  432. this.data = new Map()
  433. this.del = sinon.stub()
  434. this.lrangeCallCount = -1
  435. }
  436. setList(key, list) {
  437. this.data.set(key, list)
  438. }
  439. getList(key) {
  440. return this.data.get(key)
  441. }
  442. throwErrorOnLrangeCall(callNum) {
  443. this.lrangeCallThrowingError = callNum
  444. }
  445. async lrange(key, start, stop) {
  446. this.lrangeCallCount += 1
  447. if (
  448. this.lrangeCallThrowingError != null &&
  449. this.lrangeCallThrowingError === this.lrangeCallCount
  450. ) {
  451. throw new Error('LRANGE failed!')
  452. }
  453. const list = this.data.get(key) ?? []
  454. return list.slice(start, stop + 1)
  455. }
  456. async lrem(key, count, elementToRemove) {
  457. expect(count).to.be.greaterThan(0)
  458. const original = this.data.get(key) ?? []
  459. const filtered = original.filter(element => {
  460. if (count > 0 && element === elementToRemove) {
  461. count--
  462. return false
  463. }
  464. return true
  465. })
  466. this.data.set(key, filtered)
  467. }
  468. async exec() {
  469. // Nothing to do
  470. }
  471. multi() {
  472. return this
  473. }
  474. }
  475. function makeUpdates(updateCount, extraFields = {}) {
  476. const updates = []
  477. for (let i = 0; i < updateCount; i++) {
  478. updates.push({ v: i, ...extraFields })
  479. }
  480. return updates
  481. }
  482. function makeRawUpdates(updates) {
  483. return updates.map(JSON.stringify)
  484. }