ErrorRecorder.js 9.0 KB

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