ErrorRecorder.js 8.5 KB

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