RetryTests.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import _ from 'lodash'
  2. import nock from 'nock'
  3. import { expect } from 'chai'
  4. import { fetchStringWithResponse } from '@overleaf/fetch-utils'
  5. import assert from 'node:assert'
  6. import mongodb from 'mongodb-legacy'
  7. import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
  8. import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
  9. const { ObjectId } = mongodb
  10. const MockHistoryStore = () => nock('http://127.0.0.1:3100')
  11. const MockWeb = () => nock('http://127.0.0.1:3000')
  12. const MockCallback = () => nock('http://127.0.0.1')
  13. describe('Retrying failed projects', function () {
  14. const historyId = new ObjectId().toString()
  15. beforeEach(async function () {
  16. this.timestamp = new Date()
  17. await ProjectHistoryApp.ensureRunning()
  18. this.project_id = new ObjectId().toString()
  19. this.doc_id = new ObjectId().toString()
  20. this.file_id = new ObjectId().toString()
  21. MockHistoryStore().post('/api/projects').reply(200, {
  22. projectId: historyId,
  23. })
  24. MockWeb()
  25. .get(`/project/${this.project_id}/details`)
  26. .reply(200, {
  27. name: 'Test Project',
  28. overleaf: {
  29. history: {
  30. id: historyId,
  31. },
  32. },
  33. })
  34. MockHistoryStore()
  35. .get(`/api/projects/${historyId}/latest/history`)
  36. .reply(200, {
  37. chunk: {
  38. startVersion: 0,
  39. history: {
  40. changes: [],
  41. },
  42. },
  43. })
  44. await ProjectHistoryClient.initializeProject(historyId)
  45. })
  46. afterEach(function () {
  47. nock.cleanAll()
  48. })
  49. describe('normalizeFailure', function () {
  50. it('should normalize the error', async function () {
  51. const baseLineFailures = await ProjectHistoryClient.getFailures()
  52. await ProjectHistoryClient.setFailures([
  53. {
  54. project_id: new ObjectId(),
  55. attempts: 1,
  56. error: 'Error: ESOCKETTIMEDOUT',
  57. },
  58. {
  59. project_id: new ObjectId(),
  60. attempts: 1,
  61. error: 'OError: ESOCKETTIMEDOUT',
  62. },
  63. {
  64. project_id: new ObjectId(),
  65. attempts: 1,
  66. resyncStartedAt: new Date(),
  67. },
  68. ])
  69. const body = await ProjectHistoryClient.getFailures()
  70. expect(body).to.deep.equal(
  71. _.merge(baseLineFailures, {
  72. attempts: {
  73. 'socket-timeout': 2,
  74. other: 1,
  75. },
  76. counts: {
  77. 'socket-timeout': 2,
  78. other: 1,
  79. },
  80. })
  81. )
  82. })
  83. })
  84. describe('retrying project history', function () {
  85. describe('when there is a soft failure', function () {
  86. beforeEach(async function () {
  87. this.flushCall = MockHistoryStore()
  88. .put(
  89. `/api/projects/${historyId}/blobs/0a207c060e61f3b88eaee0a8cd0696f46fb155eb`
  90. )
  91. .reply(201)
  92. .post(`/api/projects/${historyId}/legacy_changes?end_version=0`)
  93. .reply(200)
  94. const update = {
  95. pathname: '/main.tex',
  96. docLines: 'a\nb',
  97. doc: this.doc_id,
  98. meta: { user_id: this.user_id, ts: new Date() },
  99. }
  100. await ProjectHistoryClient.pushRawUpdate(this.project_id, update)
  101. await ProjectHistoryClient.setFailures([
  102. {
  103. project_id: this.project_id,
  104. attempts: 1,
  105. error: 'soft-error',
  106. },
  107. ])
  108. })
  109. it('flushes the project history queue', async function () {
  110. const { response } = await fetchStringWithResponse(
  111. 'http://127.0.0.1:3054/retry/failures?failureType=soft&limit=1&timeout=10000',
  112. {
  113. method: 'POST',
  114. }
  115. )
  116. expect(response.status).to.equal(200)
  117. assert(
  118. this.flushCall.isDone(),
  119. 'made calls to history service to store updates'
  120. )
  121. })
  122. it('retries in the background when requested', async function () {
  123. this.callback = MockCallback()
  124. .matchHeader('Authorization', '123')
  125. .get('/ping')
  126. .reply(200)
  127. const { body, response } = await fetchStringWithResponse(
  128. 'http://127.0.0.1:3054/retry/failures?failureType=soft&limit=1&timeout=10000&callbackUrl=http%3A%2F%2F127.0.0.1%2Fping',
  129. {
  130. method: 'POST',
  131. headers: {
  132. 'X-CALLBACK-Authorization': '123',
  133. },
  134. }
  135. )
  136. expect(response.status).to.equal(200)
  137. expect(body).to.equal(
  138. '{"retryStatus":"running retryFailures in background"}'
  139. )
  140. assert(
  141. !this.flushCall.isDone(),
  142. 'did not make calls to history service to store updates in the foreground'
  143. )
  144. await new Promise(resolve => setTimeout(resolve, 100))
  145. assert(
  146. this.flushCall.isDone(),
  147. 'made calls to history service to store updates in the background'
  148. )
  149. assert(this.callback.isDone(), 'hit the callback url')
  150. })
  151. })
  152. describe('when there is a hard failure', function () {
  153. beforeEach(async function () {
  154. MockWeb()
  155. .get(`/project/${this.project_id}/details`)
  156. .reply(200, {
  157. name: 'Test Project',
  158. overleaf: {
  159. history: {
  160. id: historyId,
  161. },
  162. },
  163. })
  164. await ProjectHistoryClient.setFailures([
  165. {
  166. project_id: this.project_id,
  167. attempts: 100,
  168. error: 'hard-error',
  169. },
  170. ])
  171. })
  172. it('calls web to resync the project', async function () {
  173. const resyncCall = MockWeb()
  174. .post(`/project/${this.project_id}/history/resync`)
  175. .reply(200)
  176. const { response } = await fetchStringWithResponse(
  177. 'http://127.0.0.1:3054/retry/failures?failureType=hard&limit=1&timeout=10000',
  178. {
  179. method: 'POST',
  180. }
  181. )
  182. expect(response.status).to.equal(200)
  183. assert(resyncCall.isDone(), 'made a call to web to resync project')
  184. })
  185. })
  186. })
  187. })