ErrorRecorder.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { callbackify } from 'node:util'
  2. import logger from '@overleaf/logger'
  3. import metrics from '@overleaf/metrics'
  4. import { db } from './mongodb.js'
  5. async function record(projectId, queueSize, error) {
  6. if (error != null) {
  7. const errorRecord = {
  8. queueSize,
  9. error: error.toString(),
  10. stack: error.stack,
  11. ts: new Date(),
  12. }
  13. logger.debug(
  14. { projectId, errorRecord },
  15. 'recording failed attempt to process updates'
  16. )
  17. try {
  18. await db.projectHistoryFailures.updateOne(
  19. { project_id: projectId },
  20. {
  21. $set: errorRecord,
  22. $inc: { attempts: 1 },
  23. $push: {
  24. history: {
  25. $each: [errorRecord],
  26. $position: 0,
  27. $slice: 10,
  28. },
  29. }, // only keep recent failures
  30. },
  31. { upsert: true }
  32. )
  33. } catch (mongoError) {
  34. logger.error(
  35. { projectId, mongoError },
  36. 'failed to change project statues in mongo'
  37. )
  38. }
  39. throw error
  40. } else {
  41. try {
  42. await db.projectHistoryFailures.deleteOne({ project_id: projectId })
  43. } catch (mongoError) {
  44. logger.error(
  45. { projectId, mongoError },
  46. 'failed to change project statues in mongo'
  47. )
  48. }
  49. return queueSize
  50. }
  51. }
  52. async function setForceDebug(projectId, state) {
  53. if (state == null) {
  54. state = true
  55. }
  56. logger.debug({ projectId, state }, 'setting forceDebug state for project')
  57. await db.projectHistoryFailures.updateOne(
  58. { project_id: projectId },
  59. { $set: { forceDebug: state } },
  60. { upsert: true }
  61. )
  62. }
  63. // we only record the sync start time, and not the end time, because the
  64. // record should be cleared on success.
  65. async function recordSyncStart(projectId) {
  66. await db.projectHistoryFailures.updateOne(
  67. { project_id: projectId },
  68. {
  69. $currentDate: { resyncStartedAt: true },
  70. $inc: { resyncAttempts: 1 },
  71. $push: {
  72. history: {
  73. $each: [{ resyncStartedAt: new Date() }],
  74. $position: 0,
  75. $slice: 10,
  76. },
  77. },
  78. },
  79. { upsert: true }
  80. )
  81. }
  82. /**
  83. * @param projectId
  84. * @return {Promise<{error: string, forceDebug?: boolean}|null>}
  85. */
  86. async function getFailureRecord(projectId) {
  87. return await db.projectHistoryFailures.findOne({ project_id: projectId })
  88. }
  89. async function getLastFailure(projectId) {
  90. const result = await db.projectHistoryFailures.findOneAndUpdate(
  91. { project_id: projectId },
  92. { $inc: { requestCount: 1 } }, // increment the request count every time we check the last failure
  93. { projection: { error: 1, ts: 1 } }
  94. )
  95. return result && result.value
  96. }
  97. async function getFailedProjects() {
  98. return await db.projectHistoryFailures.find({}).toArray()
  99. }
  100. async function getFailuresByType() {
  101. const results = await db.projectHistoryFailures.find({}).toArray()
  102. const failureCounts = {}
  103. const failureAttempts = {}
  104. const failureRequests = {}
  105. const maxQueueSize = {}
  106. // count all the failures and number of attempts by type
  107. for (const result of results || []) {
  108. const failureType = result.error
  109. const attempts = result.attempts || 1 // allow for field to be absent
  110. const requests = result.requestCount || 0
  111. const queueSize = result.queueSize || 0
  112. if (failureCounts[failureType] > 0) {
  113. failureCounts[failureType]++
  114. failureAttempts[failureType] += attempts
  115. failureRequests[failureType] += requests
  116. maxQueueSize[failureType] = Math.max(queueSize, maxQueueSize[failureType])
  117. } else {
  118. failureCounts[failureType] = 1
  119. failureAttempts[failureType] = attempts
  120. failureRequests[failureType] = requests
  121. maxQueueSize[failureType] = queueSize
  122. }
  123. }
  124. return { failureCounts, failureAttempts, failureRequests, maxQueueSize }
  125. }
  126. async function getFailures() {
  127. const { failureCounts, failureAttempts, failureRequests, maxQueueSize } =
  128. await getFailuresByType()
  129. let attempts, failureType, label, requests
  130. const shortNames = {
  131. 'Error: bad response from filestore: 404': 'filestore-404',
  132. 'Error: bad response from filestore: 500': 'filestore-500',
  133. 'NotFoundError: got a 404 from web api': 'web-api-404',
  134. 'OError: history store a non-success status code: 413': 'history-store-413',
  135. 'OError: history store a non-success status code: 422': 'history-store-422',
  136. 'OError: history store a non-success status code: 500': 'history-store-500',
  137. 'OError: history store a non-success status code: 503': 'history-store-503',
  138. 'Error: history store a non-success status code: 413': 'history-store-413',
  139. 'Error: history store a non-success status code: 422': 'history-store-422',
  140. 'Error: history store a non-success status code: 500': 'history-store-500',
  141. 'Error: history store a non-success status code: 503': 'history-store-503',
  142. 'Error: web returned a non-success status code: 500 (attempts: 2)':
  143. 'web-500',
  144. 'Error: ESOCKETTIMEDOUT': 'socket-timeout',
  145. 'Error: no project found': 'no-project-found',
  146. 'OpsOutOfOrderError: project structure version out of order on incoming updates':
  147. 'incoming-project-version-out-of-order',
  148. 'OpsOutOfOrderError: doc version out of order on incoming updates':
  149. 'incoming-doc-version-out-of-order',
  150. 'OpsOutOfOrderError: project structure version out of order':
  151. 'chunk-project-version-out-of-order',
  152. 'OpsOutOfOrderError: doc version out of order':
  153. 'chunk-doc-version-out-of-order',
  154. 'Error: failed to extend lock': 'lock-overrun',
  155. 'Error: tried to release timed out lock': 'lock-overrun',
  156. 'Error: Timeout': 'lock-overrun',
  157. 'Error: sync ongoing': 'sync-ongoing',
  158. 'SyncError: unexpected resyncProjectStructure update': 'sync-error',
  159. '[object Error]': 'unknown-error-object',
  160. 'UpdateWithUnknownFormatError: update with unknown format':
  161. 'unknown-format',
  162. 'Error: update with unknown format': 'unknown-format',
  163. 'TextOperationError: The base length of the second operation has to be the target length of the first operation':
  164. 'text-op-error',
  165. 'Error: ENOSPC: no space left on device, write': 'ENOSPC',
  166. '*': 'other',
  167. }
  168. // set all the known errors to zero if not present (otherwise gauges stay on their last value)
  169. const summaryCounts = {}
  170. const summaryAttempts = {}
  171. const summaryRequests = {}
  172. const summaryMaxQueueSize = {}
  173. for (failureType in shortNames) {
  174. label = shortNames[failureType]
  175. summaryCounts[label] = 0
  176. summaryAttempts[label] = 0
  177. summaryRequests[label] = 0
  178. summaryMaxQueueSize[label] = 0
  179. }
  180. // record a metric for each type of failure
  181. for (failureType in failureCounts) {
  182. const failureCount = failureCounts[failureType]
  183. label = shortNames[failureType] || shortNames['*']
  184. summaryCounts[label] += failureCount
  185. summaryAttempts[label] += failureAttempts[failureType]
  186. summaryRequests[label] += failureRequests[failureType]
  187. summaryMaxQueueSize[label] = Math.max(
  188. maxQueueSize[failureType],
  189. summaryMaxQueueSize[label]
  190. )
  191. }
  192. for (label in summaryCounts) {
  193. const count = summaryCounts[label]
  194. metrics.globalGauge('failed', count, 1, { status: label })
  195. }
  196. for (label in summaryAttempts) {
  197. attempts = summaryAttempts[label]
  198. metrics.globalGauge('attempts', attempts, 1, { status: label })
  199. }
  200. for (label in summaryRequests) {
  201. requests = summaryRequests[label]
  202. metrics.globalGauge('requests', requests, 1, { status: label })
  203. }
  204. for (label in summaryMaxQueueSize) {
  205. const queueSize = summaryMaxQueueSize[label]
  206. metrics.globalGauge('max-queue-size', queueSize, 1, { status: label })
  207. }
  208. return {
  209. counts: summaryCounts,
  210. attempts: summaryAttempts,
  211. requests: summaryRequests,
  212. maxQueueSize: summaryMaxQueueSize,
  213. }
  214. }
  215. // EXPORTS
  216. const getFailedProjectsCb = callbackify(getFailedProjects)
  217. const getFailureRecordCb = callbackify(getFailureRecord)
  218. const getFailuresCb = callbackify(getFailures)
  219. const getLastFailureCb = callbackify(getLastFailure)
  220. const recordCb = callbackify(record)
  221. const recordSyncStartCb = callbackify(recordSyncStart)
  222. const setForceDebugCb = callbackify(setForceDebug)
  223. export {
  224. getFailedProjectsCb as getFailedProjects,
  225. getFailureRecordCb as getFailureRecord,
  226. getLastFailureCb as getLastFailure,
  227. getFailuresCb as getFailures,
  228. recordCb as record,
  229. recordSyncStartCb as recordSyncStart,
  230. setForceDebugCb as setForceDebug,
  231. }
  232. export const promises = {
  233. getFailedProjects,
  234. getFailureRecord,
  235. getLastFailure,
  236. getFailures,
  237. record,
  238. recordSyncStart,
  239. setForceDebug,
  240. }