ErrorRecorder.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { callbackify } from '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. async function getFailureRecord(projectId) {
  83. return await db.projectHistoryFailures.findOne({ project_id: projectId })
  84. }
  85. async function getLastFailure(projectId) {
  86. const result = await db.projectHistoryFailures.findOneAndUpdate(
  87. { project_id: projectId },
  88. { $inc: { requestCount: 1 } }, // increment the request count every time we check the last failure
  89. { projection: { error: 1, ts: 1 } }
  90. )
  91. return result && result.value
  92. }
  93. async function getFailedProjects() {
  94. return await db.projectHistoryFailures.find({}).toArray()
  95. }
  96. async function getFailuresByType() {
  97. const results = await db.projectHistoryFailures.find({}).toArray()
  98. const failureCounts = {}
  99. const failureAttempts = {}
  100. const failureRequests = {}
  101. const maxQueueSize = {}
  102. // count all the failures and number of attempts by type
  103. for (const result of results || []) {
  104. const failureType = result.error
  105. const attempts = result.attempts || 1 // allow for field to be absent
  106. const requests = result.requestCount || 0
  107. const queueSize = result.queueSize || 0
  108. if (failureCounts[failureType] > 0) {
  109. failureCounts[failureType]++
  110. failureAttempts[failureType] += attempts
  111. failureRequests[failureType] += requests
  112. maxQueueSize[failureType] = Math.max(queueSize, maxQueueSize[failureType])
  113. } else {
  114. failureCounts[failureType] = 1
  115. failureAttempts[failureType] = attempts
  116. failureRequests[failureType] = requests
  117. maxQueueSize[failureType] = queueSize
  118. }
  119. }
  120. return { failureCounts, failureAttempts, failureRequests, maxQueueSize }
  121. }
  122. async function getFailures() {
  123. const { failureCounts, failureAttempts, failureRequests, maxQueueSize } =
  124. await getFailuresByType()
  125. let attempts, failureType, label, requests
  126. const shortNames = {
  127. 'Error: bad response from filestore: 404': 'filestore-404',
  128. 'Error: bad response from filestore: 500': 'filestore-500',
  129. 'NotFoundError: got a 404 from web api': 'web-api-404',
  130. 'OError: history store a non-success status code: 413': 'history-store-413',
  131. 'OError: history store a non-success status code: 422': 'history-store-422',
  132. 'OError: history store a non-success status code: 500': 'history-store-500',
  133. 'OError: history store a non-success status code: 503': 'history-store-503',
  134. 'Error: history store a non-success status code: 413': 'history-store-413',
  135. 'Error: history store a non-success status code: 422': 'history-store-422',
  136. 'Error: history store a non-success status code: 500': 'history-store-500',
  137. 'Error: history store a non-success status code: 503': 'history-store-503',
  138. 'Error: web returned a non-success status code: 500 (attempts: 2)':
  139. 'web-500',
  140. 'Error: ESOCKETTIMEDOUT': 'socket-timeout',
  141. 'Error: no project found': 'no-project-found',
  142. 'OpsOutOfOrderError: project structure version out of order on incoming updates':
  143. 'incoming-project-version-out-of-order',
  144. 'OpsOutOfOrderError: doc version out of order on incoming updates':
  145. 'incoming-doc-version-out-of-order',
  146. 'OpsOutOfOrderError: project structure version out of order':
  147. 'chunk-project-version-out-of-order',
  148. 'OpsOutOfOrderError: doc version out of order':
  149. 'chunk-doc-version-out-of-order',
  150. 'Error: failed to extend lock': 'lock-overrun',
  151. 'Error: tried to release timed out lock': 'lock-overrun',
  152. 'Error: Timeout': 'lock-overrun',
  153. 'Error: sync ongoing': 'sync-ongoing',
  154. 'SyncError: unexpected resyncProjectStructure update': 'sync-error',
  155. '[object Error]': 'unknown-error-object',
  156. 'UpdateWithUnknownFormatError: update with unknown format':
  157. 'unknown-format',
  158. 'Error: update with unknown format': 'unknown-format',
  159. 'TextOperationError: The base length of the second operation has to be the target length of the first operation':
  160. 'text-op-error',
  161. 'Error: ENOSPC: no space left on device, write': 'ENOSPC',
  162. '*': 'other',
  163. }
  164. // set all the known errors to zero if not present (otherwise gauges stay on their last value)
  165. const summaryCounts = {}
  166. const summaryAttempts = {}
  167. const summaryRequests = {}
  168. const summaryMaxQueueSize = {}
  169. for (failureType in shortNames) {
  170. label = shortNames[failureType]
  171. summaryCounts[label] = 0
  172. summaryAttempts[label] = 0
  173. summaryRequests[label] = 0
  174. summaryMaxQueueSize[label] = 0
  175. }
  176. // record a metric for each type of failure
  177. for (failureType in failureCounts) {
  178. const failureCount = failureCounts[failureType]
  179. label = shortNames[failureType] || shortNames['*']
  180. summaryCounts[label] += failureCount
  181. summaryAttempts[label] += failureAttempts[failureType]
  182. summaryRequests[label] += failureRequests[failureType]
  183. summaryMaxQueueSize[label] = Math.max(
  184. maxQueueSize[failureType],
  185. summaryMaxQueueSize[label]
  186. )
  187. }
  188. for (label in summaryCounts) {
  189. const count = summaryCounts[label]
  190. metrics.globalGauge('failed', count, 1, { status: label })
  191. }
  192. for (label in summaryAttempts) {
  193. attempts = summaryAttempts[label]
  194. metrics.globalGauge('attempts', attempts, 1, { status: label })
  195. }
  196. for (label in summaryRequests) {
  197. requests = summaryRequests[label]
  198. metrics.globalGauge('requests', requests, 1, { status: label })
  199. }
  200. for (label in summaryMaxQueueSize) {
  201. const queueSize = summaryMaxQueueSize[label]
  202. metrics.globalGauge('max-queue-size', queueSize, 1, { status: label })
  203. }
  204. return {
  205. counts: summaryCounts,
  206. attempts: summaryAttempts,
  207. requests: summaryRequests,
  208. maxQueueSize: summaryMaxQueueSize,
  209. }
  210. }
  211. // EXPORTS
  212. const getFailedProjectsCb = callbackify(getFailedProjects)
  213. const getFailureRecordCb = callbackify(getFailureRecord)
  214. const getFailuresCb = callbackify(getFailures)
  215. const getLastFailureCb = callbackify(getLastFailure)
  216. const recordCb = callbackify(record)
  217. const recordSyncStartCb = callbackify(recordSyncStart)
  218. const setForceDebugCb = callbackify(setForceDebug)
  219. export {
  220. getFailedProjectsCb as getFailedProjects,
  221. getFailureRecordCb as getFailureRecord,
  222. getLastFailureCb as getLastFailure,
  223. getFailuresCb as getFailures,
  224. recordCb as record,
  225. recordSyncStartCb as recordSyncStart,
  226. setForceDebugCb as setForceDebug,
  227. }
  228. export const promises = {
  229. getFailedProjects,
  230. getFailureRecord,
  231. getLastFailure,
  232. getFailures,
  233. record,
  234. recordSyncStart,
  235. setForceDebug,
  236. }