chunk_buffer.test.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. 'use strict'
  2. const { expect } = require('chai')
  3. const sinon = require('sinon')
  4. const {
  5. Chunk,
  6. Snapshot,
  7. History,
  8. File,
  9. AddFileOperation,
  10. Change,
  11. } = require('overleaf-editor-core')
  12. const cleanup = require('./support/cleanup')
  13. const fixtures = require('./support/fixtures')
  14. const chunkBuffer = require('../../../../storage/lib/chunk_buffer')
  15. const chunkStore = require('../../../../storage/lib/chunk_store')
  16. const redisBackend = require('../../../../storage/lib/chunk_store/redis')
  17. const metrics = require('@overleaf/metrics')
  18. describe('chunk buffer', function () {
  19. beforeEach(cleanup.everything)
  20. beforeEach(fixtures.create)
  21. beforeEach(function () {
  22. sinon.spy(metrics, 'inc')
  23. })
  24. afterEach(function () {
  25. metrics.inc.restore()
  26. })
  27. const projectId = '123456'
  28. describe('loadLatest', function () {
  29. // Initialize project and create a test chunk
  30. beforeEach(async function () {
  31. // Initialize project in chunk store
  32. await chunkStore.initializeProject(projectId)
  33. })
  34. describe('with an existing chunk', function () {
  35. beforeEach(async function () {
  36. // Create a sample chunk with some content
  37. const snapshot = new Snapshot()
  38. const changes = [
  39. new Change(
  40. [new AddFileOperation('test.tex', File.fromString('Hello World'))],
  41. new Date(),
  42. []
  43. ),
  44. ]
  45. const history = new History(snapshot, changes)
  46. const chunk = new Chunk(history, 1) // startVersion 1
  47. // Store the chunk directly in the chunk store using create method
  48. // which internally calls uploadChunk
  49. await chunkStore.create(projectId, chunk)
  50. // Clear any existing cache
  51. await redisBackend.clearCache(projectId)
  52. })
  53. it('should load from chunk store and update cache on first access (cache miss)', async function () {
  54. // First access should load from chunk store and populate cache
  55. const firstResult = await chunkBuffer.loadLatest(projectId)
  56. // Verify the chunk is correct
  57. expect(firstResult).to.not.be.null
  58. expect(firstResult.getStartVersion()).to.equal(1)
  59. expect(firstResult.getEndVersion()).to.equal(2)
  60. // Verify that we got a cache miss metric
  61. expect(
  62. metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
  63. status: 'cache-miss',
  64. })
  65. ).to.be.true
  66. // Reset the metrics spy
  67. metrics.inc.resetHistory()
  68. // Second access should hit the cache
  69. const secondResult = await chunkBuffer.loadLatest(projectId)
  70. // Verify we got the same chunk
  71. expect(secondResult).to.not.be.null
  72. expect(secondResult.getStartVersion()).to.equal(1)
  73. expect(secondResult.getEndVersion()).to.equal(2)
  74. // Verify that we got a cache hit metric
  75. expect(
  76. metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
  77. status: 'cache-hit',
  78. })
  79. ).to.be.true
  80. // Verify both chunks are equivalent
  81. expect(secondResult.getStartVersion()).to.equal(
  82. firstResult.getStartVersion()
  83. )
  84. expect(secondResult.getEndVersion()).to.equal(
  85. firstResult.getEndVersion()
  86. )
  87. })
  88. it('should refresh the cache when chunk changes in the store', async function () {
  89. // First access to load into cache
  90. const firstResult = await chunkBuffer.loadLatest(projectId)
  91. expect(firstResult.getStartVersion()).to.equal(1)
  92. // Reset metrics spy
  93. metrics.inc.resetHistory()
  94. // Create a new chunk with different content
  95. const newSnapshot = new Snapshot()
  96. const newChanges = [
  97. new Change(
  98. [
  99. new AddFileOperation(
  100. 'updated.tex',
  101. File.fromString('Updated content')
  102. ),
  103. ],
  104. new Date(),
  105. []
  106. ),
  107. ]
  108. const newHistory = new History(newSnapshot, newChanges)
  109. const newChunk = new Chunk(newHistory, 2) // Different start version
  110. // Store the new chunk directly in the chunk store
  111. await chunkStore.create(projectId, newChunk)
  112. // Access again - should detect the change and refresh cache
  113. const secondResult = await chunkBuffer.loadLatest(projectId)
  114. // Verify we got the updated chunk
  115. expect(secondResult.getStartVersion()).to.equal(2)
  116. expect(secondResult.getEndVersion()).to.equal(3)
  117. // Verify that we got a cache miss metric (since the cached chunk was invalidated)
  118. expect(
  119. metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
  120. status: 'cache-miss',
  121. })
  122. ).to.be.true
  123. })
  124. it('should continue using cache when chunk in store has not changed', async function () {
  125. // First access to load into cache
  126. await chunkBuffer.loadLatest(projectId)
  127. // Reset metrics spy
  128. metrics.inc.resetHistory()
  129. // Access again without changing the underlying chunk
  130. const result = await chunkBuffer.loadLatest(projectId)
  131. // Verify we got the same chunk
  132. expect(result.getStartVersion()).to.equal(1)
  133. expect(result.getEndVersion()).to.equal(2)
  134. // Verify that we got a cache hit metric
  135. expect(
  136. metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
  137. status: 'cache-hit',
  138. })
  139. ).to.be.true
  140. })
  141. })
  142. describe('with an empty project', function () {
  143. it('should handle a case with empty chunks (no changes)', async function () {
  144. // Clear the cache
  145. await redisBackend.clearCache(projectId)
  146. // Load the initial empty chunk via buffer
  147. const result = await chunkBuffer.loadLatest(projectId)
  148. // Verify we got the empty chunk
  149. expect(result.getStartVersion()).to.equal(0)
  150. expect(result.getEndVersion()).to.equal(0) // Start equals end for empty chunks
  151. expect(result.history.changes.length).to.equal(0)
  152. // Verify cache miss metric
  153. expect(
  154. metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
  155. status: 'cache-miss',
  156. })
  157. ).to.be.true
  158. // Reset metrics
  159. metrics.inc.resetHistory()
  160. // Second access should hit the cache
  161. const secondResult = await chunkBuffer.loadLatest(projectId)
  162. // Verify we got the same empty chunk
  163. expect(secondResult.getStartVersion()).to.equal(0)
  164. expect(secondResult.getEndVersion()).to.equal(0)
  165. expect(secondResult.history.changes.length).to.equal(0)
  166. // Verify cache hit metric
  167. expect(
  168. metrics.inc.calledWith('chunk_buffer.loadLatest', 1, {
  169. status: 'cache-hit',
  170. })
  171. ).to.be.true
  172. })
  173. })
  174. })
  175. })