@contember/engine-actions 2.2.0-alpha.2 → 2.2.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/CLAUDE.md +58 -0
  2. package/dist/development/index.cjs +19 -7
  3. package/dist/development/index.cjs.map +1 -1
  4. package/dist/development/index.js +19 -7
  5. package/dist/development/index.js.map +1 -1
  6. package/dist/development/src/ActionsExecutionContainerHookFactory.cjs +5 -2
  7. package/dist/development/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
  8. package/dist/development/src/ActionsExecutionContainerHookFactory.js +5 -2
  9. package/dist/development/src/ActionsExecutionContainerHookFactory.js.map +1 -1
  10. package/dist/development/src/ActionsMetrics.cjs +98 -0
  11. package/dist/development/src/ActionsMetrics.cjs.map +1 -0
  12. package/dist/development/src/ActionsMetrics.js +98 -0
  13. package/dist/development/src/ActionsMetrics.js.map +1 -0
  14. package/dist/development/src/dispatch/EventDispatcher.cjs +27 -9
  15. package/dist/development/src/dispatch/EventDispatcher.cjs.map +1 -1
  16. package/dist/development/src/dispatch/EventDispatcher.js +27 -9
  17. package/dist/development/src/dispatch/EventDispatcher.js.map +1 -1
  18. package/dist/development/src/dispatch/EventsRepository.cjs +49 -20
  19. package/dist/development/src/dispatch/EventsRepository.cjs.map +1 -1
  20. package/dist/development/src/dispatch/EventsRepository.js +49 -20
  21. package/dist/development/src/dispatch/EventsRepository.js.map +1 -1
  22. package/dist/development/src/dispatch/ProjectDispatcher.cjs +21 -13
  23. package/dist/development/src/dispatch/ProjectDispatcher.cjs.map +1 -1
  24. package/dist/development/src/dispatch/ProjectDispatcher.js +21 -13
  25. package/dist/development/src/dispatch/ProjectDispatcher.js.map +1 -1
  26. package/dist/development/src/dispatch/WebhookTargetHandler.cjs +29 -2
  27. package/dist/development/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
  28. package/dist/development/src/dispatch/WebhookTargetHandler.js +29 -2
  29. package/dist/development/src/dispatch/WebhookTargetHandler.js.map +1 -1
  30. package/dist/development/src/graphql/resolvers/ResolversFactory.cjs +1 -1
  31. package/dist/development/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
  32. package/dist/development/src/graphql/resolvers/ResolversFactory.js +1 -1
  33. package/dist/development/src/graphql/resolvers/ResolversFactory.js.map +1 -1
  34. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
  35. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
  36. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
  37. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
  38. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
  39. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
  40. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
  41. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
  42. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
  43. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
  44. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
  45. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
  46. package/dist/development/src/graphql/schema/actions.graphql.cjs +20 -8
  47. package/dist/development/src/graphql/schema/actions.graphql.cjs.map +1 -1
  48. package/dist/development/src/graphql/schema/actions.graphql.js +20 -8
  49. package/dist/development/src/graphql/schema/actions.graphql.js.map +1 -1
  50. package/dist/development/src/model/VariablesManager.cjs +54 -5
  51. package/dist/development/src/model/VariablesManager.cjs.map +1 -1
  52. package/dist/development/src/model/VariablesManager.js +54 -5
  53. package/dist/development/src/model/VariablesManager.js.map +1 -1
  54. package/dist/development/src/triggers/TriggerPayloadPersister.cjs +3 -1
  55. package/dist/development/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
  56. package/dist/development/src/triggers/TriggerPayloadPersister.js +3 -1
  57. package/dist/development/src/triggers/TriggerPayloadPersister.js.map +1 -1
  58. package/dist/production/index.cjs +19 -7
  59. package/dist/production/index.cjs.map +1 -1
  60. package/dist/production/index.js +19 -7
  61. package/dist/production/index.js.map +1 -1
  62. package/dist/production/src/ActionsExecutionContainerHookFactory.cjs +5 -2
  63. package/dist/production/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
  64. package/dist/production/src/ActionsExecutionContainerHookFactory.js +5 -2
  65. package/dist/production/src/ActionsExecutionContainerHookFactory.js.map +1 -1
  66. package/dist/production/src/ActionsMetrics.cjs +98 -0
  67. package/dist/production/src/ActionsMetrics.cjs.map +1 -0
  68. package/dist/production/src/ActionsMetrics.js +98 -0
  69. package/dist/production/src/ActionsMetrics.js.map +1 -0
  70. package/dist/production/src/dispatch/EventDispatcher.cjs +27 -9
  71. package/dist/production/src/dispatch/EventDispatcher.cjs.map +1 -1
  72. package/dist/production/src/dispatch/EventDispatcher.js +27 -9
  73. package/dist/production/src/dispatch/EventDispatcher.js.map +1 -1
  74. package/dist/production/src/dispatch/EventsRepository.cjs +49 -20
  75. package/dist/production/src/dispatch/EventsRepository.cjs.map +1 -1
  76. package/dist/production/src/dispatch/EventsRepository.js +49 -20
  77. package/dist/production/src/dispatch/EventsRepository.js.map +1 -1
  78. package/dist/production/src/dispatch/ProjectDispatcher.cjs +21 -13
  79. package/dist/production/src/dispatch/ProjectDispatcher.cjs.map +1 -1
  80. package/dist/production/src/dispatch/ProjectDispatcher.js +21 -13
  81. package/dist/production/src/dispatch/ProjectDispatcher.js.map +1 -1
  82. package/dist/production/src/dispatch/WebhookTargetHandler.cjs +29 -2
  83. package/dist/production/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
  84. package/dist/production/src/dispatch/WebhookTargetHandler.js +29 -2
  85. package/dist/production/src/dispatch/WebhookTargetHandler.js.map +1 -1
  86. package/dist/production/src/graphql/resolvers/ResolversFactory.cjs +1 -1
  87. package/dist/production/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
  88. package/dist/production/src/graphql/resolvers/ResolversFactory.js +1 -1
  89. package/dist/production/src/graphql/resolvers/ResolversFactory.js.map +1 -1
  90. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
  91. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
  92. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
  93. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
  94. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
  95. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
  96. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
  97. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
  98. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
  99. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
  100. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
  101. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
  102. package/dist/production/src/graphql/schema/actions.graphql.cjs +20 -8
  103. package/dist/production/src/graphql/schema/actions.graphql.cjs.map +1 -1
  104. package/dist/production/src/graphql/schema/actions.graphql.js +20 -8
  105. package/dist/production/src/graphql/schema/actions.graphql.js.map +1 -1
  106. package/dist/production/src/model/VariablesManager.cjs +54 -5
  107. package/dist/production/src/model/VariablesManager.cjs.map +1 -1
  108. package/dist/production/src/model/VariablesManager.js +54 -5
  109. package/dist/production/src/model/VariablesManager.js.map +1 -1
  110. package/dist/production/src/triggers/TriggerPayloadPersister.cjs +3 -1
  111. package/dist/production/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
  112. package/dist/production/src/triggers/TriggerPayloadPersister.js +3 -1
  113. package/dist/production/src/triggers/TriggerPayloadPersister.js.map +1 -1
  114. package/dist/tests/cases/unit/actionsMetrics.test.d.ts +2 -0
  115. package/dist/tests/cases/unit/actionsMetrics.test.d.ts.map +1 -0
  116. package/dist/tests/cases/unit/eventsRepository.test.d.ts +2 -0
  117. package/dist/tests/cases/unit/eventsRepository.test.d.ts.map +1 -0
  118. package/dist/tests/cases/unit/variablesManager.test.d.ts +2 -0
  119. package/dist/tests/cases/unit/variablesManager.test.d.ts.map +1 -0
  120. package/dist/tests/src/event.d.ts +4 -0
  121. package/dist/tests/src/event.d.ts.map +1 -0
  122. package/dist/tests/src/gql.d.ts.map +1 -1
  123. package/dist/tests/src/uuid.d.ts.map +1 -1
  124. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  125. package/dist/types/ActionsExecutionContainerHookFactory.d.ts +3 -1
  126. package/dist/types/ActionsExecutionContainerHookFactory.d.ts.map +1 -1
  127. package/dist/types/ActionsMetrics.d.ts +59 -0
  128. package/dist/types/ActionsMetrics.d.ts.map +1 -0
  129. package/dist/types/ListenerStoreProvider.d.ts.map +1 -1
  130. package/dist/types/authorization/ActionsPermissionsFactory.d.ts.map +1 -1
  131. package/dist/types/dispatch/DispatchWorkerSupervisor.d.ts.map +1 -1
  132. package/dist/types/dispatch/EventDispatcher.d.ts +12 -3
  133. package/dist/types/dispatch/EventDispatcher.d.ts.map +1 -1
  134. package/dist/types/dispatch/EventsRepository.d.ts +14 -2
  135. package/dist/types/dispatch/EventsRepository.d.ts.map +1 -1
  136. package/dist/types/dispatch/LazyDispatchWorker.d.ts.map +1 -1
  137. package/dist/types/dispatch/ProjectDispatcher.d.ts +5 -2
  138. package/dist/types/dispatch/ProjectDispatcher.d.ts.map +1 -1
  139. package/dist/types/dispatch/TargetHandlerResolver.d.ts.map +1 -1
  140. package/dist/types/dispatch/WebhookFetcher.d.ts.map +1 -1
  141. package/dist/types/dispatch/WebhookTargetHandler.d.ts.map +1 -1
  142. package/dist/types/graphql/http/ActionsApiMiddlewareFactory.d.ts.map +1 -1
  143. package/dist/types/graphql/http/ActionsContextResolver.d.ts.map +1 -1
  144. package/dist/types/graphql/http/ActionsGraphQLHandlerFactory.d.ts.map +1 -1
  145. package/dist/types/graphql/http/ActionsWebsocketControllerFactory.d.ts.map +1 -1
  146. package/dist/types/graphql/resolvers/ResolversFactory.d.ts.map +1 -1
  147. package/dist/types/graphql/resolvers/mutation/ProcessBatchMutationResolver.d.ts.map +1 -1
  148. package/dist/types/graphql/resolvers/mutation/RetryEventMutationResolver.d.ts.map +1 -1
  149. package/dist/types/graphql/resolvers/mutation/SetVariablesMutationResolver.d.ts.map +1 -1
  150. package/dist/types/graphql/resolvers/mutation/StopEventMutationResolver.d.ts.map +1 -1
  151. package/dist/types/graphql/resolvers/query/EventsQueryResolver.d.ts.map +1 -1
  152. package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts +1 -4
  153. package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts.map +1 -1
  154. package/dist/types/graphql/schema/actions.graphql.d.ts.map +1 -1
  155. package/dist/types/graphql/schema/index.d.ts +27 -17
  156. package/dist/types/graphql/schema/index.d.ts.map +1 -1
  157. package/dist/types/index.d.ts +6 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/types/model/EventByIdQuery.d.ts.map +1 -1
  160. package/dist/types/model/EventsInProcessingQuery.d.ts.map +1 -1
  161. package/dist/types/model/EventsToProcessQuery.d.ts.map +1 -1
  162. package/dist/types/model/FailedEventsQuery.d.ts.map +1 -1
  163. package/dist/types/model/SetVariableCommand.d.ts.map +1 -1
  164. package/dist/types/model/VariablesManager.d.ts +16 -3
  165. package/dist/types/model/VariablesManager.d.ts.map +1 -1
  166. package/dist/types/model/VariablesQuery.d.ts.map +1 -1
  167. package/dist/types/triggers/TriggerHandler.d.ts.map +1 -1
  168. package/dist/types/triggers/TriggerIndirectChangesFetcher.d.ts.map +1 -1
  169. package/dist/types/triggers/TriggerListenersBuilder.d.ts.map +1 -1
  170. package/dist/types/triggers/TriggerListenersFactory.d.ts.map +1 -1
  171. package/dist/types/triggers/TriggerListenersStore.d.ts.map +1 -1
  172. package/dist/types/triggers/TriggerPayloadBuilder.d.ts.map +1 -1
  173. package/dist/types/triggers/TriggerPayloadManager.d.ts.map +1 -1
  174. package/dist/types/triggers/TriggerPayloadPersister.d.ts +3 -1
  175. package/dist/types/triggers/TriggerPayloadPersister.d.ts.map +1 -1
  176. package/dist/types/tsconfig.tsbuildinfo +1 -1
  177. package/dist/types/utils/map.d.ts.map +1 -1
  178. package/dist/types/utils/notifyChannel.d.ts.map +1 -1
  179. package/graphql.codegen.yml +1 -1
  180. package/package.json +18 -16
  181. package/src/ActionsExecutionContainerHookFactory.ts +6 -0
  182. package/src/ActionsMetrics.ts +135 -0
  183. package/src/dispatch/EventDispatcher.ts +36 -11
  184. package/src/dispatch/EventsRepository.ts +68 -26
  185. package/src/dispatch/ProjectDispatcher.ts +27 -11
  186. package/src/dispatch/WebhookTargetHandler.ts +36 -1
  187. package/src/graphql/resolvers/ResolversFactory.ts +1 -1
  188. package/src/graphql/resolvers/mutation/SetVariablesMutationResolver.ts +1 -1
  189. package/src/graphql/resolvers/query/EventsQueryResolver.ts +1 -1
  190. package/src/graphql/resolvers/query/VariablesQueryResolver.ts +1 -4
  191. package/src/graphql/schema/actions.graphql.ts +20 -8
  192. package/src/graphql/schema/index.ts +30 -17
  193. package/src/index.ts +27 -5
  194. package/src/model/VariablesManager.ts +66 -6
  195. package/src/triggers/TriggerPayloadPersister.ts +6 -0
  196. package/tests/cases/unit/actionsMetrics.test.ts +76 -0
  197. package/tests/cases/unit/eventsRepository.test.ts +136 -0
  198. package/tests/cases/unit/variablesManager.test.ts +154 -0
  199. package/tests/cases/unit/webhookHandler.test.ts +80 -24
  200. package/tests/src/event.ts +27 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActionsMetrics.js","sources":["../../../../packages/engine-actions/src/ActionsMetrics.ts"],"sourcesContent":["import prom, { Registry } from 'prom-client'\n\nconst LABEL_PROJECT = 'contember_project'\ntype Label = typeof LABEL_PROJECT\n\n/**\n * Prometheus metrics for the Actions dispatch subsystem.\n *\n * Two audiences, deliberately kept as cheap in-memory counters/gauges (no table polling):\n * - system / operator — is the dispatch loop alive and flowing, or stuck?\n * `workerHeartbeat` (liveness; works even when the queue is empty) + `workerCrashed`.\n * - userland — are webhook deliveries failing?\n * `eventsSucceeded` / `deliveryAttemptsFailed` / `eventsFailed`.\n *\n * Counting units matter: a *delivery attempt* and an *event* are different. A success is always\n * terminal (one event = one successful attempt), but a failure splits — a failed attempt may be\n * retried many times, while a terminally failed event gives up once. Hence the asymmetric set:\n * one success counter, but both a per-attempt failure counter and a terminal-event failure counter.\n *\n * Queue backlog is *estimated* as a trend from the counters:\n * enqueued − succeeded − failed (compare rates; the absolute value drifts across restarts and\n * ignores manual retry/stop, so for an exact depth query the `actions_event` table instead).\n *\n * Labelled only by `contember_project` to keep cardinality bounded; per-target / per-trigger\n * detail belongs in the table-backed userland view, not in Prometheus.\n */\nexport class ActionsMetrics {\n\t/** Events written to the queue (inflow). Unit: event. */\n\tpublic readonly eventsEnqueued: prom.Counter<Label>\n\t/** Events delivered successfully (terminal). Unit: event (= successful attempt). */\n\tpublic readonly eventsSucceeded: prom.Counter<Label>\n\t/** Failed webhook delivery attempts, including ones that will be retried. Unit: attempt. */\n\tpublic readonly deliveryAttemptsFailed: prom.Counter<Label>\n\t/** Events that reached a terminal failed state (retries exhausted or unknown target). Unit: event. */\n\tpublic readonly eventsFailed: prom.Counter<Label>\n\t/** Unix timestamp (seconds) of the last dispatch-loop iteration; staleness ⇒ stuck/dead worker. */\n\tpublic readonly workerHeartbeat: prom.Gauge<Label>\n\t/** Dispatch-loop crashes (each is auto-restarted by the supervisor). */\n\tpublic readonly workerCrashed: prom.Counter<Label>\n\n\tconstructor(registry: Registry) {\n\t\tconst labelNames: Label[] = [LABEL_PROJECT]\n\t\tthis.eventsEnqueued = new prom.Counter({\n\t\t\tregisters: [registry],\n\t\t\tname: 'contember_actions_events_enqueued_total',\n\t\t\thelp: 'Total number of action events written to the queue (inflow), by contember_project.',\n\t\t\tlabelNames,\n\t\t})\n\t\tthis.eventsSucceeded = new prom.Counter({\n\t\t\tregisters: [registry],\n\t\t\tname: 'contember_actions_events_succeeded_total',\n\t\t\thelp: 'Total number of action events delivered successfully, by contember_project.',\n\t\t\tlabelNames,\n\t\t})\n\t\tthis.deliveryAttemptsFailed = new prom.Counter({\n\t\t\tregisters: [registry],\n\t\t\tname: 'contember_actions_delivery_attempts_failed_total',\n\t\t\thelp: 'Total number of failed webhook delivery attempts (including attempts that will be retried), by contember_project.',\n\t\t\tlabelNames,\n\t\t})\n\t\tthis.eventsFailed = new prom.Counter({\n\t\t\tregisters: [registry],\n\t\t\tname: 'contember_actions_events_failed_total',\n\t\t\thelp: 'Total number of action events that reached a terminal failed state (retries exhausted or unknown target), by contember_project.',\n\t\t\tlabelNames,\n\t\t})\n\t\tthis.workerHeartbeat = new prom.Gauge({\n\t\t\tregisters: [registry],\n\t\t\tname: 'contember_actions_worker_heartbeat_timestamp_seconds',\n\t\t\thelp: 'Unix timestamp of the last dispatch-loop iteration per contember_project; staleness indicates a stuck or dead worker.',\n\t\t\tlabelNames,\n\t\t})\n\t\tthis.workerCrashed = new prom.Counter({\n\t\t\tregisters: [registry],\n\t\t\tname: 'contember_actions_worker_crashed_total',\n\t\t\thelp: 'Total number of dispatch-loop crashes per contember_project (each is auto-restarted by the supervisor).',\n\t\t\tlabelNames,\n\t\t})\n\t}\n\n\tpublic forProject(projectSlug: string): ProjectActionsMetrics {\n\t\treturn new ProjectActionsMetrics(this, projectSlug)\n\t}\n}\n\n/**\n * A view of {@link ActionsMetrics} bound to a single project, so call sites don't repeat the label.\n */\nexport class ProjectActionsMetrics {\n\tprivate readonly labels: { [LABEL_PROJECT]: string }\n\n\tconstructor(\n\t\tprivate readonly metrics: ActionsMetrics,\n\t\tprivate readonly projectSlug: string,\n\t) {\n\t\tthis.labels = { [LABEL_PROJECT]: projectSlug }\n\t}\n\n\tpublic enqueued(count: number): void {\n\t\tif (count > 0) {\n\t\t\tthis.metrics.eventsEnqueued.inc(this.labels, count)\n\t\t}\n\t}\n\n\tpublic succeeded(count: number): void {\n\t\tif (count > 0) {\n\t\t\tthis.metrics.eventsSucceeded.inc(this.labels, count)\n\t\t}\n\t}\n\n\tpublic deliveryAttemptFailed(count: number): void {\n\t\tif (count > 0) {\n\t\t\tthis.metrics.deliveryAttemptsFailed.inc(this.labels, count)\n\t\t}\n\t}\n\n\tpublic failed(count: number): void {\n\t\tif (count > 0) {\n\t\t\tthis.metrics.eventsFailed.inc(this.labels, count)\n\t\t}\n\t}\n\n\tpublic heartbeat(): void {\n\t\tthis.metrics.workerHeartbeat.set(this.labels, Date.now() / 1000)\n\t}\n\n\tpublic crashed(): void {\n\t\tthis.metrics.workerCrashed.inc(this.labels, 1)\n\t}\n\n\t/** Drop the per-project heartbeat series when the worker stops, so a removed project doesn't look stuck. */\n\tpublic dispose(): void {\n\t\tthis.metrics.workerHeartbeat.remove(this.projectSlug)\n\t}\n}\n"],"names":[],"mappings":";;;;AAEA,MAAM,gBAAgB;AAwBf,MAAM,eAAe;AAAA,EAc3B,YAAY,UAAoB;AAZhC,kBAAA,MAAgB,gBAAA;AAEhB,kBAAA,MAAgB,iBAAA;AAEhB,kBAAA,MAAgB,wBAAA;AAEhB,kBAAA,MAAgB,cAAA;AAEhB,kBAAA,MAAgB,iBAAA;AAEhB,kBAAA,MAAgB,eAAA;AAGf,UAAM,aAAsB,CAAC,aAAa;AAC1C,SAAK,iBAAiB,IAAI,KAAK,QAAQ;AAAA,MACtC,WAAW,CAAC,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IAAA,CACA;AACD,SAAK,kBAAkB,IAAI,KAAK,QAAQ;AAAA,MACvC,WAAW,CAAC,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IAAA,CACA;AACD,SAAK,yBAAyB,IAAI,KAAK,QAAQ;AAAA,MAC9C,WAAW,CAAC,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IAAA,CACA;AACD,SAAK,eAAe,IAAI,KAAK,QAAQ;AAAA,MACpC,WAAW,CAAC,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IAAA,CACA;AACD,SAAK,kBAAkB,IAAI,KAAK,MAAM;AAAA,MACrC,WAAW,CAAC,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IAAA,CACA;AACD,SAAK,gBAAgB,IAAI,KAAK,QAAQ;AAAA,MACrC,WAAW,CAAC,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,IAAA,CACA;AAAA,EACF;AAAA,EAEO,WAAW,aAA4C;AAC7D,WAAO,IAAI,sBAAsB,MAAM,WAAW;AAAA,EACnD;AACD;AAKO,MAAM,sBAAsB;AAAA,EAGlC,YACkB,SACA,aAChB;AAFgB,SAAA,UAAA;AACA,SAAA,cAAA;AAJlB,kBAAA,MAAiB,QAAA;AAMhB,SAAK,SAAS,EAAE,CAAC,aAAa,GAAG,YAAA;AAAA,EAClC;AAAA,EAEO,SAAS,OAAqB;AACpC,QAAI,QAAQ,GAAG;AACd,WAAK,QAAQ,eAAe,IAAI,KAAK,QAAQ,KAAK;AAAA,IACnD;AAAA,EACD;AAAA,EAEO,UAAU,OAAqB;AACrC,QAAI,QAAQ,GAAG;AACd,WAAK,QAAQ,gBAAgB,IAAI,KAAK,QAAQ,KAAK;AAAA,IACpD;AAAA,EACD;AAAA,EAEO,sBAAsB,OAAqB;AACjD,QAAI,QAAQ,GAAG;AACd,WAAK,QAAQ,uBAAuB,IAAI,KAAK,QAAQ,KAAK;AAAA,IAC3D;AAAA,EACD;AAAA,EAEO,OAAO,OAAqB;AAClC,QAAI,QAAQ,GAAG;AACd,WAAK,QAAQ,aAAa,IAAI,KAAK,QAAQ,KAAK;AAAA,IACjD;AAAA,EACD;AAAA,EAEO,YAAkB;AACxB,SAAK,QAAQ,gBAAgB,IAAI,KAAK,QAAQ,KAAK,IAAA,IAAQ,GAAI;AAAA,EAChE;AAAA,EAEO,UAAgB;AACtB,SAAK,QAAQ,cAAc,IAAI,KAAK,QAAQ,CAAC;AAAA,EAC9C;AAAA;AAAA,EAGO,UAAgB;AACtB,SAAK,QAAQ,gBAAgB,OAAO,KAAK,WAAW;AAAA,EACrD;AACD;"}
@@ -6,16 +6,22 @@ class EventDispatcher {
6
6
  this.variablesManager = variablesManager;
7
7
  this.invokeResolver = invokeResolver;
8
8
  }
9
- async processBatch({ db, contentSchemaResolver, logger }) {
9
+ async processBatch({ db, contentSchemaResolver, logger, project }) {
10
10
  const schema = (await contentSchemaResolver.getSchema({ db, normalize: true })).schema;
11
- const batch = await this.eventsRepository.fetchBatch(schema.actions, db.client);
12
11
  const batchId = Math.random().toString().substring(2);
13
12
  const batchLogger = logger.child({ loc: "Actions.EventDispatcher", batchId });
13
+ const batch = await this.eventsRepository.fetchBatch(schema.actions, db.client, batchLogger);
14
14
  if (!batch.ok) {
15
15
  batchLogger.debug("Nothing to process", {
16
16
  backoffMs: batch.backoffMs ?? "undefined"
17
17
  });
18
- return { failed: 0, succeed: 0, backoffMs: batch.backoffMs };
18
+ return {
19
+ succeeded: 0,
20
+ retried: 0,
21
+ failedAfterAttempt: 0,
22
+ failedUnknownTarget: batch.unknownTargetFailed,
23
+ backoffMs: batch.backoffMs
24
+ };
19
25
  }
20
26
  const { target, events } = batch;
21
27
  try {
@@ -23,19 +29,31 @@ class EventDispatcher {
23
29
  batchLogger.debug("Processing started", {
24
30
  events: batch.events.map((it) => it.id)
25
31
  });
26
- const variables = await this.variablesManager.fetchVariables(db);
32
+ const variables = await this.variablesManager.fetchVariables(db, project.slug);
27
33
  const handledEvents = await handler.handle({ target, events, logger: batchLogger, variables });
28
- const [succeed, failed] = await this.eventsRepository.persistProcessed(db.client, handledEvents);
29
- batchLogger.debug("Processing done", { succeed, failed });
30
- return { succeed, failed, backoffMs: 0 };
34
+ const { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, handledEvents, batchLogger);
35
+ batchLogger.debug("Processing done", { succeed: succeeded, failed });
36
+ return {
37
+ succeeded,
38
+ retried,
39
+ failedAfterAttempt: failed,
40
+ failedUnknownTarget: batch.unknownTargetFailed,
41
+ backoffMs: 0
42
+ };
31
43
  } catch (e) {
32
44
  logger.error(e, { loc: "EventDispatcher", batchId });
33
45
  const failedEvents = events.map((it) => ({
34
46
  row: it,
35
47
  result: { ok: false, errorMessage: `Internal error` }
36
48
  }));
37
- await this.eventsRepository.persistProcessed(db.client, failedEvents);
38
- return { succeed: 0, failed: batch.events.length, backoffMs: 0 };
49
+ const { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, failedEvents, batchLogger);
50
+ return {
51
+ succeeded,
52
+ retried,
53
+ failedAfterAttempt: failed,
54
+ failedUnknownTarget: batch.unknownTargetFailed,
55
+ backoffMs: 0
56
+ };
39
57
  }
40
58
  }
41
59
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EventDispatcher.cjs","sources":["../../../../../packages/engine-actions/src/dispatch/EventDispatcher.ts"],"sourcesContent":["import { EventsRepository } from './EventsRepository.js'\nimport { TargetHandlerResolver } from './TargetHandlerResolver.js'\nimport { HandledEvent } from './types.js'\nimport { Logger } from '@contember/logger'\nimport { ContentSchemaResolver } from '@contember/engine-http'\nimport { DatabaseContext } from '@contember/engine-system-api'\nimport { VariablesManager } from '../model/VariablesManager.js'\n\ntype ProcessBatchArgs = {\n\tdb: DatabaseContext\n\tcontentSchemaResolver: ContentSchemaResolver\n\tlogger: Logger\n}\n\ntype ProcessBatchResult = {\n\tsucceed: number\n\tfailed: number\n\tbackoffMs: number | undefined\n}\n\nexport class EventDispatcher {\n\tconstructor(\n\t\tprivate readonly eventsRepository: EventsRepository,\n\t\tprivate readonly variablesManager: VariablesManager,\n\t\tprivate readonly invokeResolver: TargetHandlerResolver,\n\t) {\n\t}\n\n\tasync processBatch({ db, contentSchemaResolver, logger }: ProcessBatchArgs): Promise<ProcessBatchResult> {\n\t\tconst schema = (await contentSchemaResolver.getSchema({ db, normalize: true })).schema\n\t\tconst batch = await this.eventsRepository.fetchBatch(schema.actions, db.client)\n\t\tconst batchId = Math.random().toString().substring(2)\n\t\tconst batchLogger = logger.child({ loc: 'Actions.EventDispatcher', batchId })\n\t\tif (!batch.ok) {\n\t\t\tbatchLogger.debug('Nothing to process', {\n\t\t\t\tbackoffMs: batch.backoffMs ?? 'undefined',\n\t\t\t})\n\t\t\treturn { failed: 0, succeed: 0, backoffMs: batch.backoffMs }\n\t\t}\n\t\tconst { target, events } = batch\n\t\ttry {\n\t\t\tconst handler = this.invokeResolver.resolveHandler(target)\n\t\t\tbatchLogger.debug('Processing started', {\n\t\t\t\tevents: batch.events.map(it => it.id),\n\t\t\t})\n\t\t\tconst variables = await this.variablesManager.fetchVariables(db)\n\t\t\tconst handledEvents = await handler.handle({ target, events, logger: batchLogger, variables })\n\t\t\tconst [succeed, failed] = await this.eventsRepository.persistProcessed(db.client, handledEvents)\n\t\t\tbatchLogger.debug('Processing done', { succeed, failed })\n\t\t\treturn { succeed, failed, backoffMs: 0 }\n\t\t} catch (e) {\n\t\t\tlogger.error(e, { loc: 'EventDispatcher', batchId })\n\t\t\tconst failedEvents = events.map((it): HandledEvent => ({\n\t\t\t\trow: it,\n\t\t\t\tresult: { ok: false, errorMessage: `Internal error` },\n\t\t\t}))\n\t\t\tawait this.eventsRepository.persistProcessed(db.client, failedEvents)\n\n\t\t\treturn { succeed: 0, failed: batch.events.length, backoffMs: 0 }\n\t\t}\n\t}\n}\n"],"names":[],"mappings":";;AAoBO,MAAM,gBAAgB;AAAA,EAC5B,YACkB,kBACA,kBACA,gBAChB;AAHgB,SAAA,mBAAA;AACA,SAAA,mBAAA;AACA,SAAA,iBAAA;AAAA,EAElB;AAAA,EAEA,MAAM,aAAa,EAAE,IAAI,uBAAuB,UAAyD;AACxG,UAAM,UAAU,MAAM,sBAAsB,UAAU,EAAE,IAAI,WAAW,KAAA,CAAM,GAAG;AAChF,UAAM,QAAQ,MAAM,KAAK,iBAAiB,WAAW,OAAO,SAAS,GAAG,MAAM;AAC9E,UAAM,UAAU,KAAK,OAAA,EAAS,SAAA,EAAW,UAAU,CAAC;AACpD,UAAM,cAAc,OAAO,MAAM,EAAE,KAAK,2BAA2B,SAAS;AAC5E,QAAI,CAAC,MAAM,IAAI;AACd,kBAAY,MAAM,sBAAsB;AAAA,QACvC,WAAW,MAAM,aAAa;AAAA,MAAA,CAC9B;AACD,aAAO,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,MAAM,UAAA;AAAA,IAClD;AACA,UAAM,EAAE,QAAQ,OAAA,IAAW;AAC3B,QAAI;AACH,YAAM,UAAU,KAAK,eAAe,eAAe,MAAM;AACzD,kBAAY,MAAM,sBAAsB;AAAA,QACvC,QAAQ,MAAM,OAAO,IAAI,CAAA,OAAM,GAAG,EAAE;AAAA,MAAA,CACpC;AACD,YAAM,YAAY,MAAM,KAAK,iBAAiB,eAAe,EAAE;AAC/D,YAAM,gBAAgB,MAAM,QAAQ,OAAO,EAAE,QAAQ,QAAQ,QAAQ,aAAa,WAAW;AAC7F,YAAM,CAAC,SAAS,MAAM,IAAI,MAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,aAAa;AAC/F,kBAAY,MAAM,mBAAmB,EAAE,SAAS,QAAQ;AACxD,aAAO,EAAE,SAAS,QAAQ,WAAW,EAAA;AAAA,IACtC,SAAS,GAAG;AACX,aAAO,MAAM,GAAG,EAAE,KAAK,mBAAmB,SAAS;AACnD,YAAM,eAAe,OAAO,IAAI,CAAC,QAAsB;AAAA,QACtD,KAAK;AAAA,QACL,QAAQ,EAAE,IAAI,OAAO,cAAc,iBAAA;AAAA,MAAiB,EACnD;AACF,YAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,YAAY;AAEpE,aAAO,EAAE,SAAS,GAAG,QAAQ,MAAM,OAAO,QAAQ,WAAW,EAAA;AAAA,IAC9D;AAAA,EACD;AACD;;"}
1
+ {"version":3,"file":"EventDispatcher.cjs","sources":["../../../../../packages/engine-actions/src/dispatch/EventDispatcher.ts"],"sourcesContent":["import { EventsRepository } from './EventsRepository.js'\nimport { TargetHandlerResolver } from './TargetHandlerResolver.js'\nimport { HandledEvent } from './types.js'\nimport { Logger } from '@contember/logger'\nimport { ContentSchemaResolver } from '@contember/engine-http'\nimport { DatabaseContext } from '@contember/engine-system-api'\nimport { VariablesManager } from '../model/VariablesManager.js'\n\ntype ProcessBatchArgs = {\n\tdb: DatabaseContext\n\tcontentSchemaResolver: ContentSchemaResolver\n\tlogger: Logger\n\tproject: { slug: string }\n}\n\ntype ProcessBatchResult = {\n\t/** Events delivered successfully (terminal). */\n\tsucceeded: number\n\t/** Failed delivery attempts that will be retried. */\n\tretried: number\n\t/** Events terminally failed after a delivery attempt (retries exhausted). */\n\tfailedAfterAttempt: number\n\t/** Events terminally failed without a delivery attempt (target missing from schema). */\n\tfailedUnknownTarget: number\n\tbackoffMs: number | undefined\n}\n\nexport class EventDispatcher {\n\tconstructor(\n\t\tprivate readonly eventsRepository: EventsRepository,\n\t\tprivate readonly variablesManager: VariablesManager,\n\t\tprivate readonly invokeResolver: TargetHandlerResolver,\n\t) {\n\t}\n\n\tasync processBatch({ db, contentSchemaResolver, logger, project }: ProcessBatchArgs): Promise<ProcessBatchResult> {\n\t\tconst schema = (await contentSchemaResolver.getSchema({ db, normalize: true })).schema\n\t\tconst batchId = Math.random().toString().substring(2)\n\t\tconst batchLogger = logger.child({ loc: 'Actions.EventDispatcher', batchId })\n\t\tconst batch = await this.eventsRepository.fetchBatch(schema.actions, db.client, batchLogger)\n\t\tif (!batch.ok) {\n\t\t\tbatchLogger.debug('Nothing to process', {\n\t\t\t\tbackoffMs: batch.backoffMs ?? 'undefined',\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tsucceeded: 0,\n\t\t\t\tretried: 0,\n\t\t\t\tfailedAfterAttempt: 0,\n\t\t\t\tfailedUnknownTarget: batch.unknownTargetFailed,\n\t\t\t\tbackoffMs: batch.backoffMs,\n\t\t\t}\n\t\t}\n\t\tconst { target, events } = batch\n\t\ttry {\n\t\t\tconst handler = this.invokeResolver.resolveHandler(target)\n\t\t\tbatchLogger.debug('Processing started', {\n\t\t\t\tevents: batch.events.map(it => it.id),\n\t\t\t})\n\t\t\tconst variables = await this.variablesManager.fetchVariables(db, project.slug)\n\t\t\tconst handledEvents = await handler.handle({ target, events, logger: batchLogger, variables })\n\t\t\tconst { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, handledEvents, batchLogger)\n\t\t\tbatchLogger.debug('Processing done', { succeed: succeeded, failed })\n\t\t\treturn {\n\t\t\t\tsucceeded,\n\t\t\t\tretried,\n\t\t\t\tfailedAfterAttempt: failed,\n\t\t\t\tfailedUnknownTarget: batch.unknownTargetFailed,\n\t\t\t\tbackoffMs: 0,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tlogger.error(e, { loc: 'EventDispatcher', batchId })\n\t\t\tconst failedEvents = events.map((it): HandledEvent => ({\n\t\t\t\trow: it,\n\t\t\t\tresult: { ok: false, errorMessage: `Internal error` },\n\t\t\t}))\n\t\t\tconst { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, failedEvents, batchLogger)\n\n\t\t\treturn {\n\t\t\t\tsucceeded,\n\t\t\t\tretried,\n\t\t\t\tfailedAfterAttempt: failed,\n\t\t\t\tfailedUnknownTarget: batch.unknownTargetFailed,\n\t\t\t\tbackoffMs: 0,\n\t\t\t}\n\t\t}\n\t}\n}\n"],"names":[],"mappings":";;AA2BO,MAAM,gBAAgB;AAAA,EAC5B,YACkB,kBACA,kBACA,gBAChB;AAHgB,SAAA,mBAAA;AACA,SAAA,mBAAA;AACA,SAAA,iBAAA;AAAA,EAElB;AAAA,EAEA,MAAM,aAAa,EAAE,IAAI,uBAAuB,QAAQ,WAA0D;AACjH,UAAM,UAAU,MAAM,sBAAsB,UAAU,EAAE,IAAI,WAAW,KAAA,CAAM,GAAG;AAChF,UAAM,UAAU,KAAK,OAAA,EAAS,SAAA,EAAW,UAAU,CAAC;AACpD,UAAM,cAAc,OAAO,MAAM,EAAE,KAAK,2BAA2B,SAAS;AAC5E,UAAM,QAAQ,MAAM,KAAK,iBAAiB,WAAW,OAAO,SAAS,GAAG,QAAQ,WAAW;AAC3F,QAAI,CAAC,MAAM,IAAI;AACd,kBAAY,MAAM,sBAAsB;AAAA,QACvC,WAAW,MAAM,aAAa;AAAA,MAAA,CAC9B;AACD,aAAO;AAAA,QACN,WAAW;AAAA,QACX,SAAS;AAAA,QACT,oBAAoB;AAAA,QACpB,qBAAqB,MAAM;AAAA,QAC3B,WAAW,MAAM;AAAA,MAAA;AAAA,IAEnB;AACA,UAAM,EAAE,QAAQ,OAAA,IAAW;AAC3B,QAAI;AACH,YAAM,UAAU,KAAK,eAAe,eAAe,MAAM;AACzD,kBAAY,MAAM,sBAAsB;AAAA,QACvC,QAAQ,MAAM,OAAO,IAAI,CAAA,OAAM,GAAG,EAAE;AAAA,MAAA,CACpC;AACD,YAAM,YAAY,MAAM,KAAK,iBAAiB,eAAe,IAAI,QAAQ,IAAI;AAC7E,YAAM,gBAAgB,MAAM,QAAQ,OAAO,EAAE,QAAQ,QAAQ,QAAQ,aAAa,WAAW;AAC7F,YAAM,EAAE,WAAW,SAAS,OAAA,IAAW,MAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,eAAe,WAAW;AACzH,kBAAY,MAAM,mBAAmB,EAAE,SAAS,WAAW,QAAQ;AACnE,aAAO;AAAA,QACN;AAAA,QACA;AAAA,QACA,oBAAoB;AAAA,QACpB,qBAAqB,MAAM;AAAA,QAC3B,WAAW;AAAA,MAAA;AAAA,IAEb,SAAS,GAAG;AACX,aAAO,MAAM,GAAG,EAAE,KAAK,mBAAmB,SAAS;AACnD,YAAM,eAAe,OAAO,IAAI,CAAC,QAAsB;AAAA,QACtD,KAAK;AAAA,QACL,QAAQ,EAAE,IAAI,OAAO,cAAc,iBAAA;AAAA,MAAiB,EACnD;AACF,YAAM,EAAE,WAAW,SAAS,OAAA,IAAW,MAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,cAAc,WAAW;AAExH,aAAO;AAAA,QACN;AAAA,QACA;AAAA,QACA,oBAAoB;AAAA,QACpB,qBAAqB,MAAM;AAAA,QAC3B,WAAW;AAAA,MAAA;AAAA,IAEb;AAAA,EACD;AACD;;"}
@@ -4,16 +4,22 @@ class EventDispatcher {
4
4
  this.variablesManager = variablesManager;
5
5
  this.invokeResolver = invokeResolver;
6
6
  }
7
- async processBatch({ db, contentSchemaResolver, logger }) {
7
+ async processBatch({ db, contentSchemaResolver, logger, project }) {
8
8
  const schema = (await contentSchemaResolver.getSchema({ db, normalize: true })).schema;
9
- const batch = await this.eventsRepository.fetchBatch(schema.actions, db.client);
10
9
  const batchId = Math.random().toString().substring(2);
11
10
  const batchLogger = logger.child({ loc: "Actions.EventDispatcher", batchId });
11
+ const batch = await this.eventsRepository.fetchBatch(schema.actions, db.client, batchLogger);
12
12
  if (!batch.ok) {
13
13
  batchLogger.debug("Nothing to process", {
14
14
  backoffMs: batch.backoffMs ?? "undefined"
15
15
  });
16
- return { failed: 0, succeed: 0, backoffMs: batch.backoffMs };
16
+ return {
17
+ succeeded: 0,
18
+ retried: 0,
19
+ failedAfterAttempt: 0,
20
+ failedUnknownTarget: batch.unknownTargetFailed,
21
+ backoffMs: batch.backoffMs
22
+ };
17
23
  }
18
24
  const { target, events } = batch;
19
25
  try {
@@ -21,19 +27,31 @@ class EventDispatcher {
21
27
  batchLogger.debug("Processing started", {
22
28
  events: batch.events.map((it) => it.id)
23
29
  });
24
- const variables = await this.variablesManager.fetchVariables(db);
30
+ const variables = await this.variablesManager.fetchVariables(db, project.slug);
25
31
  const handledEvents = await handler.handle({ target, events, logger: batchLogger, variables });
26
- const [succeed, failed] = await this.eventsRepository.persistProcessed(db.client, handledEvents);
27
- batchLogger.debug("Processing done", { succeed, failed });
28
- return { succeed, failed, backoffMs: 0 };
32
+ const { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, handledEvents, batchLogger);
33
+ batchLogger.debug("Processing done", { succeed: succeeded, failed });
34
+ return {
35
+ succeeded,
36
+ retried,
37
+ failedAfterAttempt: failed,
38
+ failedUnknownTarget: batch.unknownTargetFailed,
39
+ backoffMs: 0
40
+ };
29
41
  } catch (e) {
30
42
  logger.error(e, { loc: "EventDispatcher", batchId });
31
43
  const failedEvents = events.map((it) => ({
32
44
  row: it,
33
45
  result: { ok: false, errorMessage: `Internal error` }
34
46
  }));
35
- await this.eventsRepository.persistProcessed(db.client, failedEvents);
36
- return { succeed: 0, failed: batch.events.length, backoffMs: 0 };
47
+ const { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, failedEvents, batchLogger);
48
+ return {
49
+ succeeded,
50
+ retried,
51
+ failedAfterAttempt: failed,
52
+ failedUnknownTarget: batch.unknownTargetFailed,
53
+ backoffMs: 0
54
+ };
37
55
  }
38
56
  }
39
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EventDispatcher.js","sources":["../../../../../packages/engine-actions/src/dispatch/EventDispatcher.ts"],"sourcesContent":["import { EventsRepository } from './EventsRepository.js'\nimport { TargetHandlerResolver } from './TargetHandlerResolver.js'\nimport { HandledEvent } from './types.js'\nimport { Logger } from '@contember/logger'\nimport { ContentSchemaResolver } from '@contember/engine-http'\nimport { DatabaseContext } from '@contember/engine-system-api'\nimport { VariablesManager } from '../model/VariablesManager.js'\n\ntype ProcessBatchArgs = {\n\tdb: DatabaseContext\n\tcontentSchemaResolver: ContentSchemaResolver\n\tlogger: Logger\n}\n\ntype ProcessBatchResult = {\n\tsucceed: number\n\tfailed: number\n\tbackoffMs: number | undefined\n}\n\nexport class EventDispatcher {\n\tconstructor(\n\t\tprivate readonly eventsRepository: EventsRepository,\n\t\tprivate readonly variablesManager: VariablesManager,\n\t\tprivate readonly invokeResolver: TargetHandlerResolver,\n\t) {\n\t}\n\n\tasync processBatch({ db, contentSchemaResolver, logger }: ProcessBatchArgs): Promise<ProcessBatchResult> {\n\t\tconst schema = (await contentSchemaResolver.getSchema({ db, normalize: true })).schema\n\t\tconst batch = await this.eventsRepository.fetchBatch(schema.actions, db.client)\n\t\tconst batchId = Math.random().toString().substring(2)\n\t\tconst batchLogger = logger.child({ loc: 'Actions.EventDispatcher', batchId })\n\t\tif (!batch.ok) {\n\t\t\tbatchLogger.debug('Nothing to process', {\n\t\t\t\tbackoffMs: batch.backoffMs ?? 'undefined',\n\t\t\t})\n\t\t\treturn { failed: 0, succeed: 0, backoffMs: batch.backoffMs }\n\t\t}\n\t\tconst { target, events } = batch\n\t\ttry {\n\t\t\tconst handler = this.invokeResolver.resolveHandler(target)\n\t\t\tbatchLogger.debug('Processing started', {\n\t\t\t\tevents: batch.events.map(it => it.id),\n\t\t\t})\n\t\t\tconst variables = await this.variablesManager.fetchVariables(db)\n\t\t\tconst handledEvents = await handler.handle({ target, events, logger: batchLogger, variables })\n\t\t\tconst [succeed, failed] = await this.eventsRepository.persistProcessed(db.client, handledEvents)\n\t\t\tbatchLogger.debug('Processing done', { succeed, failed })\n\t\t\treturn { succeed, failed, backoffMs: 0 }\n\t\t} catch (e) {\n\t\t\tlogger.error(e, { loc: 'EventDispatcher', batchId })\n\t\t\tconst failedEvents = events.map((it): HandledEvent => ({\n\t\t\t\trow: it,\n\t\t\t\tresult: { ok: false, errorMessage: `Internal error` },\n\t\t\t}))\n\t\t\tawait this.eventsRepository.persistProcessed(db.client, failedEvents)\n\n\t\t\treturn { succeed: 0, failed: batch.events.length, backoffMs: 0 }\n\t\t}\n\t}\n}\n"],"names":[],"mappings":"AAoBO,MAAM,gBAAgB;AAAA,EAC5B,YACkB,kBACA,kBACA,gBAChB;AAHgB,SAAA,mBAAA;AACA,SAAA,mBAAA;AACA,SAAA,iBAAA;AAAA,EAElB;AAAA,EAEA,MAAM,aAAa,EAAE,IAAI,uBAAuB,UAAyD;AACxG,UAAM,UAAU,MAAM,sBAAsB,UAAU,EAAE,IAAI,WAAW,KAAA,CAAM,GAAG;AAChF,UAAM,QAAQ,MAAM,KAAK,iBAAiB,WAAW,OAAO,SAAS,GAAG,MAAM;AAC9E,UAAM,UAAU,KAAK,OAAA,EAAS,SAAA,EAAW,UAAU,CAAC;AACpD,UAAM,cAAc,OAAO,MAAM,EAAE,KAAK,2BAA2B,SAAS;AAC5E,QAAI,CAAC,MAAM,IAAI;AACd,kBAAY,MAAM,sBAAsB;AAAA,QACvC,WAAW,MAAM,aAAa;AAAA,MAAA,CAC9B;AACD,aAAO,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,MAAM,UAAA;AAAA,IAClD;AACA,UAAM,EAAE,QAAQ,OAAA,IAAW;AAC3B,QAAI;AACH,YAAM,UAAU,KAAK,eAAe,eAAe,MAAM;AACzD,kBAAY,MAAM,sBAAsB;AAAA,QACvC,QAAQ,MAAM,OAAO,IAAI,CAAA,OAAM,GAAG,EAAE;AAAA,MAAA,CACpC;AACD,YAAM,YAAY,MAAM,KAAK,iBAAiB,eAAe,EAAE;AAC/D,YAAM,gBAAgB,MAAM,QAAQ,OAAO,EAAE,QAAQ,QAAQ,QAAQ,aAAa,WAAW;AAC7F,YAAM,CAAC,SAAS,MAAM,IAAI,MAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,aAAa;AAC/F,kBAAY,MAAM,mBAAmB,EAAE,SAAS,QAAQ;AACxD,aAAO,EAAE,SAAS,QAAQ,WAAW,EAAA;AAAA,IACtC,SAAS,GAAG;AACX,aAAO,MAAM,GAAG,EAAE,KAAK,mBAAmB,SAAS;AACnD,YAAM,eAAe,OAAO,IAAI,CAAC,QAAsB;AAAA,QACtD,KAAK;AAAA,QACL,QAAQ,EAAE,IAAI,OAAO,cAAc,iBAAA;AAAA,MAAiB,EACnD;AACF,YAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,YAAY;AAEpE,aAAO,EAAE,SAAS,GAAG,QAAQ,MAAM,OAAO,QAAQ,WAAW,EAAA;AAAA,IAC9D;AAAA,EACD;AACD;"}
1
+ {"version":3,"file":"EventDispatcher.js","sources":["../../../../../packages/engine-actions/src/dispatch/EventDispatcher.ts"],"sourcesContent":["import { EventsRepository } from './EventsRepository.js'\nimport { TargetHandlerResolver } from './TargetHandlerResolver.js'\nimport { HandledEvent } from './types.js'\nimport { Logger } from '@contember/logger'\nimport { ContentSchemaResolver } from '@contember/engine-http'\nimport { DatabaseContext } from '@contember/engine-system-api'\nimport { VariablesManager } from '../model/VariablesManager.js'\n\ntype ProcessBatchArgs = {\n\tdb: DatabaseContext\n\tcontentSchemaResolver: ContentSchemaResolver\n\tlogger: Logger\n\tproject: { slug: string }\n}\n\ntype ProcessBatchResult = {\n\t/** Events delivered successfully (terminal). */\n\tsucceeded: number\n\t/** Failed delivery attempts that will be retried. */\n\tretried: number\n\t/** Events terminally failed after a delivery attempt (retries exhausted). */\n\tfailedAfterAttempt: number\n\t/** Events terminally failed without a delivery attempt (target missing from schema). */\n\tfailedUnknownTarget: number\n\tbackoffMs: number | undefined\n}\n\nexport class EventDispatcher {\n\tconstructor(\n\t\tprivate readonly eventsRepository: EventsRepository,\n\t\tprivate readonly variablesManager: VariablesManager,\n\t\tprivate readonly invokeResolver: TargetHandlerResolver,\n\t) {\n\t}\n\n\tasync processBatch({ db, contentSchemaResolver, logger, project }: ProcessBatchArgs): Promise<ProcessBatchResult> {\n\t\tconst schema = (await contentSchemaResolver.getSchema({ db, normalize: true })).schema\n\t\tconst batchId = Math.random().toString().substring(2)\n\t\tconst batchLogger = logger.child({ loc: 'Actions.EventDispatcher', batchId })\n\t\tconst batch = await this.eventsRepository.fetchBatch(schema.actions, db.client, batchLogger)\n\t\tif (!batch.ok) {\n\t\t\tbatchLogger.debug('Nothing to process', {\n\t\t\t\tbackoffMs: batch.backoffMs ?? 'undefined',\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tsucceeded: 0,\n\t\t\t\tretried: 0,\n\t\t\t\tfailedAfterAttempt: 0,\n\t\t\t\tfailedUnknownTarget: batch.unknownTargetFailed,\n\t\t\t\tbackoffMs: batch.backoffMs,\n\t\t\t}\n\t\t}\n\t\tconst { target, events } = batch\n\t\ttry {\n\t\t\tconst handler = this.invokeResolver.resolveHandler(target)\n\t\t\tbatchLogger.debug('Processing started', {\n\t\t\t\tevents: batch.events.map(it => it.id),\n\t\t\t})\n\t\t\tconst variables = await this.variablesManager.fetchVariables(db, project.slug)\n\t\t\tconst handledEvents = await handler.handle({ target, events, logger: batchLogger, variables })\n\t\t\tconst { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, handledEvents, batchLogger)\n\t\t\tbatchLogger.debug('Processing done', { succeed: succeeded, failed })\n\t\t\treturn {\n\t\t\t\tsucceeded,\n\t\t\t\tretried,\n\t\t\t\tfailedAfterAttempt: failed,\n\t\t\t\tfailedUnknownTarget: batch.unknownTargetFailed,\n\t\t\t\tbackoffMs: 0,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tlogger.error(e, { loc: 'EventDispatcher', batchId })\n\t\t\tconst failedEvents = events.map((it): HandledEvent => ({\n\t\t\t\trow: it,\n\t\t\t\tresult: { ok: false, errorMessage: `Internal error` },\n\t\t\t}))\n\t\t\tconst { succeeded, retried, failed } = await this.eventsRepository.persistProcessed(db.client, failedEvents, batchLogger)\n\n\t\t\treturn {\n\t\t\t\tsucceeded,\n\t\t\t\tretried,\n\t\t\t\tfailedAfterAttempt: failed,\n\t\t\t\tfailedUnknownTarget: batch.unknownTargetFailed,\n\t\t\t\tbackoffMs: 0,\n\t\t\t}\n\t\t}\n\t}\n}\n"],"names":[],"mappings":"AA2BO,MAAM,gBAAgB;AAAA,EAC5B,YACkB,kBACA,kBACA,gBAChB;AAHgB,SAAA,mBAAA;AACA,SAAA,mBAAA;AACA,SAAA,iBAAA;AAAA,EAElB;AAAA,EAEA,MAAM,aAAa,EAAE,IAAI,uBAAuB,QAAQ,WAA0D;AACjH,UAAM,UAAU,MAAM,sBAAsB,UAAU,EAAE,IAAI,WAAW,KAAA,CAAM,GAAG;AAChF,UAAM,UAAU,KAAK,OAAA,EAAS,SAAA,EAAW,UAAU,CAAC;AACpD,UAAM,cAAc,OAAO,MAAM,EAAE,KAAK,2BAA2B,SAAS;AAC5E,UAAM,QAAQ,MAAM,KAAK,iBAAiB,WAAW,OAAO,SAAS,GAAG,QAAQ,WAAW;AAC3F,QAAI,CAAC,MAAM,IAAI;AACd,kBAAY,MAAM,sBAAsB;AAAA,QACvC,WAAW,MAAM,aAAa;AAAA,MAAA,CAC9B;AACD,aAAO;AAAA,QACN,WAAW;AAAA,QACX,SAAS;AAAA,QACT,oBAAoB;AAAA,QACpB,qBAAqB,MAAM;AAAA,QAC3B,WAAW,MAAM;AAAA,MAAA;AAAA,IAEnB;AACA,UAAM,EAAE,QAAQ,OAAA,IAAW;AAC3B,QAAI;AACH,YAAM,UAAU,KAAK,eAAe,eAAe,MAAM;AACzD,kBAAY,MAAM,sBAAsB;AAAA,QACvC,QAAQ,MAAM,OAAO,IAAI,CAAA,OAAM,GAAG,EAAE;AAAA,MAAA,CACpC;AACD,YAAM,YAAY,MAAM,KAAK,iBAAiB,eAAe,IAAI,QAAQ,IAAI;AAC7E,YAAM,gBAAgB,MAAM,QAAQ,OAAO,EAAE,QAAQ,QAAQ,QAAQ,aAAa,WAAW;AAC7F,YAAM,EAAE,WAAW,SAAS,OAAA,IAAW,MAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,eAAe,WAAW;AACzH,kBAAY,MAAM,mBAAmB,EAAE,SAAS,WAAW,QAAQ;AACnE,aAAO;AAAA,QACN;AAAA,QACA;AAAA,QACA,oBAAoB;AAAA,QACpB,qBAAqB,MAAM;AAAA,QAC3B,WAAW;AAAA,MAAA;AAAA,IAEb,SAAS,GAAG;AACX,aAAO,MAAM,GAAG,EAAE,KAAK,mBAAmB,SAAS;AACnD,YAAM,eAAe,OAAO,IAAI,CAAC,QAAsB;AAAA,QACtD,KAAK;AAAA,QACL,QAAQ,EAAE,IAAI,OAAO,cAAc,iBAAA;AAAA,MAAiB,EACnD;AACF,YAAM,EAAE,WAAW,SAAS,OAAA,IAAW,MAAM,KAAK,iBAAiB,iBAAiB,GAAG,QAAQ,cAAc,WAAW;AAExH,aAAO;AAAA,QACN;AAAA,QACA;AAAA,QACA,oBAAoB;AAAA,QACpB,qBAAqB,MAAM;AAAA,QAC3B,WAAW;AAAA,MAAA;AAAA,IAEb;AAAA,EACD;AACD;"}
@@ -7,19 +7,29 @@ const DEFAULT_REPEAT_INTERVAL_MS = 5e3;
7
7
  const DEFAULT_MAX_ATTEMPTS = 10;
8
8
  const DEFAULT_BATCH_SIZE = 1;
9
9
  class EventsRepository {
10
- async fetchBatch(actions, db) {
11
- const primaryEvent = (await this.fetchInternal(db, 1))[0];
12
- if (!primaryEvent) {
13
- return { ok: false, backoffMs: await this.fetchBackOff(db) };
14
- }
15
- const target = actions.targets[primaryEvent.target];
16
- if (!target) {
17
- await this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id);
18
- return await this.fetchBatch(actions, db);
10
+ async fetchBatch(actions, db, logger) {
11
+ let unknownTargetFailed = 0;
12
+ while (true) {
13
+ const primaryEvent = (await this.fetchInternal(db, 1))[0];
14
+ if (!primaryEvent) {
15
+ return { ok: false, backoffMs: await this.fetchBackOff(db), unknownTargetFailed };
16
+ }
17
+ const target = actions.targets[primaryEvent.target];
18
+ if (!target) {
19
+ if (await this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id)) {
20
+ unknownTargetFailed++;
21
+ logger.error("Action event failed permanently, its target is not in the schema", {
22
+ eventId: primaryEvent.id,
23
+ target: primaryEvent.target,
24
+ trigger: primaryEvent.trigger
25
+ });
26
+ }
27
+ continue;
28
+ }
29
+ const batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1;
30
+ const batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : [];
31
+ return { ok: true, events: [primaryEvent, ...batch], target, unknownTargetFailed };
19
32
  }
20
- const batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1;
21
- const batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : [];
22
- return { ok: true, events: [primaryEvent, ...batch], target };
23
33
  }
24
34
  async fetchBackOff(db) {
25
35
  const result = await Database.SelectBuilder.create().from("actions_event").match(EventsToProcessSpecification.eventsToProcessStateSpecification).select((it) => it.raw("ceil(extract(epoch from visible_at - now()) * 1000)"), "ms").orderBy("visible_at", "asc").limit(1).getResult(db);
@@ -34,17 +44,32 @@ class EventsRepository {
34
44
  visible_at: visibleAt
35
45
  }).from((it) => it.from("events").where((it2) => it2.columnsEq(["events", "id"], ["actions_event", "id"]))).returning(new Database.Literal("*")).execute(db);
36
46
  }
37
- async persistProcessed(db, events) {
47
+ async persistProcessed(db, events, logger) {
38
48
  return await db.transaction(async (trx) => {
39
49
  await trx.query(Database.Connection.REPEATABLE_READ);
40
50
  const succeed = events.filter((it) => it.result.ok);
41
51
  await this.markSucceed(trx, succeed.map((it) => it.row));
52
+ let retried = 0;
53
+ let failed = 0;
42
54
  for (const event of events) {
43
55
  if (!event.result.ok) {
44
- await this.markFailed(trx, event);
56
+ const terminal = await this.markFailed(trx, event);
57
+ if (terminal) {
58
+ failed++;
59
+ logger.error("Action event failed permanently, no attempts left", {
60
+ eventId: event.row.id,
61
+ target: event.row.target,
62
+ trigger: event.row.trigger,
63
+ attempts: event.row.num_retries + 1,
64
+ code: event.result.code,
65
+ errorMessage: event.result.errorMessage
66
+ });
67
+ } else {
68
+ retried++;
69
+ }
45
70
  }
46
71
  }
47
- return [succeed.length, events.length - succeed.length];
72
+ return { succeeded: succeed.length, retried, failed };
48
73
  });
49
74
  }
50
75
  async requeue(db, events) {
@@ -80,6 +105,7 @@ class EventsRepository {
80
105
  last_state_change: now
81
106
  }).where((it) => it.in("id", rows.map((it2) => it2.id))).execute(db);
82
107
  }
108
+ /** Returns `true` when the event reached a terminal `failed` state, `false` when it will be retried. */
83
109
  async markFailed(db, event) {
84
110
  const numRetries = event.row.num_retries + 1;
85
111
  const now = /* @__PURE__ */ new Date();
@@ -87,23 +113,25 @@ class EventsRepository {
87
113
  nextAttempt.setTime(
88
114
  nextAttempt.getTime() + (event.target?.initialRepeatIntervalMs ?? DEFAULT_REPEAT_INTERVAL_MS) * Math.pow(2, event.row.num_retries)
89
115
  );
116
+ const terminal = numRetries >= (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
90
117
  await Database.UpdateBuilder.create().table("actions_event").values({
91
118
  last_state_change: now,
92
119
  num_retries: numRetries,
93
120
  log: JSON.stringify([...event.row.log, event.result]),
94
- ...numRetries < (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS) ? {
121
+ ...terminal ? {
122
+ state: "failed"
123
+ } : {
95
124
  state: "retrying",
96
125
  visible_at: nextAttempt
97
- } : {
98
- state: "failed"
99
126
  }
100
127
  }).where({ id: event.row.id }).execute(db);
128
+ return terminal;
101
129
  }
102
130
  async markFailedOnUnknownTarget(db, targetName, primaryEventId) {
103
- await Database.UpdateBuilder.create().table("actions_event").where(
131
+ const affectedRows = await Database.UpdateBuilder.create().table("actions_event").where(
104
132
  (it) => it.or(
105
133
  (it2) => it2.and(
106
- (it3) => it3.in("state", ["retrying", "created", "processing"]).compare("visible_at", Database.Operator.lte, "now").compare("target", Database.Operator.eq, targetName)
134
+ (it3) => it3.in("state", ["processing"]).compare("target", Database.Operator.eq, targetName)
107
135
  ).and(
108
136
  (it3) => it3.compare("id", Database.Operator.eq, primaryEventId)
109
137
  )
@@ -116,6 +144,7 @@ class EventsRepository {
116
144
  state: "failed",
117
145
  resolved_at: /* @__PURE__ */ new Date()
118
146
  }).execute(db);
147
+ return affectedRows > 0;
119
148
  }
120
149
  }
121
150
  exports.EventsRepository = EventsRepository;
@@ -1 +1 @@
1
- {"version":3,"file":"EventsRepository.cjs","sources":["../../../../../packages/engine-actions/src/dispatch/EventsRepository.ts"],"sourcesContent":["import { Client, Connection, Literal, LockModifier, LockType, Operator, SelectBuilder, UpdateBuilder } from '@contember/database'\nimport { EventRow, HandledEvent } from './types.js'\nimport { Actions } from '@contember/schema'\nimport { eventsToProcessSpecification, eventsToProcessStateSpecification } from '../model/EventsToProcessSpecification.js'\nimport { notify } from '../utils/notifyChannel.js'\n\nconst ACK_TIMEOUT_MS = 1_000 * 60 * 10 // 10 minutes\nconst DEFAULT_REPEAT_INTERVAL_MS = 5_000 // 5 seconds\nconst DEFAULT_MAX_ATTEMPTS = 10\nconst DEFAULT_BATCH_SIZE = 1\n\nexport class EventsRepository {\n\tpublic async fetchBatch(actions: Actions.Schema, db: Client): Promise<FetchBatchResult> {\n\t\tconst primaryEvent = (await this.fetchInternal(db, 1))[0]\n\t\tif (!primaryEvent) {\n\t\t\treturn { ok: false, backoffMs: await this.fetchBackOff(db) }\n\t\t}\n\t\tconst target = actions.targets[primaryEvent.target]\n\t\tif (!target) {\n\t\t\tawait this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id)\n\t\t\treturn await this.fetchBatch(actions, db)\n\t\t}\n\t\tconst batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1\n\t\tconst batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : []\n\t\treturn { ok: true, events: [primaryEvent, ...batch], target }\n\t}\n\n\tprivate async fetchBackOff(db: Client): Promise<number | undefined> {\n\t\tconst result = await SelectBuilder.create<{ ms: number }>()\n\t\t\t.from('actions_event')\n\t\t\t.match(eventsToProcessStateSpecification)\n\t\t\t.select(it => it.raw('ceil(extract(epoch from visible_at - now()) * 1000)'), 'ms')\n\t\t\t.orderBy('visible_at', 'asc')\n\t\t\t.limit(1)\n\t\t\t.getResult(db)\n\n\t\treturn result.length ? Math.max(0, result[0].ms) : undefined\n\t}\n\n\tprivate async fetchInternal(db: Client, limit: number, targetName?: string): Promise<EventRow[]> {\n\t\tconst visibleAt = new Date()\n\t\tvisibleAt.setTime(visibleAt.getTime() + ACK_TIMEOUT_MS)\n\t\treturn await UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.with('events', qb =>\n\t\t\t\tqb\n\t\t\t\t\t.select(it => it.raw('*'))\n\t\t\t\t\t.from('actions_event')\n\t\t\t\t\t.match(eventsToProcessSpecification)\n\t\t\t\t\t.where(targetName ? { target: targetName } : {})\n\t\t\t\t\t.limit(limit)\n\t\t\t\t\t.lock(LockType.forNoKeyUpdate, LockModifier.skipLocked))\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'processing',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: visibleAt,\n\t\t\t})\n\t\t\t.from(it => it.from('events').where(it => it.columnsEq(['events', 'id'], ['actions_event', 'id'])))\n\t\t\t.returning<EventRow>(new Literal('*'))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async persistProcessed(db: Client, events: HandledEvent[]): Promise<[number, number]> {\n\t\treturn await db.transaction(async trx => {\n\t\t\tawait trx.query(Connection.REPEATABLE_READ)\n\t\t\tconst succeed = events.filter(it => it.result.ok)\n\t\t\tawait this.markSucceed(trx, succeed.map(it => it.row))\n\n\t\t\tfor (const event of events) {\n\t\t\t\tif (!event.result.ok) {\n\t\t\t\t\tawait this.markFailed(trx, event)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn [succeed.length, events.length - succeed.length]\n\t\t})\n\t}\n\n\tpublic async requeue(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: it =>\n\t\t\t\t\tit.raw(\n\t\t\t\t\t\t`\n\t\t\t\t\tCASE \n\t\t\t\t\tWHEN num_retries = 0 THEN \n\t\t\t\t\t\t?::__SCHEMA__.actions_trigger_state \n\t\t\t\t\tELSE \n\t\t\t\t\t?::__SCHEMA__.actions_trigger_state END\n\t\t\t\t`,\n\t\t\t\t\t\t'created',\n\t\t\t\t\t\t'retrying',\n\t\t\t\t\t),\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async markStopped(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'stopped',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markSucceed(db: Client, rows: EventRow[]): Promise<void> {\n\t\tconst now = new Date()\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'succeed',\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tresolved_at: now,\n\t\t\t\tlast_state_change: now,\n\t\t\t})\n\t\t\t.where(it => it.in('id', rows.map(it => it.id)))\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markFailed(db: Client, event: HandledEvent): Promise<void> {\n\t\tconst numRetries = event.row.num_retries + 1\n\t\tconst now = new Date()\n\t\tconst nextAttempt = new Date()\n\t\tnextAttempt.setTime(\n\t\t\tnextAttempt.getTime() + (event.target?.initialRepeatIntervalMs ?? DEFAULT_REPEAT_INTERVAL_MS) * Math.pow(2, event.row.num_retries),\n\t\t)\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tlast_state_change: now,\n\t\t\t\tnum_retries: numRetries,\n\t\t\t\tlog: JSON.stringify([...event.row.log, event.result]) as any,\n\t\t\t\t...(numRetries < (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)\n\t\t\t\t\t? {\n\t\t\t\t\t\tstate: 'retrying',\n\t\t\t\t\t\tvisible_at: nextAttempt,\n\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\tstate: 'failed',\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\t.where({ id: event.row.id })\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markFailedOnUnknownTarget(db: Client, targetName: string, primaryEventId: string) {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.where(it =>\n\t\t\t\tit.or(it =>\n\t\t\t\t\tit\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.in('state', ['retrying', 'created', 'processing'])\n\t\t\t\t\t\t\t\t.compare('visible_at', Operator.lte, 'now')\n\t\t\t\t\t\t\t\t.compare('target', Operator.eq, targetName)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.compare('id', Operator.eq, primaryEventId)\n\t\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t\t.values<EventRow>({\n\t\t\t\t// intentionally not retrying\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tlog: it => it.raw('log || ?::jsonb', { ok: false, errorMessage: `Event target ${targetName} not found, stopping.` }),\n\t\t\t\tstate: 'failed',\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.execute(db)\n\t}\n}\n\nexport type FetchBatchResult =\n\t| { ok: true; events: EventRow[]; target: Actions.AnyTarget }\n\t| { ok: false; backoffMs?: number }\n"],"names":["SelectBuilder","eventsToProcessStateSpecification","UpdateBuilder","eventsToProcessSpecification","LockType","LockModifier","it","Literal","Connection","Operator"],"mappings":"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"}
1
+ {"version":3,"file":"EventsRepository.cjs","sources":["../../../../../packages/engine-actions/src/dispatch/EventsRepository.ts"],"sourcesContent":["import { Client, Connection, Literal, LockModifier, LockType, Operator, SelectBuilder, UpdateBuilder } from '@contember/database'\nimport { EventRow, HandledEvent } from './types.js'\nimport { Actions } from '@contember/schema'\nimport { eventsToProcessSpecification, eventsToProcessStateSpecification } from '../model/EventsToProcessSpecification.js'\nimport { notify } from '../utils/notifyChannel.js'\nimport { Logger } from '@contember/logger'\n\nconst ACK_TIMEOUT_MS = 1_000 * 60 * 10 // 10 minutes\nconst DEFAULT_REPEAT_INTERVAL_MS = 5_000 // 5 seconds\nconst DEFAULT_MAX_ATTEMPTS = 10\nconst DEFAULT_BATCH_SIZE = 1\n\nexport class EventsRepository {\n\tpublic async fetchBatch(actions: Actions.Schema, db: Client, logger: Logger): Promise<FetchBatchResult> {\n\t\t// Events referencing a target that no longer exists in the schema are terminally failed and\n\t\t// skipped over; count them so the caller can report them as terminal failures.\n\t\tlet unknownTargetFailed = 0\n\t\twhile (true) {\n\t\t\tconst primaryEvent = (await this.fetchInternal(db, 1))[0]\n\t\t\tif (!primaryEvent) {\n\t\t\t\treturn { ok: false, backoffMs: await this.fetchBackOff(db), unknownTargetFailed }\n\t\t\t}\n\t\t\tconst target = actions.targets[primaryEvent.target]\n\t\t\tif (!target) {\n\t\t\t\tif (await this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id)) {\n\t\t\t\t\tunknownTargetFailed++\n\t\t\t\t\t// Never retried: the schema has to name the target again before such an event can be delivered.\n\t\t\t\t\tlogger.error('Action event failed permanently, its target is not in the schema', {\n\t\t\t\t\t\teventId: primaryEvent.id,\n\t\t\t\t\t\ttarget: primaryEvent.target,\n\t\t\t\t\t\ttrigger: primaryEvent.trigger,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1\n\t\t\tconst batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : []\n\t\t\treturn { ok: true, events: [primaryEvent, ...batch], target, unknownTargetFailed }\n\t\t}\n\t}\n\n\tprivate async fetchBackOff(db: Client): Promise<number | undefined> {\n\t\tconst result = await SelectBuilder.create<{ ms: number }>()\n\t\t\t.from('actions_event')\n\t\t\t.match(eventsToProcessStateSpecification)\n\t\t\t.select(it => it.raw('ceil(extract(epoch from visible_at - now()) * 1000)'), 'ms')\n\t\t\t.orderBy('visible_at', 'asc')\n\t\t\t.limit(1)\n\t\t\t.getResult(db)\n\n\t\treturn result.length ? Math.max(0, result[0].ms) : undefined\n\t}\n\n\tprivate async fetchInternal(db: Client, limit: number, targetName?: string): Promise<EventRow[]> {\n\t\tconst visibleAt = new Date()\n\t\tvisibleAt.setTime(visibleAt.getTime() + ACK_TIMEOUT_MS)\n\t\treturn await UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.with('events', qb =>\n\t\t\t\tqb\n\t\t\t\t\t.select(it => it.raw('*'))\n\t\t\t\t\t.from('actions_event')\n\t\t\t\t\t.match(eventsToProcessSpecification)\n\t\t\t\t\t.where(targetName ? { target: targetName } : {})\n\t\t\t\t\t.limit(limit)\n\t\t\t\t\t.lock(LockType.forNoKeyUpdate, LockModifier.skipLocked))\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'processing',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: visibleAt,\n\t\t\t})\n\t\t\t.from(it => it.from('events').where(it => it.columnsEq(['events', 'id'], ['actions_event', 'id'])))\n\t\t\t.returning<EventRow>(new Literal('*'))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async persistProcessed(db: Client, events: HandledEvent[], logger: Logger): Promise<PersistProcessedResult> {\n\t\treturn await db.transaction(async trx => {\n\t\t\tawait trx.query(Connection.REPEATABLE_READ)\n\t\t\tconst succeed = events.filter(it => it.result.ok)\n\t\t\tawait this.markSucceed(trx, succeed.map(it => it.row))\n\n\t\t\tlet retried = 0\n\t\t\tlet failed = 0\n\t\t\tfor (const event of events) {\n\t\t\t\tif (!event.result.ok) {\n\t\t\t\t\tconst terminal = await this.markFailed(trx, event)\n\t\t\t\t\tif (terminal) {\n\t\t\t\t\t\tfailed++\n\t\t\t\t\t\t// Nothing else reports a given-up event; the target is named, never described by URL or headers.\n\t\t\t\t\t\tlogger.error('Action event failed permanently, no attempts left', {\n\t\t\t\t\t\t\teventId: event.row.id,\n\t\t\t\t\t\t\ttarget: event.row.target,\n\t\t\t\t\t\t\ttrigger: event.row.trigger,\n\t\t\t\t\t\t\tattempts: event.row.num_retries + 1,\n\t\t\t\t\t\t\tcode: event.result.code,\n\t\t\t\t\t\t\terrorMessage: event.result.errorMessage,\n\t\t\t\t\t\t})\n\t\t\t\t\t} else {\n\t\t\t\t\t\tretried++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { succeeded: succeed.length, retried, failed }\n\t\t})\n\t}\n\n\tpublic async requeue(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: it =>\n\t\t\t\t\tit.raw(\n\t\t\t\t\t\t`\n\t\t\t\t\tCASE \n\t\t\t\t\tWHEN num_retries = 0 THEN \n\t\t\t\t\t\t?::__SCHEMA__.actions_trigger_state \n\t\t\t\t\tELSE \n\t\t\t\t\t?::__SCHEMA__.actions_trigger_state END\n\t\t\t\t`,\n\t\t\t\t\t\t'created',\n\t\t\t\t\t\t'retrying',\n\t\t\t\t\t),\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async markStopped(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'stopped',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markSucceed(db: Client, rows: EventRow[]): Promise<void> {\n\t\tconst now = new Date()\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'succeed',\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tresolved_at: now,\n\t\t\t\tlast_state_change: now,\n\t\t\t})\n\t\t\t.where(it => it.in('id', rows.map(it => it.id)))\n\t\t\t.execute(db)\n\t}\n\n\t/** Returns `true` when the event reached a terminal `failed` state, `false` when it will be retried. */\n\tprivate async markFailed(db: Client, event: HandledEvent): Promise<boolean> {\n\t\tconst numRetries = event.row.num_retries + 1\n\t\tconst now = new Date()\n\t\tconst nextAttempt = new Date()\n\t\tnextAttempt.setTime(\n\t\t\tnextAttempt.getTime() + (event.target?.initialRepeatIntervalMs ?? DEFAULT_REPEAT_INTERVAL_MS) * Math.pow(2, event.row.num_retries),\n\t\t)\n\t\tconst terminal = numRetries >= (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tlast_state_change: now,\n\t\t\t\tnum_retries: numRetries,\n\t\t\t\tlog: JSON.stringify([...event.row.log, event.result]) as any,\n\t\t\t\t...(terminal\n\t\t\t\t\t? {\n\t\t\t\t\t\tstate: 'failed',\n\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\tstate: 'retrying',\n\t\t\t\t\t\tvisible_at: nextAttempt,\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\t.where({ id: event.row.id })\n\t\t\t.execute(db)\n\t\treturn terminal\n\t}\n\n\tprivate async markFailedOnUnknownTarget(db: Client, targetName: string, primaryEventId: string): Promise<boolean> {\n\t\tconst affectedRows = await UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.where(it =>\n\t\t\t\tit.or(it =>\n\t\t\t\t\tit\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.in('state', ['processing'])\n\t\t\t\t\t\t\t\t.compare('target', Operator.eq, targetName)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.compare('id', Operator.eq, primaryEventId)\n\t\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t\t.values<EventRow>({\n\t\t\t\t// intentionally not retrying\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tlog: it => it.raw('log || ?::jsonb', { ok: false, errorMessage: `Event target ${targetName} not found, stopping.` }),\n\t\t\t\tstate: 'failed',\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.execute(db)\n\t\treturn affectedRows > 0\n\t}\n}\n\nexport type FetchBatchResult =\n\t| { ok: true; events: EventRow[]; target: Actions.AnyTarget; unknownTargetFailed: number }\n\t| { ok: false; backoffMs?: number; unknownTargetFailed: number }\n\nexport type PersistProcessedResult = {\n\t/** Events delivered successfully (terminal). */\n\tsucceeded: number\n\t/** Events whose delivery attempt failed and that will be retried later. */\n\tretried: number\n\t/** Events whose delivery attempt failed and that reached a terminal `failed` state. */\n\tfailed: number\n}\n"],"names":["SelectBuilder","eventsToProcessStateSpecification","UpdateBuilder","eventsToProcessSpecification","LockType","LockModifier","it","Literal","Connection","Operator"],"mappings":"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"}
@@ -5,19 +5,29 @@ const DEFAULT_REPEAT_INTERVAL_MS = 5e3;
5
5
  const DEFAULT_MAX_ATTEMPTS = 10;
6
6
  const DEFAULT_BATCH_SIZE = 1;
7
7
  class EventsRepository {
8
- async fetchBatch(actions, db) {
9
- const primaryEvent = (await this.fetchInternal(db, 1))[0];
10
- if (!primaryEvent) {
11
- return { ok: false, backoffMs: await this.fetchBackOff(db) };
12
- }
13
- const target = actions.targets[primaryEvent.target];
14
- if (!target) {
15
- await this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id);
16
- return await this.fetchBatch(actions, db);
8
+ async fetchBatch(actions, db, logger) {
9
+ let unknownTargetFailed = 0;
10
+ while (true) {
11
+ const primaryEvent = (await this.fetchInternal(db, 1))[0];
12
+ if (!primaryEvent) {
13
+ return { ok: false, backoffMs: await this.fetchBackOff(db), unknownTargetFailed };
14
+ }
15
+ const target = actions.targets[primaryEvent.target];
16
+ if (!target) {
17
+ if (await this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id)) {
18
+ unknownTargetFailed++;
19
+ logger.error("Action event failed permanently, its target is not in the schema", {
20
+ eventId: primaryEvent.id,
21
+ target: primaryEvent.target,
22
+ trigger: primaryEvent.trigger
23
+ });
24
+ }
25
+ continue;
26
+ }
27
+ const batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1;
28
+ const batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : [];
29
+ return { ok: true, events: [primaryEvent, ...batch], target, unknownTargetFailed };
17
30
  }
18
- const batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1;
19
- const batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : [];
20
- return { ok: true, events: [primaryEvent, ...batch], target };
21
31
  }
22
32
  async fetchBackOff(db) {
23
33
  const result = await SelectBuilder.create().from("actions_event").match(eventsToProcessStateSpecification).select((it) => it.raw("ceil(extract(epoch from visible_at - now()) * 1000)"), "ms").orderBy("visible_at", "asc").limit(1).getResult(db);
@@ -32,17 +42,32 @@ class EventsRepository {
32
42
  visible_at: visibleAt
33
43
  }).from((it) => it.from("events").where((it2) => it2.columnsEq(["events", "id"], ["actions_event", "id"]))).returning(new Literal("*")).execute(db);
34
44
  }
35
- async persistProcessed(db, events) {
45
+ async persistProcessed(db, events, logger) {
36
46
  return await db.transaction(async (trx) => {
37
47
  await trx.query(Connection.REPEATABLE_READ);
38
48
  const succeed = events.filter((it) => it.result.ok);
39
49
  await this.markSucceed(trx, succeed.map((it) => it.row));
50
+ let retried = 0;
51
+ let failed = 0;
40
52
  for (const event of events) {
41
53
  if (!event.result.ok) {
42
- await this.markFailed(trx, event);
54
+ const terminal = await this.markFailed(trx, event);
55
+ if (terminal) {
56
+ failed++;
57
+ logger.error("Action event failed permanently, no attempts left", {
58
+ eventId: event.row.id,
59
+ target: event.row.target,
60
+ trigger: event.row.trigger,
61
+ attempts: event.row.num_retries + 1,
62
+ code: event.result.code,
63
+ errorMessage: event.result.errorMessage
64
+ });
65
+ } else {
66
+ retried++;
67
+ }
43
68
  }
44
69
  }
45
- return [succeed.length, events.length - succeed.length];
70
+ return { succeeded: succeed.length, retried, failed };
46
71
  });
47
72
  }
48
73
  async requeue(db, events) {
@@ -78,6 +103,7 @@ class EventsRepository {
78
103
  last_state_change: now
79
104
  }).where((it) => it.in("id", rows.map((it2) => it2.id))).execute(db);
80
105
  }
106
+ /** Returns `true` when the event reached a terminal `failed` state, `false` when it will be retried. */
81
107
  async markFailed(db, event) {
82
108
  const numRetries = event.row.num_retries + 1;
83
109
  const now = /* @__PURE__ */ new Date();
@@ -85,23 +111,25 @@ class EventsRepository {
85
111
  nextAttempt.setTime(
86
112
  nextAttempt.getTime() + (event.target?.initialRepeatIntervalMs ?? DEFAULT_REPEAT_INTERVAL_MS) * Math.pow(2, event.row.num_retries)
87
113
  );
114
+ const terminal = numRetries >= (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
88
115
  await UpdateBuilder$1.create().table("actions_event").values({
89
116
  last_state_change: now,
90
117
  num_retries: numRetries,
91
118
  log: JSON.stringify([...event.row.log, event.result]),
92
- ...numRetries < (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS) ? {
119
+ ...terminal ? {
120
+ state: "failed"
121
+ } : {
93
122
  state: "retrying",
94
123
  visible_at: nextAttempt
95
- } : {
96
- state: "failed"
97
124
  }
98
125
  }).where({ id: event.row.id }).execute(db);
126
+ return terminal;
99
127
  }
100
128
  async markFailedOnUnknownTarget(db, targetName, primaryEventId) {
101
- await UpdateBuilder$1.create().table("actions_event").where(
129
+ const affectedRows = await UpdateBuilder$1.create().table("actions_event").where(
102
130
  (it) => it.or(
103
131
  (it2) => it2.and(
104
- (it3) => it3.in("state", ["retrying", "created", "processing"]).compare("visible_at", Operator.lte, "now").compare("target", Operator.eq, targetName)
132
+ (it3) => it3.in("state", ["processing"]).compare("target", Operator.eq, targetName)
105
133
  ).and(
106
134
  (it3) => it3.compare("id", Operator.eq, primaryEventId)
107
135
  )
@@ -114,6 +142,7 @@ class EventsRepository {
114
142
  state: "failed",
115
143
  resolved_at: /* @__PURE__ */ new Date()
116
144
  }).execute(db);
145
+ return affectedRows > 0;
117
146
  }
118
147
  }
119
148
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"EventsRepository.js","sources":["../../../../../packages/engine-actions/src/dispatch/EventsRepository.ts"],"sourcesContent":["import { Client, Connection, Literal, LockModifier, LockType, Operator, SelectBuilder, UpdateBuilder } from '@contember/database'\nimport { EventRow, HandledEvent } from './types.js'\nimport { Actions } from '@contember/schema'\nimport { eventsToProcessSpecification, eventsToProcessStateSpecification } from '../model/EventsToProcessSpecification.js'\nimport { notify } from '../utils/notifyChannel.js'\n\nconst ACK_TIMEOUT_MS = 1_000 * 60 * 10 // 10 minutes\nconst DEFAULT_REPEAT_INTERVAL_MS = 5_000 // 5 seconds\nconst DEFAULT_MAX_ATTEMPTS = 10\nconst DEFAULT_BATCH_SIZE = 1\n\nexport class EventsRepository {\n\tpublic async fetchBatch(actions: Actions.Schema, db: Client): Promise<FetchBatchResult> {\n\t\tconst primaryEvent = (await this.fetchInternal(db, 1))[0]\n\t\tif (!primaryEvent) {\n\t\t\treturn { ok: false, backoffMs: await this.fetchBackOff(db) }\n\t\t}\n\t\tconst target = actions.targets[primaryEvent.target]\n\t\tif (!target) {\n\t\t\tawait this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id)\n\t\t\treturn await this.fetchBatch(actions, db)\n\t\t}\n\t\tconst batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1\n\t\tconst batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : []\n\t\treturn { ok: true, events: [primaryEvent, ...batch], target }\n\t}\n\n\tprivate async fetchBackOff(db: Client): Promise<number | undefined> {\n\t\tconst result = await SelectBuilder.create<{ ms: number }>()\n\t\t\t.from('actions_event')\n\t\t\t.match(eventsToProcessStateSpecification)\n\t\t\t.select(it => it.raw('ceil(extract(epoch from visible_at - now()) * 1000)'), 'ms')\n\t\t\t.orderBy('visible_at', 'asc')\n\t\t\t.limit(1)\n\t\t\t.getResult(db)\n\n\t\treturn result.length ? Math.max(0, result[0].ms) : undefined\n\t}\n\n\tprivate async fetchInternal(db: Client, limit: number, targetName?: string): Promise<EventRow[]> {\n\t\tconst visibleAt = new Date()\n\t\tvisibleAt.setTime(visibleAt.getTime() + ACK_TIMEOUT_MS)\n\t\treturn await UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.with('events', qb =>\n\t\t\t\tqb\n\t\t\t\t\t.select(it => it.raw('*'))\n\t\t\t\t\t.from('actions_event')\n\t\t\t\t\t.match(eventsToProcessSpecification)\n\t\t\t\t\t.where(targetName ? { target: targetName } : {})\n\t\t\t\t\t.limit(limit)\n\t\t\t\t\t.lock(LockType.forNoKeyUpdate, LockModifier.skipLocked))\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'processing',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: visibleAt,\n\t\t\t})\n\t\t\t.from(it => it.from('events').where(it => it.columnsEq(['events', 'id'], ['actions_event', 'id'])))\n\t\t\t.returning<EventRow>(new Literal('*'))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async persistProcessed(db: Client, events: HandledEvent[]): Promise<[number, number]> {\n\t\treturn await db.transaction(async trx => {\n\t\t\tawait trx.query(Connection.REPEATABLE_READ)\n\t\t\tconst succeed = events.filter(it => it.result.ok)\n\t\t\tawait this.markSucceed(trx, succeed.map(it => it.row))\n\n\t\t\tfor (const event of events) {\n\t\t\t\tif (!event.result.ok) {\n\t\t\t\t\tawait this.markFailed(trx, event)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn [succeed.length, events.length - succeed.length]\n\t\t})\n\t}\n\n\tpublic async requeue(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: it =>\n\t\t\t\t\tit.raw(\n\t\t\t\t\t\t`\n\t\t\t\t\tCASE \n\t\t\t\t\tWHEN num_retries = 0 THEN \n\t\t\t\t\t\t?::__SCHEMA__.actions_trigger_state \n\t\t\t\t\tELSE \n\t\t\t\t\t?::__SCHEMA__.actions_trigger_state END\n\t\t\t\t`,\n\t\t\t\t\t\t'created',\n\t\t\t\t\t\t'retrying',\n\t\t\t\t\t),\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async markStopped(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'stopped',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markSucceed(db: Client, rows: EventRow[]): Promise<void> {\n\t\tconst now = new Date()\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'succeed',\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tresolved_at: now,\n\t\t\t\tlast_state_change: now,\n\t\t\t})\n\t\t\t.where(it => it.in('id', rows.map(it => it.id)))\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markFailed(db: Client, event: HandledEvent): Promise<void> {\n\t\tconst numRetries = event.row.num_retries + 1\n\t\tconst now = new Date()\n\t\tconst nextAttempt = new Date()\n\t\tnextAttempt.setTime(\n\t\t\tnextAttempt.getTime() + (event.target?.initialRepeatIntervalMs ?? DEFAULT_REPEAT_INTERVAL_MS) * Math.pow(2, event.row.num_retries),\n\t\t)\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tlast_state_change: now,\n\t\t\t\tnum_retries: numRetries,\n\t\t\t\tlog: JSON.stringify([...event.row.log, event.result]) as any,\n\t\t\t\t...(numRetries < (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)\n\t\t\t\t\t? {\n\t\t\t\t\t\tstate: 'retrying',\n\t\t\t\t\t\tvisible_at: nextAttempt,\n\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\tstate: 'failed',\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\t.where({ id: event.row.id })\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markFailedOnUnknownTarget(db: Client, targetName: string, primaryEventId: string) {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.where(it =>\n\t\t\t\tit.or(it =>\n\t\t\t\t\tit\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.in('state', ['retrying', 'created', 'processing'])\n\t\t\t\t\t\t\t\t.compare('visible_at', Operator.lte, 'now')\n\t\t\t\t\t\t\t\t.compare('target', Operator.eq, targetName)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.compare('id', Operator.eq, primaryEventId)\n\t\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t\t.values<EventRow>({\n\t\t\t\t// intentionally not retrying\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tlog: it => it.raw('log || ?::jsonb', { ok: false, errorMessage: `Event target ${targetName} not found, stopping.` }),\n\t\t\t\tstate: 'failed',\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.execute(db)\n\t}\n}\n\nexport type FetchBatchResult =\n\t| { ok: true; events: EventRow[]; target: Actions.AnyTarget }\n\t| { ok: false; backoffMs?: number }\n"],"names":["UpdateBuilder","it"],"mappings":"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"}
1
+ {"version":3,"file":"EventsRepository.js","sources":["../../../../../packages/engine-actions/src/dispatch/EventsRepository.ts"],"sourcesContent":["import { Client, Connection, Literal, LockModifier, LockType, Operator, SelectBuilder, UpdateBuilder } from '@contember/database'\nimport { EventRow, HandledEvent } from './types.js'\nimport { Actions } from '@contember/schema'\nimport { eventsToProcessSpecification, eventsToProcessStateSpecification } from '../model/EventsToProcessSpecification.js'\nimport { notify } from '../utils/notifyChannel.js'\nimport { Logger } from '@contember/logger'\n\nconst ACK_TIMEOUT_MS = 1_000 * 60 * 10 // 10 minutes\nconst DEFAULT_REPEAT_INTERVAL_MS = 5_000 // 5 seconds\nconst DEFAULT_MAX_ATTEMPTS = 10\nconst DEFAULT_BATCH_SIZE = 1\n\nexport class EventsRepository {\n\tpublic async fetchBatch(actions: Actions.Schema, db: Client, logger: Logger): Promise<FetchBatchResult> {\n\t\t// Events referencing a target that no longer exists in the schema are terminally failed and\n\t\t// skipped over; count them so the caller can report them as terminal failures.\n\t\tlet unknownTargetFailed = 0\n\t\twhile (true) {\n\t\t\tconst primaryEvent = (await this.fetchInternal(db, 1))[0]\n\t\t\tif (!primaryEvent) {\n\t\t\t\treturn { ok: false, backoffMs: await this.fetchBackOff(db), unknownTargetFailed }\n\t\t\t}\n\t\t\tconst target = actions.targets[primaryEvent.target]\n\t\t\tif (!target) {\n\t\t\t\tif (await this.markFailedOnUnknownTarget(db, primaryEvent.target, primaryEvent.id)) {\n\t\t\t\t\tunknownTargetFailed++\n\t\t\t\t\t// Never retried: the schema has to name the target again before such an event can be delivered.\n\t\t\t\t\tlogger.error('Action event failed permanently, its target is not in the schema', {\n\t\t\t\t\t\teventId: primaryEvent.id,\n\t\t\t\t\t\ttarget: primaryEvent.target,\n\t\t\t\t\t\ttrigger: primaryEvent.trigger,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst batchSize = (target.batchSize ?? DEFAULT_BATCH_SIZE) - 1\n\t\t\tconst batch = batchSize > 0 ? await this.fetchInternal(db, batchSize) : []\n\t\t\treturn { ok: true, events: [primaryEvent, ...batch], target, unknownTargetFailed }\n\t\t}\n\t}\n\n\tprivate async fetchBackOff(db: Client): Promise<number | undefined> {\n\t\tconst result = await SelectBuilder.create<{ ms: number }>()\n\t\t\t.from('actions_event')\n\t\t\t.match(eventsToProcessStateSpecification)\n\t\t\t.select(it => it.raw('ceil(extract(epoch from visible_at - now()) * 1000)'), 'ms')\n\t\t\t.orderBy('visible_at', 'asc')\n\t\t\t.limit(1)\n\t\t\t.getResult(db)\n\n\t\treturn result.length ? Math.max(0, result[0].ms) : undefined\n\t}\n\n\tprivate async fetchInternal(db: Client, limit: number, targetName?: string): Promise<EventRow[]> {\n\t\tconst visibleAt = new Date()\n\t\tvisibleAt.setTime(visibleAt.getTime() + ACK_TIMEOUT_MS)\n\t\treturn await UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.with('events', qb =>\n\t\t\t\tqb\n\t\t\t\t\t.select(it => it.raw('*'))\n\t\t\t\t\t.from('actions_event')\n\t\t\t\t\t.match(eventsToProcessSpecification)\n\t\t\t\t\t.where(targetName ? { target: targetName } : {})\n\t\t\t\t\t.limit(limit)\n\t\t\t\t\t.lock(LockType.forNoKeyUpdate, LockModifier.skipLocked))\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'processing',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: visibleAt,\n\t\t\t})\n\t\t\t.from(it => it.from('events').where(it => it.columnsEq(['events', 'id'], ['actions_event', 'id'])))\n\t\t\t.returning<EventRow>(new Literal('*'))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async persistProcessed(db: Client, events: HandledEvent[], logger: Logger): Promise<PersistProcessedResult> {\n\t\treturn await db.transaction(async trx => {\n\t\t\tawait trx.query(Connection.REPEATABLE_READ)\n\t\t\tconst succeed = events.filter(it => it.result.ok)\n\t\t\tawait this.markSucceed(trx, succeed.map(it => it.row))\n\n\t\t\tlet retried = 0\n\t\t\tlet failed = 0\n\t\t\tfor (const event of events) {\n\t\t\t\tif (!event.result.ok) {\n\t\t\t\t\tconst terminal = await this.markFailed(trx, event)\n\t\t\t\t\tif (terminal) {\n\t\t\t\t\t\tfailed++\n\t\t\t\t\t\t// Nothing else reports a given-up event; the target is named, never described by URL or headers.\n\t\t\t\t\t\tlogger.error('Action event failed permanently, no attempts left', {\n\t\t\t\t\t\t\teventId: event.row.id,\n\t\t\t\t\t\t\ttarget: event.row.target,\n\t\t\t\t\t\t\ttrigger: event.row.trigger,\n\t\t\t\t\t\t\tattempts: event.row.num_retries + 1,\n\t\t\t\t\t\t\tcode: event.result.code,\n\t\t\t\t\t\t\terrorMessage: event.result.errorMessage,\n\t\t\t\t\t\t})\n\t\t\t\t\t} else {\n\t\t\t\t\t\tretried++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { succeeded: succeed.length, retried, failed }\n\t\t})\n\t}\n\n\tpublic async requeue(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: it =>\n\t\t\t\t\tit.raw(\n\t\t\t\t\t\t`\n\t\t\t\t\tCASE \n\t\t\t\t\tWHEN num_retries = 0 THEN \n\t\t\t\t\t\t?::__SCHEMA__.actions_trigger_state \n\t\t\t\t\tELSE \n\t\t\t\t\t?::__SCHEMA__.actions_trigger_state END\n\t\t\t\t`,\n\t\t\t\t\t\t'created',\n\t\t\t\t\t\t'retrying',\n\t\t\t\t\t),\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tvisible_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tpublic async markStopped(db: Client, events: string[]): Promise<void> {\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'stopped',\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.where(it => it.in('id', events))\n\t\t\t.execute(db)\n\t}\n\n\tprivate async markSucceed(db: Client, rows: EventRow[]): Promise<void> {\n\t\tconst now = new Date()\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tstate: 'succeed',\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tresolved_at: now,\n\t\t\t\tlast_state_change: now,\n\t\t\t})\n\t\t\t.where(it => it.in('id', rows.map(it => it.id)))\n\t\t\t.execute(db)\n\t}\n\n\t/** Returns `true` when the event reached a terminal `failed` state, `false` when it will be retried. */\n\tprivate async markFailed(db: Client, event: HandledEvent): Promise<boolean> {\n\t\tconst numRetries = event.row.num_retries + 1\n\t\tconst now = new Date()\n\t\tconst nextAttempt = new Date()\n\t\tnextAttempt.setTime(\n\t\t\tnextAttempt.getTime() + (event.target?.initialRepeatIntervalMs ?? DEFAULT_REPEAT_INTERVAL_MS) * Math.pow(2, event.row.num_retries),\n\t\t)\n\t\tconst terminal = numRetries >= (event.target?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)\n\t\tawait UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.values<EventRow>({\n\t\t\t\tlast_state_change: now,\n\t\t\t\tnum_retries: numRetries,\n\t\t\t\tlog: JSON.stringify([...event.row.log, event.result]) as any,\n\t\t\t\t...(terminal\n\t\t\t\t\t? {\n\t\t\t\t\t\tstate: 'failed',\n\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\tstate: 'retrying',\n\t\t\t\t\t\tvisible_at: nextAttempt,\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\t.where({ id: event.row.id })\n\t\t\t.execute(db)\n\t\treturn terminal\n\t}\n\n\tprivate async markFailedOnUnknownTarget(db: Client, targetName: string, primaryEventId: string): Promise<boolean> {\n\t\tconst affectedRows = await UpdateBuilder.create()\n\t\t\t.table('actions_event')\n\t\t\t.where(it =>\n\t\t\t\tit.or(it =>\n\t\t\t\t\tit\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.in('state', ['processing'])\n\t\t\t\t\t\t\t\t.compare('target', Operator.eq, targetName)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.and(it =>\n\t\t\t\t\t\t\tit\n\t\t\t\t\t\t\t\t.compare('id', Operator.eq, primaryEventId)\n\t\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t\t.values<EventRow>({\n\t\t\t\t// intentionally not retrying\n\t\t\t\tlast_state_change: new Date(),\n\t\t\t\tnum_retries: it => it.raw('num_retries + 1'),\n\t\t\t\tlog: it => it.raw('log || ?::jsonb', { ok: false, errorMessage: `Event target ${targetName} not found, stopping.` }),\n\t\t\t\tstate: 'failed',\n\t\t\t\tresolved_at: new Date(),\n\t\t\t})\n\t\t\t.execute(db)\n\t\treturn affectedRows > 0\n\t}\n}\n\nexport type FetchBatchResult =\n\t| { ok: true; events: EventRow[]; target: Actions.AnyTarget; unknownTargetFailed: number }\n\t| { ok: false; backoffMs?: number; unknownTargetFailed: number }\n\nexport type PersistProcessedResult = {\n\t/** Events delivered successfully (terminal). */\n\tsucceeded: number\n\t/** Events whose delivery attempt failed and that will be retried later. */\n\tretried: number\n\t/** Events whose delivery attempt failed and that reached a terminal `failed` state. */\n\tfailed: number\n}\n"],"names":["UpdateBuilder","it"],"mappings":"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"}