CheckRedisMongoSyncStateTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. const MockWebApi = require('./helpers/MockWebApi')
  2. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  3. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  4. const { promisify } = require('node:util')
  5. const { exec } = require('node:child_process')
  6. const { expect } = require('chai')
  7. const Settings = require('@overleaf/settings')
  8. const fs = require('node:fs')
  9. const Path = require('node:path')
  10. const MockDocstoreApi = require('./helpers/MockDocstoreApi')
  11. const sinon = require('sinon')
  12. const rclient = require('@overleaf/redis-wrapper').createClient(
  13. Settings.redis.documentupdater
  14. )
  15. describe('CheckRedisMongoSyncState', function () {
  16. beforeEach(async function () {
  17. await DocUpdaterApp.ensureRunning()
  18. })
  19. beforeEach(async function () {
  20. await rclient.flushall()
  21. })
  22. let peekDocumentInDocstore
  23. beforeEach(function () {
  24. peekDocumentInDocstore = sinon.spy(MockDocstoreApi, 'peekDocument')
  25. })
  26. afterEach(function () {
  27. peekDocumentInDocstore.restore()
  28. })
  29. async function runScript(options) {
  30. let result
  31. try {
  32. result = await promisify(exec)(
  33. Object.entries(options)
  34. .map(([key, value]) => `${key}=${value}`)
  35. .concat(['node', 'scripts/check_redis_mongo_sync_state.js'])
  36. .join(' ')
  37. )
  38. } catch (error) {
  39. // includes details like exit code, stdErr and stdOut
  40. return error
  41. }
  42. result.code = 0
  43. return result
  44. }
  45. describe('without projects', function () {
  46. it('should work when in sync', async function () {
  47. const result = await runScript({})
  48. expect(result.code).to.equal(0)
  49. expect(result.stdout).to.include('Processed 0 projects')
  50. expect(result.stdout).to.include(
  51. 'Found 0 projects with 0 out of sync docs'
  52. )
  53. })
  54. })
  55. describe('with a project', function () {
  56. let projectId, docId
  57. beforeEach(async function () {
  58. projectId = DocUpdaterClient.randomId()
  59. docId = DocUpdaterClient.randomId()
  60. MockWebApi.insertDoc(projectId, docId, {
  61. lines: ['mongo', 'lines'],
  62. version: 1,
  63. })
  64. await DocUpdaterClient.preloadDoc(projectId, docId)
  65. })
  66. it('should work when in sync', async function () {
  67. const result = await runScript({})
  68. expect(result.code).to.equal(0)
  69. expect(result.stdout).to.include('Processed 1 projects')
  70. expect(result.stdout).to.include(
  71. 'Found 0 projects with 0 out of sync docs'
  72. )
  73. expect(peekDocumentInDocstore).to.not.have.been.called
  74. })
  75. describe('with out of sync lines', function () {
  76. beforeEach(function () {
  77. MockWebApi.insertDoc(projectId, docId, {
  78. lines: ['updated', 'mongo', 'lines'],
  79. version: 1,
  80. })
  81. })
  82. it('should detect the out of sync state', async function () {
  83. const result = await runScript({})
  84. expect(result.code).to.equal(1)
  85. expect(result.stdout).to.include('Processed 1 projects')
  86. expect(result.stdout).to.include(
  87. 'Found 1 projects with 1 out of sync docs'
  88. )
  89. })
  90. })
  91. describe('with out of sync ranges', function () {
  92. beforeEach(function () {
  93. MockWebApi.insertDoc(projectId, docId, {
  94. lines: ['mongo', 'lines'],
  95. version: 1,
  96. ranges: { changes: ['FAKE CHANGE'] },
  97. })
  98. })
  99. it('should detect the out of sync state', async function () {
  100. const result = await runScript({})
  101. expect(result.code).to.equal(1)
  102. expect(result.stdout).to.include('Processed 1 projects')
  103. expect(result.stdout).to.include(
  104. 'Found 1 projects with 1 out of sync docs'
  105. )
  106. })
  107. })
  108. describe('with out of sync version', function () {
  109. beforeEach(function () {
  110. MockWebApi.insertDoc(projectId, docId, {
  111. lines: ['mongo', 'lines'],
  112. version: 2,
  113. })
  114. })
  115. it('should detect the out of sync state', async function () {
  116. const result = await runScript({})
  117. expect(result.code).to.equal(1)
  118. expect(result.stdout).to.include('Processed 1 projects')
  119. expect(result.stdout).to.include(
  120. 'Found 1 projects with 1 out of sync docs'
  121. )
  122. })
  123. it('should auto-fix the out of sync state', async function () {
  124. const result = await runScript({
  125. AUTO_FIX_VERSION_MISMATCH: 'true',
  126. })
  127. expect(result.code).to.equal(0)
  128. expect(result.stdout).to.include('Processed 1 projects')
  129. expect(result.stdout).to.include(
  130. 'Found 0 projects with 0 out of sync docs'
  131. )
  132. })
  133. })
  134. describe('with a project', function () {
  135. let projectId2, docId2
  136. beforeEach(async function () {
  137. projectId2 = DocUpdaterClient.randomId()
  138. docId2 = DocUpdaterClient.randomId()
  139. MockWebApi.insertDoc(projectId2, docId2, {
  140. lines: ['mongo', 'lines'],
  141. version: 1,
  142. })
  143. await DocUpdaterClient.preloadDoc(projectId2, docId2)
  144. })
  145. it('should work when in sync', async function () {
  146. const result = await runScript({})
  147. expect(result.code).to.equal(0)
  148. expect(result.stdout).to.include('Processed 2 projects')
  149. expect(result.stdout).to.include(
  150. 'Found 0 projects with 0 out of sync docs'
  151. )
  152. })
  153. describe('with one out of sync', function () {
  154. beforeEach(function () {
  155. MockWebApi.insertDoc(projectId, docId, {
  156. lines: ['updated', 'mongo', 'lines'],
  157. version: 1,
  158. })
  159. })
  160. it('should detect one project out of sync', async function () {
  161. const result = await runScript({})
  162. expect(result.code).to.equal(1)
  163. expect(result.stdout).to.include('Processed 2 projects')
  164. expect(result.stdout).to.include(
  165. 'Found 1 projects with 1 out of sync docs'
  166. )
  167. })
  168. it('should write differences to disk', async function () {
  169. const FOLDER = '/tmp/folder'
  170. await fs.promises.rm(FOLDER, { recursive: true, force: true })
  171. const result = await runScript({
  172. WRITE_CONTENT: 'true',
  173. FOLDER,
  174. })
  175. expect(result.code).to.equal(1)
  176. expect(result.stdout).to.include('Processed 2 projects')
  177. expect(result.stdout).to.include(
  178. 'Found 1 projects with 1 out of sync docs'
  179. )
  180. const dir = Path.join(FOLDER, projectId, docId)
  181. expect(await fs.promises.readdir(FOLDER)).to.deep.equal([projectId])
  182. expect(await fs.promises.readdir(dir)).to.deep.equal([
  183. 'mongo-snapshot.txt',
  184. 'redis-snapshot.txt',
  185. ])
  186. expect(
  187. await fs.promises.readFile(
  188. Path.join(dir, 'mongo-snapshot.txt'),
  189. 'utf-8'
  190. )
  191. ).to.equal('updated\nmongo\nlines')
  192. expect(
  193. await fs.promises.readFile(
  194. Path.join(dir, 'redis-snapshot.txt'),
  195. 'utf-8'
  196. )
  197. ).to.equal('mongo\nlines')
  198. })
  199. })
  200. describe('with both out of sync', function () {
  201. beforeEach(function () {
  202. MockWebApi.insertDoc(projectId, docId, {
  203. lines: ['updated', 'mongo', 'lines'],
  204. version: 1,
  205. })
  206. MockWebApi.insertDoc(projectId2, docId2, {
  207. lines: ['updated2', 'mongo', 'lines'],
  208. version: 1,
  209. })
  210. })
  211. it('should detect both projects out of sync', async function () {
  212. const result = await runScript({})
  213. expect(result.code).to.equal(1)
  214. expect(result.stdout).to.include('Processed 2 projects')
  215. expect(result.stdout).to.include(
  216. 'Found 2 projects with 2 out of sync docs'
  217. )
  218. })
  219. })
  220. })
  221. })
  222. describe('with more projects than the LIMIT', function () {
  223. for (let i = 0; i < 20; i++) {
  224. beforeEach(async function () {
  225. const projectId = DocUpdaterClient.randomId()
  226. const docId = DocUpdaterClient.randomId()
  227. MockWebApi.insertDoc(projectId, docId, {
  228. lines: ['mongo', 'lines'],
  229. version: 1,
  230. })
  231. await DocUpdaterClient.preloadDoc(projectId, docId)
  232. })
  233. }
  234. it('should flag limit', async function () {
  235. const result = await runScript({ LIMIT: '4' })
  236. expect(result.code).to.equal(2)
  237. // A redis SCAN may return more than COUNT (aka LIMIT) entries. Match loosely.
  238. expect(result.stdout).to.match(/Processed \d+ projects/)
  239. expect(result.stderr).to.include(
  240. 'Found too many un-flushed projects (LIMIT=4). Please fix the reported projects first, then try again.'
  241. )
  242. })
  243. it('should continue with auto-flush', async function () {
  244. const result = await runScript({
  245. LIMIT: '4',
  246. FLUSH_IN_SYNC_PROJECTS: 'true',
  247. })
  248. expect(result.code).to.equal(0)
  249. expect(result.stdout).to.include('Processed 20 projects')
  250. })
  251. })
  252. describe('with partially deleted doc', function () {
  253. let projectId, docId
  254. beforeEach(async function () {
  255. projectId = DocUpdaterClient.randomId()
  256. docId = DocUpdaterClient.randomId()
  257. MockWebApi.insertDoc(projectId, docId, {
  258. lines: ['mongo', 'lines'],
  259. version: 1,
  260. })
  261. MockDocstoreApi.insertDoc(projectId, docId, {
  262. lines: ['mongo', 'lines'],
  263. version: 1,
  264. })
  265. await DocUpdaterClient.preloadDoc(projectId, docId)
  266. MockWebApi.clearDocs()
  267. })
  268. describe('with only the file-tree entry deleted', function () {
  269. it('should flag the partial deletion', async function () {
  270. const result = await runScript({})
  271. expect(result.code).to.equal(0)
  272. expect(result.stdout).to.include('Processed 1 projects')
  273. expect(result.stdout).to.include(
  274. `Found partially deleted doc ${docId} in project ${projectId}: use AUTO_FIX_PARTIALLY_DELETED_DOC_METADATA=true to fix metadata`
  275. )
  276. expect(result.stdout).to.include(
  277. 'Found 0 projects with 0 out of sync docs'
  278. )
  279. expect(MockDocstoreApi.getDoc(projectId, docId)).to.not.include({
  280. deleted: true,
  281. name: 'c.tex',
  282. })
  283. expect(peekDocumentInDocstore).to.have.been.called
  284. })
  285. it('should autofix the partial deletion', async function () {
  286. const result = await runScript({
  287. AUTO_FIX_PARTIALLY_DELETED_DOC_METADATA: 'true',
  288. })
  289. expect(result.code).to.equal(0)
  290. expect(result.stdout).to.include('Processed 1 projects')
  291. expect(result.stdout).to.include(
  292. `Found partially deleted doc ${docId} in project ${projectId}: fixing metadata`
  293. )
  294. expect(result.stdout).to.include(
  295. 'Found 0 projects with 0 out of sync docs'
  296. )
  297. expect(MockDocstoreApi.getDoc(projectId, docId)).to.include({
  298. deleted: true,
  299. name: 'c.tex',
  300. })
  301. const result2 = await runScript({})
  302. expect(result2.code).to.equal(0)
  303. expect(result2.stdout).to.include('Processed 1 projects')
  304. expect(result2.stdout).to.not.include(
  305. `Found partially deleted doc ${docId} in project ${projectId}`
  306. )
  307. expect(result2.stdout).to.include(
  308. 'Found 0 projects with 0 out of sync docs'
  309. )
  310. })
  311. })
  312. describe('with docstore metadata updated', function () {
  313. beforeEach(function (done) {
  314. MockDocstoreApi.patchDocument(
  315. projectId,
  316. docId,
  317. {
  318. deleted: true,
  319. deletedAt: new Date(),
  320. name: 'c.tex',
  321. },
  322. done
  323. )
  324. })
  325. it('should work when in sync', async function () {
  326. const result = await runScript({})
  327. expect(result.code).to.equal(0)
  328. expect(result.stdout).to.include('Processed 1 projects')
  329. expect(result.stdout).to.not.include(
  330. `Found partially deleted doc ${docId} in project ${projectId}`
  331. )
  332. expect(result.stdout).to.include(
  333. 'Found 0 projects with 0 out of sync docs'
  334. )
  335. expect(peekDocumentInDocstore).to.have.been.called
  336. })
  337. })
  338. })
  339. })