RedisManagerTests.js 15 KB

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