FlushManagerTests.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import nock from 'nock'
  2. import { expect } from 'chai'
  3. import { fetchNothing, fetchJsonWithResponse } from '@overleaf/fetch-utils'
  4. import assert from 'node:assert'
  5. import mongodb from 'mongodb-legacy'
  6. import RedisWrapper from '@overleaf/redis-wrapper'
  7. import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
  8. import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
  9. import Settings from '@overleaf/settings'
  10. const { ObjectId } = mongodb
  11. const LockKey = Settings.redis.lock.key_schema
  12. const lockRClient = RedisWrapper.createClient(Settings.redis.lock)
  13. const MockHistoryStore = () => nock('http://127.0.0.1:3100')
  14. const MockWeb = () => nock('http://127.0.0.1:3000')
  15. describe('Flushing old queues', function () {
  16. const historyId = new ObjectId().toString()
  17. beforeEach(async function () {
  18. this.timestamp = new Date()
  19. await ProjectHistoryApp.ensureRunning()
  20. this.projectId = new ObjectId().toString()
  21. this.docId = new ObjectId().toString()
  22. this.fileId = new ObjectId().toString()
  23. MockHistoryStore().post('/api/projects').reply(200, {
  24. projectId: historyId,
  25. })
  26. MockWeb()
  27. .get(`/project/${this.projectId}/details`)
  28. .reply(200, {
  29. name: 'Test Project',
  30. overleaf: {
  31. history: {
  32. id: historyId,
  33. },
  34. },
  35. })
  36. MockHistoryStore()
  37. .get(`/api/projects/${historyId}/latest/history`)
  38. .reply(200, {
  39. chunk: {
  40. startVersion: 0,
  41. history: {
  42. changes: [],
  43. },
  44. },
  45. })
  46. await ProjectHistoryClient.initializeProject(historyId)
  47. })
  48. afterEach(function () {
  49. nock.cleanAll()
  50. })
  51. describe('retrying an unflushed project', function () {
  52. describe('when the update is older than the cutoff', function () {
  53. beforeEach(async function () {
  54. this.flushCall = MockHistoryStore()
  55. .put(
  56. `/api/projects/${historyId}/blobs/0a207c060e61f3b88eaee0a8cd0696f46fb155eb`
  57. )
  58. .reply(201)
  59. .post(`/api/projects/${historyId}/legacy_changes?end_version=0`)
  60. .reply(200)
  61. const update = {
  62. pathname: '/main.tex',
  63. docLines: 'a\nb',
  64. doc: this.docId,
  65. meta: { user_id: this.user_id, ts: new Date() },
  66. }
  67. await ProjectHistoryClient.pushRawUpdate(this.projectId, update)
  68. await ProjectHistoryClient.setFirstOpTimestamp(
  69. this.projectId,
  70. Date.now() - 24 * 3600 * 1000
  71. )
  72. })
  73. it('flushes the project history queue', async function () {
  74. const response = await fetchNothing(
  75. 'http://127.0.0.1:3054/flush/old?maxAge=10800',
  76. {
  77. method: 'POST',
  78. }
  79. )
  80. expect(response.status).to.equal(200)
  81. assert(
  82. this.flushCall.isDone(),
  83. 'made calls to history service to store updates'
  84. )
  85. })
  86. it('flushes the project history queue in the background when requested', async function () {
  87. const { json, response } = await fetchJsonWithResponse(
  88. 'http://127.0.0.1:3054/flush/old?maxAge=10800&background=1',
  89. {
  90. method: 'POST',
  91. }
  92. )
  93. expect(response.status).to.equal(200)
  94. expect(json).to.deep.equal({
  95. message: 'running flush in background',
  96. })
  97. assert(
  98. !this.flushCall.isDone(),
  99. 'did not make calls to history service to store updates in the foreground'
  100. )
  101. await new Promise(resolve => setTimeout(resolve, 1000))
  102. assert(
  103. this.flushCall.isDone(),
  104. 'made calls to history service to store updates in the background'
  105. )
  106. })
  107. })
  108. describe('when the update is newer than the cutoff', function () {
  109. beforeEach(async function () {
  110. this.flushCall = MockHistoryStore()
  111. .put(
  112. `/api/projects/${historyId}/blobs/0a207c060e61f3b88eaee0a8cd0696f46fb155eb`
  113. )
  114. .reply(201)
  115. .post(`/api/projects/${historyId}/legacy_changes?end_version=0`)
  116. .reply(200)
  117. const update = {
  118. pathname: '/main.tex',
  119. docLines: 'a\nb',
  120. doc: this.docId,
  121. meta: { user_id: this.user_id, ts: new Date() },
  122. }
  123. await ProjectHistoryClient.pushRawUpdate(this.projectId, update)
  124. await ProjectHistoryClient.setFirstOpTimestamp(
  125. this.projectId,
  126. Date.now() - 60 * 1000
  127. )
  128. })
  129. it('does not flush the project history queue', async function () {
  130. const response = await fetchNothing(
  131. `http://127.0.0.1:3054/flush/old?maxAge=${3 * 3600}`,
  132. {
  133. method: 'POST',
  134. }
  135. )
  136. expect(response.status).to.equal(200)
  137. assert(
  138. !this.flushCall.isDone(),
  139. 'did not make calls to history service to store updates'
  140. )
  141. })
  142. })
  143. describe('when the project lock is already held', function () {
  144. it('returns 423', async function () {
  145. const key = LockKey.projectHistoryLock({ project_id: this.projectId })
  146. await lockRClient.set(key, 'taken')
  147. try {
  148. const { statusCode } = await ProjectHistoryClient.flushProject(
  149. this.projectId,
  150. { allowErrors: true }
  151. )
  152. expect(statusCode).to.equal(423)
  153. } finally {
  154. await lockRClient.del(key)
  155. }
  156. })
  157. })
  158. describe('when the update is newer than the cutoff and project has short queue', function () {
  159. beforeEach(function () {
  160. Settings.shortHistoryQueues.push(this.projectId)
  161. })
  162. afterEach(function () {
  163. Settings.shortHistoryQueues.length = 0
  164. })
  165. beforeEach(async function () {
  166. this.flushCall = MockHistoryStore()
  167. .put(
  168. `/api/projects/${historyId}/blobs/0a207c060e61f3b88eaee0a8cd0696f46fb155eb`
  169. )
  170. .reply(201)
  171. .post(`/api/projects/${historyId}/legacy_changes?end_version=0`)
  172. .reply(200)
  173. const update = {
  174. pathname: '/main.tex',
  175. docLines: 'a\nb',
  176. doc: this.docId,
  177. meta: { user_id: this.user_id, ts: new Date() },
  178. }
  179. await ProjectHistoryClient.pushRawUpdate(this.projectId, update)
  180. await ProjectHistoryClient.setFirstOpTimestamp(
  181. this.projectId,
  182. Date.now() - 60 * 1000
  183. )
  184. })
  185. it('flushes the project history queue', async function () {
  186. const response = await fetchNothing(
  187. `http://127.0.0.1:3054/flush/old?maxAge=${3 * 3600}`,
  188. {
  189. method: 'POST',
  190. }
  191. )
  192. expect(response.status).to.equal(200)
  193. assert(
  194. this.flushCall.isDone(),
  195. 'made calls to history service to store updates'
  196. )
  197. })
  198. it('flushes the project history queue in the background when requested', async function () {
  199. const { json, response } = await fetchJsonWithResponse(
  200. `http://127.0.0.1:3054/flush/old?maxAge=${3 * 3600}&background=1`,
  201. {
  202. method: 'POST',
  203. }
  204. )
  205. expect(response.status).to.equal(200)
  206. expect(json).to.deep.equal({
  207. message: 'running flush in background',
  208. })
  209. assert(
  210. !this.flushCall.isDone(),
  211. 'did not make calls to history service to store updates in the foreground'
  212. )
  213. await new Promise(resolve => setTimeout(resolve, 1000))
  214. assert(
  215. this.flushCall.isDone(),
  216. 'made calls to history service to store updates in the background'
  217. )
  218. })
  219. })
  220. describe('when the update does not have a timestamp', function () {
  221. beforeEach(async function () {
  222. this.flushCall = MockHistoryStore()
  223. .put(
  224. `/api/projects/${historyId}/blobs/0a207c060e61f3b88eaee0a8cd0696f46fb155eb`
  225. )
  226. .reply(201)
  227. .post(`/api/projects/${historyId}/legacy_changes?end_version=0`)
  228. .reply(200)
  229. const update = {
  230. pathname: '/main.tex',
  231. docLines: 'a\nb',
  232. doc: this.docId,
  233. meta: { user_id: this.user_id, ts: new Date() },
  234. }
  235. this.startDate = Date.now()
  236. await ProjectHistoryClient.pushRawUpdate(this.projectId, update)
  237. await ProjectHistoryClient.clearFirstOpTimestamp(this.projectId)
  238. })
  239. it('flushes the project history queue anyway', async function () {
  240. const response = await fetchNothing(
  241. `http://127.0.0.1:3054/flush/old?maxAge=${3 * 3600}`,
  242. {
  243. method: 'POST',
  244. }
  245. )
  246. expect(response.status).to.equal(200)
  247. assert(
  248. this.flushCall.isDone(),
  249. 'made calls to history service to store updates'
  250. )
  251. const result = await ProjectHistoryClient.getFirstOpTimestamp(
  252. this.projectId
  253. )
  254. expect(result).to.be.null
  255. })
  256. })
  257. })
  258. })