ProjectPersistenceManager.test.js 5.3 KB

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