@contember/engine-actions 2.2.0-alpha.3 → 2.2.0-alpha.5
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.
- package/CLAUDE.md +58 -0
- package/dist/development/index.cjs +19 -7
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +19 -7
- package/dist/development/index.js.map +1 -1
- package/dist/development/src/ActionsExecutionContainerHookFactory.cjs +5 -2
- package/dist/development/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
- package/dist/development/src/ActionsExecutionContainerHookFactory.js +5 -2
- package/dist/development/src/ActionsExecutionContainerHookFactory.js.map +1 -1
- package/dist/development/src/ActionsMetrics.cjs +98 -0
- package/dist/development/src/ActionsMetrics.cjs.map +1 -0
- package/dist/development/src/ActionsMetrics.js +98 -0
- package/dist/development/src/ActionsMetrics.js.map +1 -0
- package/dist/development/src/dispatch/EventDispatcher.cjs +27 -9
- package/dist/development/src/dispatch/EventDispatcher.cjs.map +1 -1
- package/dist/development/src/dispatch/EventDispatcher.js +27 -9
- package/dist/development/src/dispatch/EventDispatcher.js.map +1 -1
- package/dist/development/src/dispatch/EventsRepository.cjs +49 -20
- package/dist/development/src/dispatch/EventsRepository.cjs.map +1 -1
- package/dist/development/src/dispatch/EventsRepository.js +49 -20
- package/dist/development/src/dispatch/EventsRepository.js.map +1 -1
- package/dist/development/src/dispatch/ProjectDispatcher.cjs +21 -13
- package/dist/development/src/dispatch/ProjectDispatcher.cjs.map +1 -1
- package/dist/development/src/dispatch/ProjectDispatcher.js +21 -13
- package/dist/development/src/dispatch/ProjectDispatcher.js.map +1 -1
- package/dist/development/src/dispatch/WebhookTargetHandler.cjs +29 -2
- package/dist/development/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
- package/dist/development/src/dispatch/WebhookTargetHandler.js +29 -2
- package/dist/development/src/dispatch/WebhookTargetHandler.js.map +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.cjs +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.js +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.js.map +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
- package/dist/development/src/graphql/schema/actions.graphql.cjs +20 -8
- package/dist/development/src/graphql/schema/actions.graphql.cjs.map +1 -1
- package/dist/development/src/graphql/schema/actions.graphql.js +20 -8
- package/dist/development/src/graphql/schema/actions.graphql.js.map +1 -1
- package/dist/development/src/model/VariablesManager.cjs +54 -5
- package/dist/development/src/model/VariablesManager.cjs.map +1 -1
- package/dist/development/src/model/VariablesManager.js +54 -5
- package/dist/development/src/model/VariablesManager.js.map +1 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.cjs +3 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.js +3 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.js.map +1 -1
- package/dist/production/index.cjs +19 -7
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +19 -7
- package/dist/production/index.js.map +1 -1
- package/dist/production/src/ActionsExecutionContainerHookFactory.cjs +5 -2
- package/dist/production/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
- package/dist/production/src/ActionsExecutionContainerHookFactory.js +5 -2
- package/dist/production/src/ActionsExecutionContainerHookFactory.js.map +1 -1
- package/dist/production/src/ActionsMetrics.cjs +98 -0
- package/dist/production/src/ActionsMetrics.cjs.map +1 -0
- package/dist/production/src/ActionsMetrics.js +98 -0
- package/dist/production/src/ActionsMetrics.js.map +1 -0
- package/dist/production/src/dispatch/EventDispatcher.cjs +27 -9
- package/dist/production/src/dispatch/EventDispatcher.cjs.map +1 -1
- package/dist/production/src/dispatch/EventDispatcher.js +27 -9
- package/dist/production/src/dispatch/EventDispatcher.js.map +1 -1
- package/dist/production/src/dispatch/EventsRepository.cjs +49 -20
- package/dist/production/src/dispatch/EventsRepository.cjs.map +1 -1
- package/dist/production/src/dispatch/EventsRepository.js +49 -20
- package/dist/production/src/dispatch/EventsRepository.js.map +1 -1
- package/dist/production/src/dispatch/ProjectDispatcher.cjs +21 -13
- package/dist/production/src/dispatch/ProjectDispatcher.cjs.map +1 -1
- package/dist/production/src/dispatch/ProjectDispatcher.js +21 -13
- package/dist/production/src/dispatch/ProjectDispatcher.js.map +1 -1
- package/dist/production/src/dispatch/WebhookTargetHandler.cjs +29 -2
- package/dist/production/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
- package/dist/production/src/dispatch/WebhookTargetHandler.js +29 -2
- package/dist/production/src/dispatch/WebhookTargetHandler.js.map +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.cjs +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.js +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.js.map +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
- package/dist/production/src/graphql/schema/actions.graphql.cjs +20 -8
- package/dist/production/src/graphql/schema/actions.graphql.cjs.map +1 -1
- package/dist/production/src/graphql/schema/actions.graphql.js +20 -8
- package/dist/production/src/graphql/schema/actions.graphql.js.map +1 -1
- package/dist/production/src/model/VariablesManager.cjs +54 -5
- package/dist/production/src/model/VariablesManager.cjs.map +1 -1
- package/dist/production/src/model/VariablesManager.js +54 -5
- package/dist/production/src/model/VariablesManager.js.map +1 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.cjs +3 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.js +3 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.js.map +1 -1
- package/dist/tests/cases/unit/actionsMetrics.test.d.ts +2 -0
- package/dist/tests/cases/unit/actionsMetrics.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/eventsRepository.test.d.ts +2 -0
- package/dist/tests/cases/unit/eventsRepository.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/variablesManager.test.d.ts +2 -0
- package/dist/tests/cases/unit/variablesManager.test.d.ts.map +1 -0
- package/dist/tests/src/event.d.ts +4 -0
- package/dist/tests/src/event.d.ts.map +1 -0
- package/dist/tests/src/gql.d.ts.map +1 -1
- package/dist/tests/src/uuid.d.ts.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ActionsExecutionContainerHookFactory.d.ts +3 -1
- package/dist/types/ActionsExecutionContainerHookFactory.d.ts.map +1 -1
- package/dist/types/ActionsMetrics.d.ts +59 -0
- package/dist/types/ActionsMetrics.d.ts.map +1 -0
- package/dist/types/ListenerStoreProvider.d.ts.map +1 -1
- package/dist/types/authorization/ActionsPermissionsFactory.d.ts.map +1 -1
- package/dist/types/dispatch/DispatchWorkerSupervisor.d.ts.map +1 -1
- package/dist/types/dispatch/EventDispatcher.d.ts +12 -3
- package/dist/types/dispatch/EventDispatcher.d.ts.map +1 -1
- package/dist/types/dispatch/EventsRepository.d.ts +14 -2
- package/dist/types/dispatch/EventsRepository.d.ts.map +1 -1
- package/dist/types/dispatch/LazyDispatchWorker.d.ts.map +1 -1
- package/dist/types/dispatch/ProjectDispatcher.d.ts +5 -2
- package/dist/types/dispatch/ProjectDispatcher.d.ts.map +1 -1
- package/dist/types/dispatch/TargetHandlerResolver.d.ts.map +1 -1
- package/dist/types/dispatch/WebhookFetcher.d.ts.map +1 -1
- package/dist/types/dispatch/WebhookTargetHandler.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsApiMiddlewareFactory.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsContextResolver.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsGraphQLHandlerFactory.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsWebsocketControllerFactory.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/ResolversFactory.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/ProcessBatchMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/RetryEventMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/SetVariablesMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/StopEventMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/query/EventsQueryResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts +1 -4
- package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts.map +1 -1
- package/dist/types/graphql/schema/actions.graphql.d.ts.map +1 -1
- package/dist/types/graphql/schema/index.d.ts +27 -17
- package/dist/types/graphql/schema/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model/EventByIdQuery.d.ts.map +1 -1
- package/dist/types/model/EventsInProcessingQuery.d.ts.map +1 -1
- package/dist/types/model/EventsToProcessQuery.d.ts.map +1 -1
- package/dist/types/model/FailedEventsQuery.d.ts.map +1 -1
- package/dist/types/model/SetVariableCommand.d.ts.map +1 -1
- package/dist/types/model/VariablesManager.d.ts +16 -3
- package/dist/types/model/VariablesManager.d.ts.map +1 -1
- package/dist/types/model/VariablesQuery.d.ts.map +1 -1
- package/dist/types/triggers/TriggerHandler.d.ts.map +1 -1
- package/dist/types/triggers/TriggerIndirectChangesFetcher.d.ts.map +1 -1
- package/dist/types/triggers/TriggerListenersBuilder.d.ts.map +1 -1
- package/dist/types/triggers/TriggerListenersFactory.d.ts.map +1 -1
- package/dist/types/triggers/TriggerListenersStore.d.ts.map +1 -1
- package/dist/types/triggers/TriggerPayloadBuilder.d.ts.map +1 -1
- package/dist/types/triggers/TriggerPayloadManager.d.ts.map +1 -1
- package/dist/types/triggers/TriggerPayloadPersister.d.ts +3 -1
- package/dist/types/triggers/TriggerPayloadPersister.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/utils/map.d.ts.map +1 -1
- package/dist/types/utils/notifyChannel.d.ts.map +1 -1
- package/graphql.codegen.yml +1 -1
- package/package.json +18 -16
- package/src/ActionsExecutionContainerHookFactory.ts +6 -0
- package/src/ActionsMetrics.ts +135 -0
- package/src/dispatch/EventDispatcher.ts +36 -11
- package/src/dispatch/EventsRepository.ts +68 -26
- package/src/dispatch/ProjectDispatcher.ts +27 -11
- package/src/dispatch/WebhookTargetHandler.ts +36 -1
- package/src/graphql/resolvers/ResolversFactory.ts +1 -1
- package/src/graphql/resolvers/mutation/SetVariablesMutationResolver.ts +1 -1
- package/src/graphql/resolvers/query/EventsQueryResolver.ts +1 -1
- package/src/graphql/resolvers/query/VariablesQueryResolver.ts +1 -4
- package/src/graphql/schema/actions.graphql.ts +20 -8
- package/src/graphql/schema/index.ts +30 -17
- package/src/index.ts +27 -5
- package/src/model/VariablesManager.ts +66 -6
- package/src/triggers/TriggerPayloadPersister.ts +6 -0
- package/tests/cases/unit/actionsMetrics.test.ts +76 -0
- package/tests/cases/unit/eventsRepository.test.ts +136 -0
- package/tests/cases/unit/variablesManager.test.ts +154 -0
- package/tests/cases/unit/webhookHandler.test.ts +80 -24
- package/tests/src/event.ts +27 -0
|
@@ -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 {
|
|
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
|
|
29
|
-
batchLogger.debug("Processing done", { succeed, failed });
|
|
30
|
-
return {
|
|
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 {
|
|
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\
|
|
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 {
|
|
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
|
|
27
|
-
batchLogger.debug("Processing done", { succeed, failed });
|
|
28
|
-
return {
|
|
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 {
|
|
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\
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
...
|
|
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", ["
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
...
|
|
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", ["
|
|
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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"}
|
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const Database = require("@contember/database");
|
|
4
4
|
const notifyChannel = require("../utils/notifyChannel.cjs");
|
|
5
|
+
const MAX_IDLE_SLEEP_MS = 3e4;
|
|
5
6
|
class ProjectDispatcherFactory {
|
|
6
|
-
constructor(dispatcher) {
|
|
7
|
+
constructor(dispatcher, metrics) {
|
|
7
8
|
this.dispatcher = dispatcher;
|
|
9
|
+
this.metrics = metrics;
|
|
8
10
|
}
|
|
9
11
|
create({ db, contentSchemaResolver, projectSlug }) {
|
|
10
|
-
return new ProjectDispatcher(this.dispatcher, db, contentSchemaResolver, projectSlug);
|
|
12
|
+
return new ProjectDispatcher(this.dispatcher, db, contentSchemaResolver, projectSlug, this.metrics.forProject(projectSlug));
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
class ProjectDispatcher {
|
|
14
|
-
constructor(dispatcher, db, contentSchemaResolver, projectSlug) {
|
|
16
|
+
constructor(dispatcher, db, contentSchemaResolver, projectSlug, metrics) {
|
|
15
17
|
this.dispatcher = dispatcher;
|
|
16
18
|
this.db = db;
|
|
17
19
|
this.contentSchemaResolver = contentSchemaResolver;
|
|
18
20
|
this.projectSlug = projectSlug;
|
|
21
|
+
this.metrics = metrics;
|
|
19
22
|
}
|
|
20
23
|
async run({ logger, onError, onClose }) {
|
|
21
24
|
return await new Promise(async (resolve, reject) => {
|
|
@@ -45,6 +48,7 @@ class ProjectDispatcher {
|
|
|
45
48
|
end: async () => {
|
|
46
49
|
aborted = true;
|
|
47
50
|
await listener.end();
|
|
51
|
+
this.metrics.dispose();
|
|
48
52
|
logger.info("Worker terminated", {
|
|
49
53
|
succeed: succeedTotal,
|
|
50
54
|
failed: failedTotal
|
|
@@ -53,24 +57,27 @@ class ProjectDispatcher {
|
|
|
53
57
|
});
|
|
54
58
|
try {
|
|
55
59
|
while (!aborted) {
|
|
56
|
-
|
|
60
|
+
this.metrics.heartbeat();
|
|
61
|
+
const { succeeded, retried, failedAfterAttempt, failedUnknownTarget, backoffMs } = await this.dispatcher.processBatch({
|
|
57
62
|
db,
|
|
58
63
|
contentSchemaResolver: this.contentSchemaResolver,
|
|
59
|
-
logger
|
|
64
|
+
logger,
|
|
65
|
+
project: { slug: this.projectSlug }
|
|
60
66
|
});
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
const terminalFailed = failedAfterAttempt + failedUnknownTarget;
|
|
68
|
+
this.metrics.succeeded(succeeded);
|
|
69
|
+
this.metrics.deliveryAttemptFailed(retried + failedAfterAttempt);
|
|
70
|
+
this.metrics.failed(terminalFailed);
|
|
71
|
+
succeedTotal += succeeded;
|
|
72
|
+
failedTotal += terminalFailed;
|
|
63
73
|
if (pendingError) {
|
|
64
74
|
throw pendingError;
|
|
65
75
|
}
|
|
66
76
|
if (backoffMs !== 0) {
|
|
77
|
+
const waitMs = Math.min(backoffMs ?? MAX_IDLE_SLEEP_MS, MAX_IDLE_SLEEP_MS);
|
|
67
78
|
await new Promise((resolve2, reject2) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (backoffMs !== void 0) {
|
|
71
|
-
const timeoutHandle = setTimeout(resolve2, backoffMs);
|
|
72
|
-
cleanup = () => clearTimeout(timeoutHandle);
|
|
73
|
-
}
|
|
79
|
+
const timeoutHandle = setTimeout(resolve2, waitMs);
|
|
80
|
+
const cleanup = () => clearTimeout(timeoutHandle);
|
|
74
81
|
resolvePending = () => {
|
|
75
82
|
resolve2();
|
|
76
83
|
cleanup();
|
|
@@ -85,6 +92,7 @@ class ProjectDispatcher {
|
|
|
85
92
|
onClose?.();
|
|
86
93
|
} catch (e) {
|
|
87
94
|
await listener.end();
|
|
95
|
+
this.metrics.crashed();
|
|
88
96
|
onError(e);
|
|
89
97
|
}
|
|
90
98
|
});
|