ErrorRecorder.js 9.1 KB

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