ProjectPersistenceManager.test.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { vi, describe, beforeEach, it } from 'vitest'
  2. import sinon from 'sinon'
  3. import path from 'node:path'
  4. const modulePath = path.join(
  5. import.meta.dirname,
  6. '../../../app/js/ProjectPersistenceManager'
  7. )
  8. describe('ProjectPersistenceManager', () => {
  9. beforeEach(async ctx => {
  10. ctx.fsPromises = {
  11. statfs: sinon.stub(),
  12. }
  13. vi.doMock('@overleaf/metrics', () => ({
  14. default: (ctx.Metrics = { gauge: sinon.stub() }),
  15. }))
  16. vi.doMock('../../../app/js/UrlCache', () => ({
  17. default: (ctx.UrlCache = {}),
  18. }))
  19. vi.doMock(
  20. '../../../app/js/HistoryResourceWriter',
  21. () =>
  22. (ctx.HistoryResourceWriter = {
  23. clearCacheCb: sinon.stub().yields(null),
  24. })
  25. )
  26. vi.doMock('../../../app/js/CompileManager', () => ({
  27. default: (ctx.CompileManager = {}),
  28. }))
  29. vi.doMock('fs', () => ({
  30. default: { promises: ctx.fsPromises },
  31. }))
  32. vi.doMock('@overleaf/settings', () => ({
  33. default: (ctx.settings = {
  34. project_cache_length_ms: 1000,
  35. path: {
  36. compilesDir: '/compiles',
  37. outputDir: '/output',
  38. clsiCacheDir: '/cache',
  39. },
  40. }),
  41. }))
  42. ctx.ProjectPersistenceManager = (await import(modulePath)).default
  43. ctx.callback = sinon.stub()
  44. ctx.project_id = 'project-id-123'
  45. return (ctx.user_id = '1234')
  46. })
  47. describe('refreshExpiryTimeout', () => {
  48. it('should leave expiry alone if plenty of disk', async ctx => {
  49. await new Promise((resolve, reject) => {
  50. ctx.fsPromises.statfs.resolves({
  51. blocks: 100,
  52. bsize: 1,
  53. bavail: 40,
  54. })
  55. ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
  56. ctx.Metrics.gauge.should.have.been.calledWith(
  57. 'disk_available_percent',
  58. 40
  59. )
  60. ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(
  61. ctx.settings.project_cache_length_ms
  62. )
  63. resolve()
  64. })
  65. })
  66. })
  67. it('should drop EXPIRY_TIMEOUT 10% if low disk usage', async ctx => {
  68. await new Promise((resolve, reject) => {
  69. ctx.fsPromises.statfs.resolves({
  70. blocks: 100,
  71. bsize: 1,
  72. bavail: 5,
  73. })
  74. ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
  75. ctx.Metrics.gauge.should.have.been.calledWith(
  76. 'disk_available_percent',
  77. 5
  78. )
  79. ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(900)
  80. resolve()
  81. })
  82. })
  83. })
  84. it('should not drop EXPIRY_TIMEOUT to below 50% of project_cache_length_ms', async ctx => {
  85. await new Promise((resolve, reject) => {
  86. ctx.fsPromises.statfs.resolves({
  87. blocks: 100,
  88. bsize: 1,
  89. bavail: 5,
  90. })
  91. ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT = 500
  92. ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
  93. ctx.Metrics.gauge.should.have.been.calledWith(
  94. 'disk_available_percent',
  95. 5
  96. )
  97. ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(500)
  98. resolve()
  99. })
  100. })
  101. })
  102. it('should not modify EXPIRY_TIMEOUT if there is an error getting disk values', async ctx => {
  103. await new Promise((resolve, reject) => {
  104. ctx.fsPromises.statfs.rejects(new Error())
  105. ctx.ProjectPersistenceManager.refreshExpiryTimeout(() => {
  106. ctx.ProjectPersistenceManager.EXPIRY_TIMEOUT.should.equal(1000)
  107. resolve()
  108. })
  109. })
  110. })
  111. })
  112. describe('clearExpiredProjects', () => {
  113. beforeEach(ctx => {
  114. ctx.project_ids = ['project-id-1', 'project-id-2']
  115. ctx.ProjectPersistenceManager._findExpiredProjectIds = sinon
  116. .stub()
  117. .callsArgWith(0, null, ctx.project_ids)
  118. ctx.ProjectPersistenceManager.clearProjectFromCache = sinon
  119. .stub()
  120. .callsArg(2)
  121. ctx.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1)
  122. return ctx.ProjectPersistenceManager.clearExpiredProjects(ctx.callback)
  123. })
  124. it('should clear each expired project', ctx => {
  125. return Array.from(ctx.project_ids).map(projectId =>
  126. ctx.ProjectPersistenceManager.clearProjectFromCache
  127. .calledWith(projectId)
  128. .should.equal(true)
  129. )
  130. })
  131. return it('should call the callback', ctx => {
  132. return ctx.callback.called.should.equal(true)
  133. })
  134. })
  135. return describe('clearProject', () => {
  136. beforeEach(ctx => {
  137. ctx.ProjectPersistenceManager._clearProjectFromDatabase = sinon
  138. .stub()
  139. .callsArg(1)
  140. ctx.UrlCache.clearProject = sinon.stub().callsArg(2)
  141. ctx.CompileManager.clearProject = sinon.stub().callsArg(2)
  142. return ctx.ProjectPersistenceManager.clearProject(
  143. ctx.project_id,
  144. ctx.user_id,
  145. ctx.callback
  146. )
  147. })
  148. it('should clear the project from the database', ctx => {
  149. return ctx.ProjectPersistenceManager._clearProjectFromDatabase
  150. .calledWith(ctx.project_id)
  151. .should.equal(true)
  152. })
  153. it('should clear the history cache', ctx => {
  154. ctx.HistoryResourceWriter.clearCacheCb.should.have.been.calledWith(
  155. ctx.project_id,
  156. ctx.user_id
  157. )
  158. })
  159. it('should clear all the cached Urls for the project', ctx => {
  160. return ctx.UrlCache.clearProject
  161. .calledWith(ctx.project_id)
  162. .should.equal(true)
  163. })
  164. it('should clear the project compile folder', ctx => {
  165. return ctx.CompileManager.clearProject
  166. .calledWith(ctx.project_id, ctx.user_id)
  167. .should.equal(true)
  168. })
  169. return it('should call the callback', ctx => {
  170. return ctx.callback.called.should.equal(true)
  171. })
  172. })
  173. })