@reventlessdev/reventless-core 3.0.0-alpha.171 → 3.0.0-alpha.173
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/CHANGELOG.md +14 -0
- package/package.json +6 -6
- package/src/RequestContext.res +28 -0
- package/src/RequestContext.res.mjs +17 -5
- package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +8 -13
- package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res.mjs +6 -9
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +25 -5
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +5 -3
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +7 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +5 -10
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +3 -3
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +9 -2
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +2 -1
- package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +16 -3
- package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res.mjs +3 -2
- package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +16 -3
- package/src/adapter/Runtime/PluginRuntime_Builder_Single.res.mjs +3 -2
- package/src/adapter/Runtime/Runtime.res +63 -9
- package/src/adapter/Runtime/Runtime.res.mjs +25 -2
- package/src/components/Heartbeat/Heartbeat.res +2 -2
- package/src/components/Heartbeat/Heartbeat_Builder.res +2 -2
- package/src/components/Heartbeat/Heartbeat_Builder.res.mjs +2 -4
- package/src/plugin/component/Plugin_Builder.res +5 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +1 -1
- package/src/plugin/component/Plugin_Helpers.res +6 -1
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res +12 -5
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +8 -2
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +98 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.173 (2026-07-20)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** annotate comp + causationId at the dispatch boundary ([ef6d56d](https://github.com/ReventlessDev/reventless-core/commit/ef6d56d0104dbdb21b7cfbb8f3b2b1ead9787595))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.172 (2026-07-17)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **plugin:** scale heartbeat disconnect grace and enforce single-source interval ([7ba8fb7](https://github.com/ReventlessDev/reventless-core/commit/7ba8fb7bfbe023aba7b538329d2489f939b7fcea))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.171 (2026-07-17)
|
|
7
21
|
|
|
8
22
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.173",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.29",
|
|
32
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
|
-
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
34
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
35
33
|
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
34
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
35
|
+
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.102",
|
|
39
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.28",
|
|
40
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
41
|
-
"@reventlessdev/reventless-
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.102",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.79",
|
|
41
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.28"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
package/src/RequestContext.res
CHANGED
|
@@ -14,14 +14,42 @@
|
|
|
14
14
|
// In tests:
|
|
15
15
|
// ->Effect.provideService(RequestContext.tag, RequestContext.test())
|
|
16
16
|
|
|
17
|
+
// `causationId` = the direct-parent msgId of the message that triggered this
|
|
18
|
+
// invocation (Message.deriveMeta propagates it); reconstructs the parent → child
|
|
19
|
+
// causal chain within one `correlationId`. `component` / `pluginName` identify the
|
|
20
|
+
// component and plugin handling the invocation — mirror the `comp` / `plugin` log
|
|
21
|
+
// fields so a consumer of this service sees the same attribution the logs carry.
|
|
22
|
+
// All three are optional: absent in `.test()` and wherever the dispatch site has
|
|
23
|
+
// no value to supply.
|
|
17
24
|
type t = {
|
|
18
25
|
correlationId: string,
|
|
26
|
+
causationId?: string,
|
|
27
|
+
component?: string,
|
|
28
|
+
pluginName?: string,
|
|
19
29
|
identity: Reventless.Identity.t,
|
|
20
30
|
claims: dict<string>,
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
let tag: Context.tag<t> = Context.genericTag("reventless/RequestContext")
|
|
24
34
|
|
|
35
|
+
// Real dispatch-boundary constructor. Use this at runtime dispatch points
|
|
36
|
+
// (see Runtime.runEffect); `test` is test-only.
|
|
37
|
+
let make = (
|
|
38
|
+
~correlationId,
|
|
39
|
+
~causationId=?,
|
|
40
|
+
~component=?,
|
|
41
|
+
~pluginName=?,
|
|
42
|
+
~identity=Reventless.Identity.anonymous,
|
|
43
|
+
~claims=Dict.make(),
|
|
44
|
+
): t => {
|
|
45
|
+
correlationId,
|
|
46
|
+
causationId: ?causationId,
|
|
47
|
+
component: ?component,
|
|
48
|
+
pluginName: ?pluginName,
|
|
49
|
+
identity,
|
|
50
|
+
claims,
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
let getClaim = (ctx: t, key: string): option<string> =>
|
|
26
54
|
ctx.claims->Dict.get(key)
|
|
27
55
|
|
|
@@ -5,6 +5,19 @@ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/I
|
|
|
5
5
|
|
|
6
6
|
let tag = Context.GenericTag("reventless/RequestContext");
|
|
7
7
|
|
|
8
|
+
function make(correlationId, causationId, component, pluginName, identityOpt, claimsOpt) {
|
|
9
|
+
let identity = identityOpt !== undefined ? identityOpt : Identity$Reventless.anonymous;
|
|
10
|
+
let claims = claimsOpt !== undefined ? claimsOpt : ({});
|
|
11
|
+
return {
|
|
12
|
+
correlationId: correlationId,
|
|
13
|
+
causationId: causationId,
|
|
14
|
+
component: component,
|
|
15
|
+
pluginName: pluginName,
|
|
16
|
+
identity: identity,
|
|
17
|
+
claims: claims
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
function getClaim(ctx, key) {
|
|
9
22
|
return ctx.claims[key];
|
|
10
23
|
}
|
|
@@ -12,11 +25,9 @@ function getClaim(ctx, key) {
|
|
|
12
25
|
function withClaim(ctx, key, value) {
|
|
13
26
|
let next = Object.fromEntries(Object.entries(ctx.claims));
|
|
14
27
|
next[key] = value;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
claims: next
|
|
19
|
-
};
|
|
28
|
+
let newrecord = {...ctx};
|
|
29
|
+
newrecord.claims = next;
|
|
30
|
+
return newrecord;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
function test(correlationIdOpt, identityOpt, claimsOpt) {
|
|
@@ -32,6 +43,7 @@ function test(correlationIdOpt, identityOpt, claimsOpt) {
|
|
|
32
43
|
|
|
33
44
|
export {
|
|
34
45
|
tag,
|
|
46
|
+
make,
|
|
35
47
|
getClaim,
|
|
36
48
|
withClaim,
|
|
37
49
|
test,
|
|
@@ -31,26 +31,21 @@ module Make = (
|
|
|
31
31
|
let commandTopicHandlers: dict<effectHandler> = Dict.make()
|
|
32
32
|
let eventCollectorHandlers: dict<array<effectHandler>> = Dict.make()
|
|
33
33
|
|
|
34
|
-
let runEffect = (~correlationId=?, effect) => {
|
|
35
|
-
let cid = correlationId->Option.getOr("unknown")
|
|
36
|
-
effect
|
|
37
|
-
// Annotate the request scope so every EffectLogger call inside the handler
|
|
38
|
-
// carries correlationId as a top-level JSON field (decoded in install()).
|
|
39
|
-
->Effect.annotateLogs("correlationId", cid)
|
|
40
|
-
->Effect.provideService(RequestContext.tag, RequestContext.test(~correlationId=cid))
|
|
41
|
-
->Effect.runPromise
|
|
42
|
-
}
|
|
43
|
-
|
|
44
34
|
let aggregateHandler = aggregateName =>
|
|
45
35
|
async (event: RuntimeEnvironment.event, context) => {
|
|
46
36
|
let correlationId = event->RuntimeEnvironment.extractCorrelationId
|
|
37
|
+
let causationId = event->RuntimeEnvironment.extractCausationId
|
|
47
38
|
let comp = `AggregateRuntime(${aggregateName})`
|
|
39
|
+
// Shared dispatch helper (Runtime.runEffect) annotates correlationId / comp /
|
|
40
|
+
// causationId onto the invocation so the application handler's own log lines
|
|
41
|
+
// carry them without re-logging by hand.
|
|
42
|
+
let runEffect = effect => Runtime.runEffect(~correlationId?, ~causationId?, ~comp, effect)
|
|
48
43
|
switch event->CommandGenerator.metaInfo {
|
|
49
44
|
| Some(info) =>
|
|
50
45
|
switch commandGeneratorHandlers->Dict.get(info) {
|
|
51
46
|
| Some(handler) =>
|
|
52
47
|
EffectLogger.logDebug(~comp, `found CommandGenerator handler for ${info}`)->Effect.runSync
|
|
53
|
-
let _ = await runEffect(
|
|
48
|
+
let _ = await runEffect(handler(event->CommandGenerator.asPayload, context))
|
|
54
49
|
| None =>
|
|
55
50
|
let available = commandGeneratorHandlers->Dict.keysToArray->Array.join(",")
|
|
56
51
|
EffectLogger.logWarn(
|
|
@@ -66,7 +61,7 @@ module Make = (
|
|
|
66
61
|
switch commandTopicHandlers->Dict.get(urn) {
|
|
67
62
|
| Some(handler) =>
|
|
68
63
|
EffectLogger.logDebug(~comp, `found CommandTopic handler for ${urn}`)->Effect.runSync
|
|
69
|
-
await runEffect(
|
|
64
|
+
await runEffect(handler(event, context))
|
|
70
65
|
| None =>
|
|
71
66
|
switch eventCollectorHandlers->Dict.get(urn) {
|
|
72
67
|
| Some(handlers) =>
|
|
@@ -76,7 +71,7 @@ module Make = (
|
|
|
76
71
|
`found ${count} EventCollector handler(s) for ${urn}`,
|
|
77
72
|
)->Effect.runSync
|
|
78
73
|
let _ = await handlers
|
|
79
|
-
->Array.map(handler => runEffect(
|
|
74
|
+
->Array.map(handler => runEffect(handler(event, context)))
|
|
80
75
|
->Promise.all
|
|
81
76
|
| None =>
|
|
82
77
|
let available =
|
|
@@ -6,9 +6,9 @@ import * as Effect from "effect/Effect";
|
|
|
6
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
8
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
9
|
+
import * as Runtime$ReventlessCore from "./Runtime.res.mjs";
|
|
9
10
|
import * as Component$ReventlessCore from "../../components/Component.res.mjs";
|
|
10
11
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
11
|
-
import * as RequestContext$ReventlessCore from "../../RequestContext.res.mjs";
|
|
12
12
|
import * as CommandGenerator$ReventlessCore from "../../components/CommandGenerator/CommandGenerator.res.mjs";
|
|
13
13
|
|
|
14
14
|
function Make(RuntimeEnvironment) {
|
|
@@ -18,19 +18,17 @@ function Make(RuntimeEnvironment) {
|
|
|
18
18
|
let commandGeneratorHandlers = {};
|
|
19
19
|
let commandTopicHandlers = {};
|
|
20
20
|
let eventCollectorHandlers = {};
|
|
21
|
-
let runEffect = (correlationId, effect) => {
|
|
22
|
-
let cid = Stdlib_Option.getOr(correlationId, "unknown");
|
|
23
|
-
return Effect.runPromise(Effect.provideService(Effect.annotateLogs(effect, "correlationId", cid), RequestContext$ReventlessCore.tag, RequestContext$ReventlessCore.test(cid, undefined, undefined)));
|
|
24
|
-
};
|
|
25
21
|
let aggregateHandler = aggregateName => (async (event, context) => {
|
|
26
22
|
let correlationId = RuntimeEnvironment.extractCorrelationId(event);
|
|
23
|
+
let causationId = RuntimeEnvironment.extractCausationId(event);
|
|
27
24
|
let comp = `AggregateRuntime(` + aggregateName + `)`;
|
|
25
|
+
let runEffect = effect => Runtime$ReventlessCore.runEffect(correlationId, causationId, comp, undefined, effect);
|
|
28
26
|
let info = CommandGenerator$ReventlessCore.metaInfo(event);
|
|
29
27
|
if (info !== undefined) {
|
|
30
28
|
let handler = commandGeneratorHandlers[info];
|
|
31
29
|
if (handler !== undefined) {
|
|
32
30
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `found CommandGenerator handler for ` + info));
|
|
33
|
-
await runEffect(
|
|
31
|
+
await runEffect(handler(event, context));
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
34
|
let available = Object.keys(commandGeneratorHandlers).join(",");
|
|
@@ -42,13 +40,13 @@ function Make(RuntimeEnvironment) {
|
|
|
42
40
|
let handler = commandTopicHandlers[urn];
|
|
43
41
|
if (handler !== undefined) {
|
|
44
42
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `found CommandTopic handler for ` + urn));
|
|
45
|
-
return await runEffect(
|
|
43
|
+
return await runEffect(handler(event, context));
|
|
46
44
|
}
|
|
47
45
|
let handlers = eventCollectorHandlers[urn];
|
|
48
46
|
if (handlers !== undefined) {
|
|
49
47
|
let count = handlers.length.toString();
|
|
50
48
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `found ` + count + ` EventCollector handler(s) for ` + urn));
|
|
51
|
-
await Promise.all(handlers.map(handler => runEffect(
|
|
49
|
+
await Promise.all(handlers.map(handler => runEffect(handler(event, context))));
|
|
52
50
|
return;
|
|
53
51
|
}
|
|
54
52
|
let available = Object.keys(commandTopicHandlers).concat(Object.keys(eventCollectorHandlers)).join(",");
|
|
@@ -171,7 +169,6 @@ function Make(RuntimeEnvironment) {
|
|
|
171
169
|
commandGeneratorHandlers: commandGeneratorHandlers,
|
|
172
170
|
commandTopicHandlers: commandTopicHandlers,
|
|
173
171
|
eventCollectorHandlers: eventCollectorHandlers,
|
|
174
|
-
runEffect: runEffect,
|
|
175
172
|
aggregateHandler: aggregateHandler,
|
|
176
173
|
getRuntimeSpec: getRuntimeSpec,
|
|
177
174
|
setRuntimeSpec: setRuntimeSpec,
|
|
@@ -23,10 +23,17 @@ module Make = (
|
|
|
23
23
|
commandGenerator,
|
|
24
24
|
) => {
|
|
25
25
|
let resource = commandGenerator->Component.toPulumiResource
|
|
26
|
+
let name = resource.name->ComponentType.nameOpt(CommandGenerator.componentType)
|
|
26
27
|
let runtime = RuntimeEnvironment.make(
|
|
27
|
-
~name
|
|
28
|
+
~name,
|
|
28
29
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
29
|
-
handler
|
|
30
|
+
handler
|
|
31
|
+
->RuntimeEnvironment.asEffectHandler
|
|
32
|
+
->Runtime.runEffectHandler(
|
|
33
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
34
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
35
|
+
~comp=name,
|
|
36
|
+
)
|
|
30
37
|
),
|
|
31
38
|
~memorySize,
|
|
32
39
|
~timeout,
|
|
@@ -44,10 +51,17 @@ module Make = (
|
|
|
44
51
|
commandTopic,
|
|
45
52
|
) => {
|
|
46
53
|
let resource = commandTopic->Component.toPulumiResource
|
|
54
|
+
let name = resource.name->ComponentType.nameOpt(CommandTopic.componentType)
|
|
47
55
|
let runtime = RuntimeEnvironment.make(
|
|
48
|
-
~name
|
|
56
|
+
~name,
|
|
49
57
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
50
|
-
handler
|
|
58
|
+
handler
|
|
59
|
+
->RuntimeEnvironment.asEffectHandler
|
|
60
|
+
->Runtime.runEffectHandler(
|
|
61
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
62
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
63
|
+
~comp=name,
|
|
64
|
+
)
|
|
51
65
|
),
|
|
52
66
|
~memorySize,
|
|
53
67
|
~timeout,
|
|
@@ -72,7 +86,13 @@ module Make = (
|
|
|
72
86
|
let runtime = RuntimeEnvironment.make(
|
|
73
87
|
~name,
|
|
74
88
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
75
|
-
handler
|
|
89
|
+
handler
|
|
90
|
+
->RuntimeEnvironment.asEffectHandler
|
|
91
|
+
->Runtime.runEffectHandler(
|
|
92
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
93
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
94
|
+
~comp=name,
|
|
95
|
+
)
|
|
76
96
|
),
|
|
77
97
|
~memorySize,
|
|
78
98
|
~timeout,
|
|
@@ -13,7 +13,8 @@ function Make(RuntimeEnvironment) {
|
|
|
13
13
|
let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
|
|
14
14
|
let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
|
|
15
15
|
let resource = Component$ReventlessCore.toPulumiResource(commandGenerator);
|
|
16
|
-
|
|
16
|
+
let name = ComponentType$ReventlessCore.nameOpt(resource.__name, CommandGenerator$ReventlessCore.componentType);
|
|
17
|
+
return connect(RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
17
18
|
parent: resource
|
|
18
19
|
}));
|
|
19
20
|
};
|
|
@@ -21,7 +22,8 @@ function Make(RuntimeEnvironment) {
|
|
|
21
22
|
let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
|
|
22
23
|
let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
|
|
23
24
|
let resource = Component$ReventlessCore.toPulumiResource(commandTopic);
|
|
24
|
-
|
|
25
|
+
let name = ComponentType$ReventlessCore.nameOpt(resource.__name, CommandTopic$ReventlessCore.componentType);
|
|
26
|
+
return connect(RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
25
27
|
parent: resource
|
|
26
28
|
}));
|
|
27
29
|
};
|
|
@@ -34,7 +36,7 @@ function Make(RuntimeEnvironment) {
|
|
|
34
36
|
let opts = {
|
|
35
37
|
parent: opts_parent
|
|
36
38
|
};
|
|
37
|
-
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, opts);
|
|
39
|
+
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, opts);
|
|
38
40
|
EventCollectorChannel.connect(name, [{
|
|
39
41
|
channel: eventCollector.channel,
|
|
40
42
|
eventTopics: eventTopics,
|
|
@@ -26,7 +26,13 @@ module Make = (
|
|
|
26
26
|
let runtime = RuntimeEnvironment.make(
|
|
27
27
|
~name,
|
|
28
28
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
29
|
-
handler
|
|
29
|
+
handler
|
|
30
|
+
->RuntimeEnvironment.asEffectHandler
|
|
31
|
+
->Runtime.runEffectHandler(
|
|
32
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
33
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
34
|
+
~comp=name,
|
|
35
|
+
)
|
|
30
36
|
),
|
|
31
37
|
~memorySize,
|
|
32
38
|
~timeout,
|
|
@@ -16,7 +16,7 @@ function Make(RuntimeEnvironment) {
|
|
|
16
16
|
let opts = {
|
|
17
17
|
parent: opts_parent
|
|
18
18
|
};
|
|
19
|
-
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
19
|
+
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
20
20
|
parent: resource
|
|
21
21
|
});
|
|
22
22
|
EventCollectorChannel.connect(name, [{
|
|
@@ -35,20 +35,15 @@ module Make = (
|
|
|
35
35
|
})
|
|
36
36
|
type effectHandler = Runtime.effectHandler<RuntimeEnvironment.event, context, unit, string>
|
|
37
37
|
let eventCollectorHandlers: dict<array<effectHandler>> = Dict.make()
|
|
38
|
-
let runEffect = (~correlationId=?, effect) => {
|
|
39
|
-
let cid = correlationId->Option.getOr("unknown")
|
|
40
|
-
effect
|
|
41
|
-
// Annotate the request scope so every EffectLogger call inside the handler
|
|
42
|
-
// carries correlationId as a top-level JSON field (decoded in install()).
|
|
43
|
-
->Effect.annotateLogs("correlationId", cid)
|
|
44
|
-
->Effect.provideService(RequestContext.tag, RequestContext.test(~correlationId=cid))
|
|
45
|
-
->Effect.runPromise
|
|
46
|
-
}
|
|
47
38
|
|
|
48
39
|
let eventCollectorHandler = parentName =>
|
|
49
40
|
async (event: RuntimeEnvironment.event, context) => {
|
|
50
41
|
let correlationId = event->RuntimeEnvironment.extractCorrelationId
|
|
42
|
+
let causationId = event->RuntimeEnvironment.extractCausationId
|
|
51
43
|
let comp = `EventCollectorRuntime(${parentName})`
|
|
44
|
+
// Shared dispatch helper annotates correlationId / comp / causationId so the
|
|
45
|
+
// application handler's own log lines carry them (see Runtime.runEffect).
|
|
46
|
+
let runEffect = effect => Runtime.runEffect(~correlationId?, ~causationId?, ~comp, effect)
|
|
52
47
|
let _ = await event
|
|
53
48
|
->RuntimeEnvironment.groupBySource
|
|
54
49
|
->Dict.toArray
|
|
@@ -61,7 +56,7 @@ module Make = (
|
|
|
61
56
|
`found ${count} handler(s) for ${urn}`,
|
|
62
57
|
)->Effect.runSync
|
|
63
58
|
let _ = await handlers
|
|
64
|
-
->Array.map(handler => runEffect(
|
|
59
|
+
->Array.map(handler => runEffect(handler(event, context)))
|
|
65
60
|
->Promise.all
|
|
66
61
|
| None => EffectLogger.logWarn(~comp, `no handler found: ${urn}`)->Effect.runSync
|
|
67
62
|
}
|
|
@@ -6,9 +6,9 @@ import * as Pulumi from "@pulumi/pulumi";
|
|
|
6
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
7
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
8
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
9
|
+
import * as Runtime$ReventlessCore from "./Runtime.res.mjs";
|
|
9
10
|
import * as Component$ReventlessCore from "../../components/Component.res.mjs";
|
|
10
11
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
11
|
-
import * as RequestContext$ReventlessCore from "../../RequestContext.res.mjs";
|
|
12
12
|
|
|
13
13
|
function Make(RuntimeEnvironment) {
|
|
14
14
|
return EventCollectorChannel => {
|
|
@@ -29,6 +29,7 @@ function Make(RuntimeEnvironment) {
|
|
|
29
29
|
let eventCollectorHandlers = {};
|
|
30
30
|
let eventCollectorHandler = parentName => (async (event, context) => {
|
|
31
31
|
let correlationId = RuntimeEnvironment.extractCorrelationId(event);
|
|
32
|
+
let causationId = RuntimeEnvironment.extractCausationId(event);
|
|
32
33
|
let comp = `EventCollectorRuntime(` + parentName + `)`;
|
|
33
34
|
await Promise.all(Object.entries(RuntimeEnvironment.groupBySource(event)).map(async param => {
|
|
34
35
|
let event = param[1];
|
|
@@ -41,8 +42,7 @@ function Make(RuntimeEnvironment) {
|
|
|
41
42
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `found ` + count + ` handler(s) for ` + urn));
|
|
42
43
|
await Promise.all(handlers.map(handler => {
|
|
43
44
|
let effect = handler(event, context);
|
|
44
|
-
|
|
45
|
-
return Effect.runPromise(Effect.provideService(Effect.annotateLogs(effect, "correlationId", cid), RequestContext$ReventlessCore.tag, RequestContext$ReventlessCore.test(cid, undefined, undefined)));
|
|
45
|
+
return Runtime$ReventlessCore.runEffect(correlationId, causationId, comp, undefined, effect);
|
|
46
46
|
}));
|
|
47
47
|
}));
|
|
48
48
|
});
|
|
@@ -22,10 +22,17 @@ module Make = (
|
|
|
22
22
|
commandTopic,
|
|
23
23
|
) => {
|
|
24
24
|
let resource = commandTopic->Component.toPulumiResource
|
|
25
|
+
let name = resource.name->ComponentType.nameOpt(CommandTopic.componentType)
|
|
25
26
|
let runtime = RuntimeEnvironment.make(
|
|
26
|
-
~name
|
|
27
|
+
~name,
|
|
27
28
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
28
|
-
handler
|
|
29
|
+
handler
|
|
30
|
+
->RuntimeEnvironment.asEffectHandler
|
|
31
|
+
->Runtime.runEffectHandler(
|
|
32
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
33
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
34
|
+
~comp=name,
|
|
35
|
+
)
|
|
29
36
|
),
|
|
30
37
|
~memorySize,
|
|
31
38
|
~timeout,
|
|
@@ -11,7 +11,8 @@ function Make(RuntimeEnvironment) {
|
|
|
11
11
|
let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
|
|
12
12
|
let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
|
|
13
13
|
let resource = Component$ReventlessCore.toPulumiResource(commandTopic);
|
|
14
|
-
|
|
14
|
+
let name = ComponentType$ReventlessCore.nameOpt(resource.__name, CommandTopic$ReventlessCore.componentType);
|
|
15
|
+
return connect(RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
15
16
|
parent: resource
|
|
16
17
|
}));
|
|
17
18
|
};
|
|
@@ -26,7 +26,13 @@ module Make = (
|
|
|
26
26
|
let runtime = RuntimeEnvironment.make(
|
|
27
27
|
~name,
|
|
28
28
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
29
|
-
handler
|
|
29
|
+
handler
|
|
30
|
+
->RuntimeEnvironment.asEffectHandler
|
|
31
|
+
->Runtime.runEffectHandler(
|
|
32
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
33
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
34
|
+
~comp=name,
|
|
35
|
+
)
|
|
30
36
|
),
|
|
31
37
|
~memorySize,
|
|
32
38
|
~timeout,
|
|
@@ -70,10 +76,17 @@ module Make = (
|
|
|
70
76
|
dcbCommandTopic,
|
|
71
77
|
) => {
|
|
72
78
|
let resource = dcbCommandTopic->Component.toPulumiResource
|
|
79
|
+
let name = resource.name->ComponentType.nameOpt(CommandTopic.componentType)
|
|
73
80
|
let runtime = RuntimeEnvironment.make(
|
|
74
|
-
~name
|
|
81
|
+
~name,
|
|
75
82
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
76
|
-
handler
|
|
83
|
+
handler
|
|
84
|
+
->RuntimeEnvironment.asEffectHandler
|
|
85
|
+
->Runtime.runEffectHandler(
|
|
86
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
87
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
88
|
+
~comp=name,
|
|
89
|
+
)
|
|
77
90
|
),
|
|
78
91
|
~memorySize,
|
|
79
92
|
~timeout,
|
|
@@ -18,7 +18,7 @@ function Make(RuntimeEnvironment) {
|
|
|
18
18
|
let opts = {
|
|
19
19
|
parent: opts_parent
|
|
20
20
|
};
|
|
21
|
-
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
21
|
+
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
22
22
|
parent: resource
|
|
23
23
|
});
|
|
24
24
|
EventCollectorChannel.connect(name, [{
|
|
@@ -40,7 +40,8 @@ function Make(RuntimeEnvironment) {
|
|
|
40
40
|
let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
|
|
41
41
|
let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
|
|
42
42
|
let resource = Component$ReventlessCore.toPulumiResource(dcbCommandTopic);
|
|
43
|
-
|
|
43
|
+
let name = ComponentType$ReventlessCore.nameOpt(resource.__name, CommandTopic$ReventlessCore.componentType);
|
|
44
|
+
return connect(RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
44
45
|
parent: resource
|
|
45
46
|
}));
|
|
46
47
|
};
|
|
@@ -26,7 +26,13 @@ module Make = (
|
|
|
26
26
|
let runtime = RuntimeEnvironment.make(
|
|
27
27
|
~name,
|
|
28
28
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
29
|
-
handler
|
|
29
|
+
handler
|
|
30
|
+
->RuntimeEnvironment.asEffectHandler
|
|
31
|
+
->Runtime.runEffectHandler(
|
|
32
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
33
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
34
|
+
~comp=name,
|
|
35
|
+
)
|
|
30
36
|
),
|
|
31
37
|
~memorySize,
|
|
32
38
|
~timeout,
|
|
@@ -70,10 +76,17 @@ module Make = (
|
|
|
70
76
|
dcbCommandTopic,
|
|
71
77
|
) => {
|
|
72
78
|
let resource = dcbCommandTopic->Component.toPulumiResource
|
|
79
|
+
let name = resource.name->ComponentType.nameOpt(CommandTopic.componentType)
|
|
73
80
|
let runtime = RuntimeEnvironment.make(
|
|
74
|
-
~name
|
|
81
|
+
~name,
|
|
75
82
|
~handler=handler->Pulumi.Output.apply(handler =>
|
|
76
|
-
handler
|
|
83
|
+
handler
|
|
84
|
+
->RuntimeEnvironment.asEffectHandler
|
|
85
|
+
->Runtime.runEffectHandler(
|
|
86
|
+
~extractCorrelationId=RuntimeEnvironment.extractCorrelationId,
|
|
87
|
+
~extractCausationId=RuntimeEnvironment.extractCausationId,
|
|
88
|
+
~comp=name,
|
|
89
|
+
)
|
|
77
90
|
),
|
|
78
91
|
~memorySize,
|
|
79
92
|
~timeout,
|
|
@@ -18,7 +18,7 @@ function Make(RuntimeEnvironment) {
|
|
|
18
18
|
let opts = {
|
|
19
19
|
parent: opts_parent
|
|
20
20
|
};
|
|
21
|
-
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
21
|
+
let runtime = RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
22
22
|
parent: resource
|
|
23
23
|
});
|
|
24
24
|
EventCollectorChannel.connect(name, [{
|
|
@@ -40,7 +40,8 @@ function Make(RuntimeEnvironment) {
|
|
|
40
40
|
let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
|
|
41
41
|
let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
|
|
42
42
|
let resource = Component$ReventlessCore.toPulumiResource(dcbCommandTopic);
|
|
43
|
-
|
|
43
|
+
let name = ComponentType$ReventlessCore.nameOpt(resource.__name, CommandTopic$ReventlessCore.componentType);
|
|
44
|
+
return connect(RuntimeEnvironment.make(name, handler.apply(handler => Runtime$ReventlessCore.runEffectHandler(RuntimeEnvironment.extractCorrelationId, RuntimeEnvironment.extractCausationId, name, undefined, RuntimeEnvironment.asEffectHandler(handler))), memorySize, timeout, {
|
|
44
45
|
parent: resource
|
|
45
46
|
}));
|
|
46
47
|
};
|
|
@@ -4,17 +4,70 @@ type effectHandler<'event, 'context, 'result, 'error> = (
|
|
|
4
4
|
'context,
|
|
5
5
|
) => Effect.t<'result, 'error, unit>
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
// ─── Dispatch-boundary annotation ────────────────────────────────────────────
|
|
8
|
+
// Single source of truth for what every handler invocation carries. Annotating
|
|
9
|
+
// here (rather than in each runtime builder) guarantees that every log line
|
|
10
|
+
// inside the handler — framework *and* application — surfaces the same
|
|
11
|
+
// `correlationId` / `comp` / `causationId` top-level JSON fields (EffectLogger
|
|
12
|
+
// decodes them), and that a populated RequestContext is provided. `comp` is what
|
|
13
|
+
// makes two components hosted in one runtime process separable purely by field;
|
|
14
|
+
// `causationId` reconstructs the parent → child chain within one `correlationId`.
|
|
15
|
+
let annotateInvocation = (
|
|
16
|
+
effect,
|
|
17
|
+
~correlationId=?,
|
|
18
|
+
~causationId=?,
|
|
19
|
+
~comp=?,
|
|
20
|
+
~pluginName=?,
|
|
21
|
+
) => {
|
|
22
|
+
let cid = correlationId->Option.getOr("unknown")
|
|
23
|
+
let annotateOpt = (eff, key, value) =>
|
|
24
|
+
switch value {
|
|
25
|
+
| Some(v) => eff->Effect.annotateLogs(key, v)
|
|
26
|
+
| None => eff
|
|
27
|
+
}
|
|
28
|
+
effect
|
|
29
|
+
->Effect.annotateLogs("correlationId", cid)
|
|
30
|
+
->annotateOpt("comp", comp)
|
|
31
|
+
->annotateOpt("causationId", causationId)
|
|
32
|
+
->Effect.provideService(
|
|
33
|
+
RequestContext.tag,
|
|
34
|
+
RequestContext.make(~correlationId=cid, ~causationId?, ~component=?comp, ~pluginName?),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Annotate + run. Used by the multi-component dispatchers (AllAggregates,
|
|
39
|
+
// AllEventCollectors) that loop over per-source handlers.
|
|
40
|
+
let runEffect = (~correlationId=?, ~causationId=?, ~comp=?, ~pluginName=?, effect) =>
|
|
41
|
+
effect
|
|
42
|
+
->annotateInvocation(~correlationId?, ~causationId?, ~comp?, ~pluginName?)
|
|
43
|
+
->Effect.runPromise
|
|
44
|
+
|
|
45
|
+
// Convert an effectHandler to an eventHandler at build time, annotating the
|
|
46
|
+
// invocation on each call. Used by the single-component-per-Lambda strategies
|
|
47
|
+
// (Micro / PerAggregate / Plugin / PerEventCollector / PerExtensionPoint), which
|
|
48
|
+
// pass the Environment's `extractCorrelationId` / `extractCausationId` and the
|
|
49
|
+
// component `comp` so their application handlers get the same fields as the
|
|
50
|
+
// multi-component path. Extractors default to none for generic use.
|
|
51
|
+
let runEffectHandler = (
|
|
52
|
+
~extractCorrelationId: option<'event => option<string>>=?,
|
|
53
|
+
~extractCausationId: option<'event => option<string>>=?,
|
|
54
|
+
~comp=?,
|
|
55
|
+
~pluginName=?,
|
|
56
|
+
handler: effectHandler<'event, 'context, 'result, 'error>,
|
|
57
|
+
): eventHandler<'event, 'context, 'result> =>
|
|
58
|
+
(event, ctx) => {
|
|
59
|
+
let correlationId = switch extractCorrelationId {
|
|
60
|
+
| Some(f) => f(event)
|
|
61
|
+
| None => None
|
|
62
|
+
}
|
|
63
|
+
let causationId = switch extractCausationId {
|
|
64
|
+
| Some(f) => f(event)
|
|
65
|
+
| None => None
|
|
66
|
+
}
|
|
15
67
|
handler(event, ctx)
|
|
16
|
-
->
|
|
68
|
+
->annotateInvocation(~correlationId?, ~causationId?, ~comp?, ~pluginName?)
|
|
17
69
|
->Effect.runPromise
|
|
70
|
+
}
|
|
18
71
|
|
|
19
72
|
type environment<'parts> = {
|
|
20
73
|
parts: 'parts,
|
|
@@ -36,6 +89,7 @@ module type Environment = {
|
|
|
36
89
|
let make: environmentMaker<event, context, 'result, parts>
|
|
37
90
|
let groupBySource: event => dict<event>
|
|
38
91
|
let extractCorrelationId: event => option<string>
|
|
92
|
+
let extractCausationId: event => option<string>
|
|
39
93
|
let asEventHandler: 'a => eventHandler<event, context, 'result>
|
|
40
94
|
let asEffectHandler: 'a => effectHandler<event, context, 'result, 'error>
|
|
41
95
|
}
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
3
4
|
import * as Effect from "effect/Effect";
|
|
4
5
|
import * as RequestContext$ReventlessCore from "../../RequestContext.res.mjs";
|
|
5
6
|
|
|
6
|
-
function
|
|
7
|
-
|
|
7
|
+
function annotateInvocation(effect, correlationId, causationId, comp, pluginName) {
|
|
8
|
+
let cid = Stdlib_Option.getOr(correlationId, "unknown");
|
|
9
|
+
let annotateOpt = (eff, key, value) => {
|
|
10
|
+
if (value !== undefined) {
|
|
11
|
+
return Effect.annotateLogs(eff, key, value);
|
|
12
|
+
} else {
|
|
13
|
+
return eff;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return Effect.provideService(annotateOpt(annotateOpt(Effect.annotateLogs(effect, "correlationId", cid), "comp", comp), "causationId", causationId), RequestContext$ReventlessCore.tag, RequestContext$ReventlessCore.make(cid, causationId, comp, pluginName, undefined, undefined));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function runEffect(correlationId, causationId, comp, pluginName, effect) {
|
|
20
|
+
return Effect.runPromise(annotateInvocation(effect, correlationId, causationId, comp, pluginName));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runEffectHandler(extractCorrelationId, extractCausationId, comp, pluginName, handler) {
|
|
24
|
+
return (event, ctx) => {
|
|
25
|
+
let correlationId = extractCorrelationId !== undefined ? extractCorrelationId(event) : undefined;
|
|
26
|
+
let causationId = extractCausationId !== undefined ? extractCausationId(event) : undefined;
|
|
27
|
+
return Effect.runPromise(annotateInvocation(handler(event, ctx), correlationId, causationId, comp, pluginName));
|
|
28
|
+
};
|
|
8
29
|
}
|
|
9
30
|
|
|
10
31
|
let CommandHandlerDefaults = {
|
|
@@ -16,6 +37,8 @@ let CommandHandlerDefaults = {
|
|
|
16
37
|
};
|
|
17
38
|
|
|
18
39
|
export {
|
|
40
|
+
annotateInvocation,
|
|
41
|
+
runEffect,
|
|
19
42
|
runEffectHandler,
|
|
20
43
|
CommandHandlerDefaults,
|
|
21
44
|
}
|
|
@@ -11,13 +11,13 @@ module type T = {
|
|
|
11
11
|
let connect: (
|
|
12
12
|
~runtime: Runtime.environment<runtimeParts>,
|
|
13
13
|
~remoteChannel: CommandTopic_Adapter.remoteChannel,
|
|
14
|
-
~timeout: int
|
|
14
|
+
~timeout: int,
|
|
15
15
|
component,
|
|
16
16
|
) => unit
|
|
17
17
|
|
|
18
18
|
let makeHandler: (
|
|
19
19
|
~id: string,
|
|
20
|
-
~timeout: int
|
|
20
|
+
~timeout: int,
|
|
21
21
|
~publishToPluginExtensionPoint: CommandTopic.publishJsons,
|
|
22
22
|
) => Pulumi.Output.t<Runtime.eventHandler<unit, 'context, unit>>
|
|
23
23
|
|
|
@@ -5,7 +5,7 @@ module Make = (Runner: Heartbeat_Adapter.Runner): (
|
|
|
5
5
|
|
|
6
6
|
let construct = (_self, _name) => ()
|
|
7
7
|
|
|
8
|
-
let connect = (~runtime, ~remoteChannel, ~timeout
|
|
8
|
+
let connect = (~runtime, ~remoteChannel, ~timeout, heartbeat) => {
|
|
9
9
|
let heartbeatResource = heartbeat->Component.toPulumiResource
|
|
10
10
|
let name =
|
|
11
11
|
heartbeatResource.name
|
|
@@ -24,7 +24,7 @@ module Make = (Runner: Heartbeat_Adapter.Runner): (
|
|
|
24
24
|
let _ = heartbeat->Component.setOutputs(outputs)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
let makeHandler = (~id, ~timeout
|
|
27
|
+
let makeHandler = (~id, ~timeout, ~publishToPluginExtensionPoint) => {
|
|
28
28
|
module Callback = Heartbeat_Callback.Make({
|
|
29
29
|
let publishToPluginExtensionPoint = publishToPluginExtensionPoint
|
|
30
30
|
let id = id
|
|
@@ -9,8 +9,7 @@ import * as Heartbeat_Callback$ReventlessCore from "./Heartbeat_Callback.res.mjs
|
|
|
9
9
|
|
|
10
10
|
function Make(Runner) {
|
|
11
11
|
let construct = (_self, _name) => {};
|
|
12
|
-
let connect = (runtime, remoteChannel,
|
|
13
|
-
let timeout = timeoutOpt !== undefined ? timeoutOpt : 10;
|
|
12
|
+
let connect = (runtime, remoteChannel, timeout, heartbeat) => {
|
|
14
13
|
let heartbeatResource = Component$ReventlessCore.toPulumiResource(heartbeat);
|
|
15
14
|
let name = ComponentType$ReventlessCore.name(Stdlib_Option.getOr(heartbeatResource.__name, "Unnamed"), Heartbeat$ReventlessCore.componentType);
|
|
16
15
|
let opts_parent = heartbeatResource;
|
|
@@ -24,8 +23,7 @@ function Make(Runner) {
|
|
|
24
23
|
};
|
|
25
24
|
Component$ReventlessCore.setOutputs(heartbeat, outputs);
|
|
26
25
|
};
|
|
27
|
-
let makeHandler = (id,
|
|
28
|
-
let timeout = timeoutOpt !== undefined ? timeoutOpt : 10;
|
|
26
|
+
let makeHandler = (id, timeout, publishToPluginExtensionPoint) => {
|
|
29
27
|
let Callback = Heartbeat_Callback$ReventlessCore.Make({
|
|
30
28
|
publishToPluginExtensionPoint: publishToPluginExtensionPoint,
|
|
31
29
|
id: id,
|
|
@@ -887,7 +887,11 @@ module Make = (
|
|
|
887
887
|
| Some((_, pluginExtensionPointCommandTopicRemoteChannel)) =>
|
|
888
888
|
// Notify platform hook that the EP channel is available (AWS extracts SQS URL for bundled heartbeat)
|
|
889
889
|
Spec.hooks.onHeartbeatEpChannelAvailable->Option.forEach(hook =>
|
|
890
|
-
hook(
|
|
890
|
+
hook(
|
|
891
|
+
pluginExtensionPointCommandTopicRemoteChannel->Obj.magic,
|
|
892
|
+
~pluginId=id,
|
|
893
|
+
~heartbeatInterval,
|
|
894
|
+
)
|
|
891
895
|
)
|
|
892
896
|
heartbeat->PluginRuntimeBuilder.forPluginHeartbeat(
|
|
893
897
|
~handler,
|
|
@@ -545,7 +545,7 @@ function Make(Spec) {
|
|
|
545
545
|
let handler = SpecificHeartbeat.makeHandler(id, heartbeatInterval, publishToPluginExtensionPoint);
|
|
546
546
|
if (coreSetup !== undefined) {
|
|
547
547
|
let pluginExtensionPointCommandTopicRemoteChannel$1 = coreSetup[1];
|
|
548
|
-
Stdlib_Option.forEach(Spec.hooks.onHeartbeatEpChannelAvailable, hook => hook(pluginExtensionPointCommandTopicRemoteChannel$1, id));
|
|
548
|
+
Stdlib_Option.forEach(Spec.hooks.onHeartbeatEpChannelAvailable, hook => hook(pluginExtensionPointCommandTopicRemoteChannel$1, id, heartbeatInterval));
|
|
549
549
|
PluginRuntimeBuilder.forPluginHeartbeat(handler, none => SpecificHeartbeat.connect(none, pluginExtensionPointCommandTopicRemoteChannel$1, heartbeatInterval, heartbeat), undefined, undefined, heartbeat);
|
|
550
550
|
} else {
|
|
551
551
|
PluginRuntimeBuilder.forPluginHeartbeat(handler, param => {}, undefined, undefined, heartbeat);
|
|
@@ -957,7 +957,12 @@ type platformHooks = {
|
|
|
957
957
|
onDcbEventLogCreated?: unknown => unit,
|
|
958
958
|
onDcbCommandTopicCreated?: unknown => unit,
|
|
959
959
|
onDcbSlicesCreated?: unknown => unit,
|
|
960
|
-
|
|
960
|
+
// heartbeatInterval carries the plugin's heartbeat cadence so the AWS bundled
|
|
961
|
+
// heartbeat Lambda emits Heartbeat(interval) matching its EventBridge schedule
|
|
962
|
+
// rate. Both derive from the same heartbeatInterval; threading it here prevents
|
|
963
|
+
// the schedule-rate vs command-interval divergence that caused connect/disconnect
|
|
964
|
+
// flapping (schedule every 60min but Heartbeat(10) → 12min grace).
|
|
965
|
+
onHeartbeatEpChannelAvailable?: (unknown, ~pluginId: string, ~heartbeatInterval: int) => unit,
|
|
961
966
|
// ── Admin → Plugin coordination ────────────────────────────────────────
|
|
962
967
|
// Set by makePlatform after Admin.construct(); read by Plugin_Builder to
|
|
963
968
|
// wire the local admin connection path (in-memory). Starts as an empty
|
|
@@ -5,6 +5,14 @@ open Reventless.Plugin
|
|
|
5
5
|
|
|
6
6
|
module PluginExtensionPointSpec = ReventlessInfra.PluginExtensionPointSpec
|
|
7
7
|
|
|
8
|
+
// Grace before a plugin is marked Disconnected once heartbeats stop, expressed in
|
|
9
|
+
// whole minutes (schedules can only be created by minute). The margin scales with
|
|
10
|
+
// the interval so large cadences keep real headroom: a fixed +2 gave a 60-min
|
|
11
|
+
// heartbeat only ~3% slack, so a single late EventBridge beat + Lambda cold-start
|
|
12
|
+
// could trip a spurious disconnect. `+ max(2, interval / 2)` keeps ≥ +2 min for
|
|
13
|
+
// small intervals and ≥ +50% for large ones (5→7, 10→15, 60→90).
|
|
14
|
+
let disconnectGrace = interval => interval + max(2, interval / 2)
|
|
15
|
+
|
|
8
16
|
module type Spec = {
|
|
9
17
|
let runtimeOps: PluginRuntimeOperations.operations
|
|
10
18
|
let environment: string
|
|
@@ -145,10 +153,9 @@ module Make = (Spec: Spec) => {
|
|
|
145
153
|
switch cmd {
|
|
146
154
|
| PluginExtensionPointSpec.Heartbeat(interval) => [
|
|
147
155
|
PublishCommand(Plugin.name(id), Delegate.Heartbeat(Plugin.version(id))),
|
|
148
|
-
// Re-
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
HandleDirective(directiveHandler, CreateDisconnectSchedule(id, interval + 2)),
|
|
156
|
+
// Re-arm the disconnect schedule with an interval-scaled grace so a
|
|
157
|
+
// single late beat doesn't trip a spurious disconnect (see disconnectGrace).
|
|
158
|
+
HandleDirective(directiveHandler, CreateDisconnectSchedule(id, disconnectGrace(interval))),
|
|
152
159
|
]
|
|
153
160
|
| RedetectPlugin(interval) => [
|
|
154
161
|
// Deploy-time re-detect: drive Redetect (not Heartbeat) so an already-connected
|
|
@@ -156,7 +163,7 @@ module Make = (Spec: Spec) => {
|
|
|
156
163
|
// re-arm the disconnect schedule exactly like a heartbeat so liveness tracking
|
|
157
164
|
// continues from the deploy moment.
|
|
158
165
|
PublishCommand(Plugin.name(id), Delegate.Redetect(Plugin.version(id))),
|
|
159
|
-
HandleDirective(directiveHandler, CreateDisconnectSchedule(id, interval
|
|
166
|
+
HandleDirective(directiveHandler, CreateDisconnectSchedule(id, disconnectGrace(interval))),
|
|
160
167
|
]
|
|
161
168
|
| ConnectPlugin(pluginDefinition) =>
|
|
162
169
|
// Validate protocol versions declared by the connecting plugin.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
5
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
6
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
8
9
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
@@ -17,6 +18,10 @@ import * as PluginsReadModelSpec$ReventlessCore from "../lifecycle/PluginsReadMo
|
|
|
17
18
|
import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
18
19
|
import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
|
|
19
20
|
|
|
21
|
+
function disconnectGrace(interval) {
|
|
22
|
+
return interval + Primitive_int.max(2, interval / 2 | 0) | 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
function Make(Spec) {
|
|
21
26
|
let forwardCommand = async (_id, command, extensionPointName, queryEngine) => {
|
|
22
27
|
let jsons = await queryEngine.scan(PluginSpec$ReventlessCore.name, [
|
|
@@ -151,7 +156,7 @@ function Make(Spec) {
|
|
|
151
156
|
_1: {
|
|
152
157
|
TAG: "CreateDisconnectSchedule",
|
|
153
158
|
_0: id,
|
|
154
|
-
_1: cmd._0
|
|
159
|
+
_1: disconnectGrace(cmd._0)
|
|
155
160
|
}
|
|
156
161
|
}
|
|
157
162
|
];
|
|
@@ -201,7 +206,7 @@ function Make(Spec) {
|
|
|
201
206
|
_1: {
|
|
202
207
|
TAG: "CreateDisconnectSchedule",
|
|
203
208
|
_0: id,
|
|
204
|
-
_1: cmd._0
|
|
209
|
+
_1: disconnectGrace(cmd._0)
|
|
205
210
|
}
|
|
206
211
|
}
|
|
207
212
|
];
|
|
@@ -369,6 +374,7 @@ export {
|
|
|
369
374
|
name,
|
|
370
375
|
Id,
|
|
371
376
|
PluginExtensionPointSpec,
|
|
377
|
+
disconnectGrace,
|
|
372
378
|
Make,
|
|
373
379
|
moduleUrl,
|
|
374
380
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Regression tests for the plugin-lifecycle heartbeat / disconnect margin.
|
|
2
|
+
//
|
|
3
|
+
// A plugin's liveness is scheduler-driven: every heartbeat re-arms a one-time
|
|
4
|
+
// disconnect schedule, and if heartbeats stop the schedule fires and marks the
|
|
5
|
+
// plugin Disconnected. Two independently-wired values must agree — the recurring
|
|
6
|
+
// heartbeat *schedule rate* and the *interval carried inside the Heartbeat command*
|
|
7
|
+
// (which drives CreateDisconnectSchedule). A deployed plugin once ran with schedule
|
|
8
|
+
// rate(60min) but a Heartbeat(10) command (grace ~12min), so it reconnected for
|
|
9
|
+
// ~12min after each hourly beat then sat Disconnected for the rest of the hour —
|
|
10
|
+
// it flapped. See docs/plans/plugin-heartbeat-disconnect-margin-hardening.md.
|
|
11
|
+
//
|
|
12
|
+
// These tests pin the two guarantees that the fix provides at this layer:
|
|
13
|
+
// 1. the disconnect grace scales with the interval (large cadences keep headroom);
|
|
14
|
+
// 2. the disconnect schedule the EP mapping arms is derived from the *same*
|
|
15
|
+
// interval the Heartbeat/RedetectPlugin command carries — never a constant.
|
|
16
|
+
// (The schedule-rate ⇄ command-interval single-source is additionally enforced at
|
|
17
|
+
// compile time now that Heartbeat_Builder's ~timeout is mandatory.)
|
|
18
|
+
|
|
19
|
+
open JestGlobals
|
|
20
|
+
|
|
21
|
+
module PluginExtensionPointSpec = ReventlessInfra.PluginExtensionPointSpec
|
|
22
|
+
open ReventlessInfra.ExtensionPointMapping
|
|
23
|
+
|
|
24
|
+
// The mapping functor only needs runtimeOps at directive-*execution* time; the
|
|
25
|
+
// pure mapIncomingCommand under test never touches it, so a stub Spec suffices
|
|
26
|
+
// (mirrors the Obj.magic stubbing already used in HeartbeatTest.res).
|
|
27
|
+
module TestMapping = PluginExtensionPoint_Plugin.Make({
|
|
28
|
+
let runtimeOps = Obj.magic()
|
|
29
|
+
let environment = "test"
|
|
30
|
+
let updateApiSchema = None
|
|
31
|
+
let manageSubscriptions = None
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Pull the disconnect-schedule grace (in minutes) out of the actions a mapping
|
|
35
|
+
// emits for an incoming command, if any.
|
|
36
|
+
let graceFromActions = actions =>
|
|
37
|
+
actions->Array.findMap((action: commandAction<_, _>) =>
|
|
38
|
+
switch action {
|
|
39
|
+
| HandleDirective(_, PluginExtensionPointSpec.CreateDisconnectSchedule(_, timeout)) =>
|
|
40
|
+
Some(timeout)
|
|
41
|
+
| _ => None
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
let meta = Message.generateMeta(~service="test", ~user="test")
|
|
46
|
+
|
|
47
|
+
describe("disconnectGrace", () => {
|
|
48
|
+
// The observed-bug fix: the margin must scale, so a 60-min cadence keeps real
|
|
49
|
+
// headroom instead of the old fixed +2 (~3%).
|
|
50
|
+
testSync("scales with the interval (5→7, 10→15, 60→90)", () => {
|
|
51
|
+
expect(PluginExtensionPoint_Plugin.disconnectGrace(5))->toBe(7)
|
|
52
|
+
expect(PluginExtensionPoint_Plugin.disconnectGrace(10))->toBe(15)
|
|
53
|
+
expect(PluginExtensionPoint_Plugin.disconnectGrace(60))->toBe(90)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
testSync("always leaves at least a full interval of headroom past one beat", () => {
|
|
57
|
+
// Grace must exceed the interval (else a single beat's cadence trips a
|
|
58
|
+
// disconnect) and keep the historical ≥ +2 min floor for small intervals.
|
|
59
|
+
[1, 2, 5, 10, 30, 60, 120]->Array.forEach(interval => {
|
|
60
|
+
let grace = PluginExtensionPoint_Plugin.disconnectGrace(interval)
|
|
61
|
+
expect(grace > interval)->toBe(true)
|
|
62
|
+
expect(grace >= interval + 2)->toBe(true)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
testSync("a 60-min cadence gets far more than the old fixed +2 grace", () => {
|
|
67
|
+
// The exact regression: interval 60 previously armed a 62-min disconnect.
|
|
68
|
+
expect(PluginExtensionPoint_Plugin.disconnectGrace(60) > 62)->toBe(true)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("PluginExtensionPoint mapping arms the disconnect schedule from the beat interval", () => {
|
|
73
|
+
testSync("Heartbeat(interval) → CreateDisconnectSchedule(_, disconnectGrace(interval))", () => {
|
|
74
|
+
[5, 10, 60]->Array.forEach(interval => {
|
|
75
|
+
let actions = TestMapping.PluginMapping.mapIncomingCommand(
|
|
76
|
+
"Test@1",
|
|
77
|
+
PluginExtensionPointSpec.Heartbeat(interval),
|
|
78
|
+
meta,
|
|
79
|
+
)
|
|
80
|
+
expect(actions->graceFromActions)->toEqual(
|
|
81
|
+
Some(PluginExtensionPoint_Plugin.disconnectGrace(interval)),
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
testSync("RedetectPlugin(interval) arms the same interval-derived grace as a heartbeat", () => {
|
|
87
|
+
[5, 10, 60]->Array.forEach(interval => {
|
|
88
|
+
let actions = TestMapping.PluginMapping.mapIncomingCommand(
|
|
89
|
+
"Test@1",
|
|
90
|
+
PluginExtensionPointSpec.RedetectPlugin(interval),
|
|
91
|
+
meta,
|
|
92
|
+
)
|
|
93
|
+
expect(actions->graceFromActions)->toEqual(
|
|
94
|
+
Some(PluginExtensionPoint_Plugin.disconnectGrace(interval)),
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
})
|