force_resync.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #!/usr/bin/env node
  2. // To run in dev:
  3. //
  4. // docker compose run --rm project-history scripts/clear_deleted.js
  5. //
  6. // In production:
  7. //
  8. // docker run --rm $(docker ps -lq) scripts/clear_deleted.js
  9. import async from 'async'
  10. import Settings from '@overleaf/settings'
  11. import redis from '@overleaf/redis-wrapper'
  12. import { db, ObjectId } from '../app/js/mongodb.js'
  13. import * as SyncManager from '../app/js/SyncManager.js'
  14. import * as UpdatesProcessor from '../app/js/UpdatesProcessor.js'
  15. const rclient = redis.createClient(Settings.redis.project_history)
  16. const Keys = Settings.redis.project_history.key_schema
  17. const argv = process.argv.slice(2)
  18. const limit = parseInt(argv[0], 10) || null
  19. const force = argv[1] === 'force' || false
  20. let projectNotFoundErrors = 0
  21. let projectImportedFromV1Errors = 0
  22. const projectsNotFound = []
  23. const projectsImportedFromV1 = []
  24. let projectNoHistoryIdErrors = 0
  25. let projectsFailedErrors = 0
  26. const projectsFailed = []
  27. let projectsBrokenSyncErrors = 0
  28. const projectsBrokenSync = []
  29. function checkAndClear(project, callback) {
  30. const projectId = project.project_id
  31. console.log('checking project', projectId)
  32. // These can probably also be reset and their overleaf.history.id unset
  33. // (unless they are v1 projects).
  34. function checkNotV1Project(cb) {
  35. db.projects.findOne(
  36. { _id: new ObjectId(projectId) },
  37. { projection: { overleaf: true } },
  38. (err, result) => {
  39. console.log(
  40. '1. looking in mongo projects collection: err',
  41. err,
  42. 'result',
  43. JSON.stringify(result)
  44. )
  45. if (err) {
  46. return cb(err)
  47. }
  48. if (!result) {
  49. return cb(new Error('project not found in mongo'))
  50. }
  51. if (result && result.overleaf && !result.overleaf.id) {
  52. if (result.overleaf.history.id) {
  53. console.log(
  54. ' - project is not imported from v1 and has a history id - ok to resync'
  55. )
  56. return cb()
  57. } else {
  58. console.log(
  59. ' - project is not imported from v1 but does not have a history id'
  60. )
  61. return cb(new Error('no history id'))
  62. }
  63. } else {
  64. cb(new Error('project is imported from v1 - will not resync it'))
  65. }
  66. }
  67. )
  68. }
  69. function startResync(cb) {
  70. if (force) {
  71. console.log('2. starting resync for', projectId)
  72. SyncManager.startResync(projectId, err => {
  73. if (err) {
  74. console.log('ERR', JSON.stringify(err.message))
  75. return cb(err)
  76. }
  77. setTimeout(cb, 3000) // include a delay to allow the request to be processed
  78. })
  79. } else {
  80. console.log('2. dry run, would start resync for', projectId)
  81. cb()
  82. }
  83. }
  84. function forceFlush(cb) {
  85. if (force) {
  86. console.log('3. forcing a flush for', projectId)
  87. UpdatesProcessor.processUpdatesForProject(projectId, err => {
  88. console.log('err', err)
  89. return cb(err)
  90. })
  91. } else {
  92. console.log('3. dry run, would force a flush for', projectId)
  93. cb()
  94. }
  95. }
  96. function watchRedisQueue(cb) {
  97. const key = Keys.projectHistoryOps({ project_id: projectId })
  98. function checkQueueEmpty(_callback) {
  99. rclient.llen(key, (err, result) => {
  100. console.log('LLEN', projectId, err, result)
  101. if (err) {
  102. _callback(err)
  103. }
  104. if (result === 0) {
  105. _callback()
  106. } else {
  107. _callback(new Error('queue not empty'))
  108. }
  109. })
  110. }
  111. if (force) {
  112. console.log('4. checking redis queue key', key)
  113. async.retry({ times: 30, interval: 1000 }, checkQueueEmpty, err => {
  114. cb(err)
  115. })
  116. } else {
  117. console.log('4. dry run, would check redis key', key)
  118. cb()
  119. }
  120. }
  121. function checkMongoFailureEntry(cb) {
  122. if (force) {
  123. console.log('5. checking key in mongo projectHistoryFailures', projectId)
  124. db.projectHistoryFailures.findOne(
  125. { project_id: projectId },
  126. { projection: { _id: 1 } },
  127. (err, result) => {
  128. console.log('got result', err, result)
  129. if (err) {
  130. return cb(err)
  131. }
  132. if (result) {
  133. return cb(new Error('failure record still exists'))
  134. }
  135. return cb()
  136. }
  137. )
  138. } else {
  139. console.log('5. would check failure record for', projectId, 'in mongo')
  140. cb()
  141. }
  142. }
  143. // do the checks and deletions
  144. async.waterfall(
  145. [
  146. checkNotV1Project,
  147. startResync,
  148. forceFlush,
  149. watchRedisQueue,
  150. checkMongoFailureEntry,
  151. ],
  152. err => {
  153. if (!err) {
  154. return setTimeout(callback, 1000) // include a 1 second delay
  155. } else if (err.message === 'project not found in mongo') {
  156. projectNotFoundErrors++
  157. projectsNotFound.push(projectId)
  158. return callback()
  159. } else if (err.message === 'no history id') {
  160. projectNoHistoryIdErrors++
  161. return callback()
  162. } else if (
  163. err.message === 'project is imported from v1 - will not resync it'
  164. ) {
  165. projectImportedFromV1Errors++
  166. projectsImportedFromV1.push(projectId)
  167. return callback()
  168. } else if (
  169. err.message === 'history store a non-success status code: 422'
  170. ) {
  171. projectsFailedErrors++
  172. projectsFailed.push(projectId)
  173. return callback()
  174. } else if (err.message === 'sync ongoing') {
  175. projectsBrokenSyncErrors++
  176. projectsBrokenSync.push(projectId)
  177. return callback()
  178. } else {
  179. console.log('error:', err)
  180. return callback()
  181. }
  182. }
  183. )
  184. }
  185. // find all the broken projects from the failure records
  186. const errorsToResync = [
  187. 'Error: history store a non-success status code: 422',
  188. 'OError: history store a non-success status code: 422',
  189. 'OpsOutOfOrderError: project structure version out of order',
  190. ]
  191. async function main() {
  192. const results = await db.projectHistoryFailures
  193. .find({ error: { $in: errorsToResync } })
  194. .toArray()
  195. console.log('number of queues without history store 442 =', results.length)
  196. // now check if the project is truly deleted in mongo
  197. async.eachSeries(results.slice(0, limit), checkAndClear, err => {
  198. console.log('Final error status', err)
  199. console.log(
  200. 'Project flush failed again errors',
  201. projectsFailedErrors,
  202. projectsFailed
  203. )
  204. console.log(
  205. 'Project flush ongoing errors',
  206. projectsBrokenSyncErrors,
  207. projectsBrokenSync
  208. )
  209. console.log(
  210. 'Project not found errors',
  211. projectNotFoundErrors,
  212. projectsNotFound
  213. )
  214. console.log('Project without history_id errors', projectNoHistoryIdErrors)
  215. console.log(
  216. 'Project imported from V1 errors',
  217. projectImportedFromV1Errors,
  218. projectsImportedFromV1
  219. )
  220. process.exit()
  221. })
  222. }
  223. main().catch(error => {
  224. console.error(error)
  225. process.exit(1)
  226. })