@reventlessdev/reventless-aws 3.0.0-alpha.303 → 3.0.0-alpha.305
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 +2 -2
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +12 -0
- package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +7 -8
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +35 -7
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +18 -7
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +18 -2
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +7 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +46 -22
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +16 -11
- package/src/util/Util_LambdaEnvBudget.res +74 -0
- package/src/util/Util_LambdaEnvBudget.res.mjs +37 -0
- package/tests/EventCollectorEntryPoint_OpsTest.res +13 -4
- package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +19 -13
- package/tests/Util_LambdaEnvBudgetTest.res +78 -0
- package/tests/Util_LambdaEnvBudgetTest.res.mjs +55 -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.305 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** move the EventCollector's queue-URL maps out of the environment ([31172cb](https://github.com/ReventlessDev/reventless-core/commit/31172cb793d267729f201794ff785f9f982315fa))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.304 (2026-08-16)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** ship the DCB slice registry in the archive, not the environment ([3cda4cb](https://github.com/ReventlessDev/reventless-core/commit/3cda4cb780848598a436ab8e80843cf15a4d22b6))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.303 (2026-08-16)
|
|
7
21
|
|
|
8
22
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.305",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.77",
|
|
20
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
20
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
21
22
|
"@reventlessdev/reventless-core": "3.0.0-alpha.237",
|
|
22
23
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.143",
|
|
23
24
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
24
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
25
25
|
"@reventlessdev/reventless-postgres": "3.0.0-alpha.101",
|
|
26
26
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.115"
|
|
27
27
|
},
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// builds shared DcbEventLog operations and per-slice handlers keyed by command type name.
|
|
4
4
|
// Dual routing: AppSync direct invocation and SQS CommandTopic events.
|
|
5
5
|
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
import * as Chunk from "effect/Chunk";
|
|
7
9
|
import * as Effect from "effect/Effect";
|
|
8
10
|
import * as Stream from "effect/Stream";
|
|
@@ -245,6 +247,16 @@ async function buildHandler() {
|
|
|
245
247
|
// Lambda always sets HANDLER_CONFIG; the production path is unchanged.
|
|
246
248
|
if (!configStr) return [null, null, undefined, undefined];
|
|
247
249
|
const config = JSON.parse(configStr);
|
|
250
|
+
// The slice registry rides in the archive, not the environment — two module
|
|
251
|
+
// specifiers per slice outgrow Lambda's 4KB env-var ceiling (see the asset in
|
|
252
|
+
// StateChangeSliceRuntime_Builder_Single). An inline registry still wins, so a
|
|
253
|
+
// caller that supplies one — every test that drives a config directly — needs
|
|
254
|
+
// no asset on disk. process.cwd() is /var/task, where the archive unpacks.
|
|
255
|
+
if (config.stateChangeSliceModules === undefined) {
|
|
256
|
+
config.stateChangeSliceModules = JSON.parse(
|
|
257
|
+
readFileSync(join(process.cwd(), "sliceModules.json"), "utf-8"),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
248
260
|
return buildHandlersForConfig(config, { loadModule: dynamicImport });
|
|
249
261
|
}
|
|
250
262
|
|
|
@@ -46,12 +46,9 @@
|
|
|
46
46
|
// "readModelNames": string[] // RMs this extension may enqueue events into (filter into readModelQueueUrls)
|
|
47
47
|
// }
|
|
48
48
|
// ],
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// "readModelQueueUrls": { // rmName → env-var name holding the RM's EventCollector SQS URL
|
|
53
|
-
// "ProductDemands": "PRM_ProductDemands_QUEUE_URL"
|
|
54
|
-
// },
|
|
49
|
+
// (publishToAggregates / readModelQueueUrls moved to the queueUrls.json asset —
|
|
50
|
+
// one entry per target, so they outgrew the 4KB environment. Each still maps a
|
|
51
|
+
// target name to the PTA_/PRM_ env var holding its SQS URL.)
|
|
55
52
|
// "readModelNamesForSourceName": { // deploy-side inversion of RM.sourceNames; sourceServiceName → [rmName]
|
|
56
53
|
// "Ordering.Orders": ["ProductDemands"]
|
|
57
54
|
// }
|
|
@@ -100,6 +97,8 @@ async function buildHandler() {
|
|
|
100
97
|
await runtimeExtensionsReady;
|
|
101
98
|
const config = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
|
|
102
99
|
const pluginDefinition = Ops.loadPluginDefinition();
|
|
100
|
+
// target → env-var name; the URLs themselves stay in the environment.
|
|
101
|
+
const queueUrlNames = Ops.loadQueueUrlNames();
|
|
103
102
|
|
|
104
103
|
const queryEngine = Ops.makeQueryEngine(config.pluginReadModelTableName);
|
|
105
104
|
const scheduler = Ops.makeScheduler(config.schedulerRoleArn);
|
|
@@ -165,7 +164,7 @@ async function buildHandler() {
|
|
|
165
164
|
return { aggregateNames: ep.aggregateNames, outgoingHandler: ops.outgoingJsonEventsHandler };
|
|
166
165
|
}));
|
|
167
166
|
|
|
168
|
-
const publishToAggregates = Ops.buildPublishToAggregates(
|
|
167
|
+
const publishToAggregates = Ops.buildPublishToAggregates(queueUrlNames.publishToAggregates);
|
|
169
168
|
const publishToPluginExtensionPoint = Ops.makePublishJsons(config.pluginExtensionPointCmdTopicUrl);
|
|
170
169
|
|
|
171
170
|
// Connect extension — exactly one entry per plugin Lambda; null for admin.
|
|
@@ -230,7 +229,7 @@ async function buildHandler() {
|
|
|
230
229
|
mappings: [transformedMapping],
|
|
231
230
|
};
|
|
232
231
|
|
|
233
|
-
const dicts = Ops.extensionPublishDicts(config, ext);
|
|
232
|
+
const dicts = Ops.extensionPublishDicts(config, queueUrlNames, ext);
|
|
234
233
|
const ops = extensionOperationsMake(epSpec)(mappingsModule)({
|
|
235
234
|
publishToAggregates: dicts.publishToAggregates,
|
|
236
235
|
publishToPluginExtensionPoint,
|
|
@@ -109,9 +109,16 @@ type handlerConfig = {
|
|
|
109
109
|
extensionPoints: array<extensionPointConfig>,
|
|
110
110
|
connectExtension: option<connectExtensionConfig>,
|
|
111
111
|
extensions: array<extensionConfig>,
|
|
112
|
+
readModelNamesForSourceName: dict<array<string>>,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The target→env-var maps that used to ride HANDLER_CONFIG, now read from
|
|
116
|
+
// queueUrls.json (PluginRuntime_Builder). Values are env var NAMES, not URLs —
|
|
117
|
+
// the URLs stay in the environment because the builder cannot resolve those
|
|
118
|
+
// Outputs in bulk.
|
|
119
|
+
type queueUrlNames = {
|
|
112
120
|
publishToAggregates: dict<string>,
|
|
113
121
|
readModelQueueUrls: dict<string>,
|
|
114
|
-
readModelNamesForSourceName: dict<array<string>>,
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
|
|
@@ -149,7 +156,6 @@ let parseHandlerConfig = (rawJson: string): handlerConfig => {
|
|
|
149
156
|
"schedulerQueueName",
|
|
150
157
|
"extensionPoints",
|
|
151
158
|
"extensions",
|
|
152
|
-
"publishToAggregates",
|
|
153
159
|
]->Array.forEach(k =>
|
|
154
160
|
if obj->Dict.get(k)->Option.isNone {
|
|
155
161
|
JsError.throwWithMessage("HANDLER_CONFIG missing required field: " ++ k)
|
|
@@ -225,8 +231,6 @@ let parseHandlerConfig = (rawJson: string): handlerConfig => {
|
|
|
225
231
|
extensionPoints,
|
|
226
232
|
connectExtension,
|
|
227
233
|
extensions,
|
|
228
|
-
publishToAggregates: obj->strDictOf("publishToAggregates"),
|
|
229
|
-
readModelQueueUrls: obj->strDictOf("readModelQueueUrls"),
|
|
230
234
|
readModelNamesForSourceName,
|
|
231
235
|
}
|
|
232
236
|
}
|
|
@@ -625,7 +629,11 @@ type extensionDicts = {
|
|
|
625
629
|
readModelNamesForSourceName: dict<array<string>>,
|
|
626
630
|
}
|
|
627
631
|
|
|
628
|
-
let extensionPublishDicts = (
|
|
632
|
+
let extensionPublishDicts = (
|
|
633
|
+
config: handlerConfig,
|
|
634
|
+
names: queueUrlNames,
|
|
635
|
+
ext: extensionConfig,
|
|
636
|
+
): extensionDicts => {
|
|
629
637
|
let resolve = (names: array<string>, envVarNames: dict<string>) =>
|
|
630
638
|
names
|
|
631
639
|
->Array.filterMap(name =>
|
|
@@ -637,8 +645,8 @@ let extensionPublishDicts = (config: handlerConfig, ext: extensionConfig): exten
|
|
|
637
645
|
)
|
|
638
646
|
->Dict.fromArray
|
|
639
647
|
{
|
|
640
|
-
publishToAggregates: resolve(ext.aggregateNames,
|
|
641
|
-
publishToReadModels: resolve(ext.readModelNames,
|
|
648
|
+
publishToAggregates: resolve(ext.aggregateNames, names.publishToAggregates),
|
|
649
|
+
publishToReadModels: resolve(ext.readModelNames, names.readModelQueueUrls),
|
|
642
650
|
// Scoped to this extension's EP — Extension_Operations uses this dict to
|
|
643
651
|
// decide which RMs receive each incoming event.
|
|
644
652
|
readModelNamesForSourceName: Dict.fromArray([
|
|
@@ -680,6 +688,26 @@ let loadPluginDefinition = (): Reventless.Plugin.pluginDefinition =>
|
|
|
680
688
|
)
|
|
681
689
|
}
|
|
682
690
|
|
|
691
|
+
// The target→env-var maps, read once at cold start. A plugin that publishes
|
|
692
|
+
// nowhere writes empty maps rather than omitting the file, so a read failure is
|
|
693
|
+
// a genuinely broken archive and not an empty-map case worth guessing at.
|
|
694
|
+
let loadQueueUrlNames = (): queueUrlNames =>
|
|
695
|
+
try {
|
|
696
|
+
let obj =
|
|
697
|
+
assetPath("queueUrls.json")
|
|
698
|
+
->NodeFs.readFileSync
|
|
699
|
+
->JSON.parseOrThrow
|
|
700
|
+
->JSON.Decode.object
|
|
701
|
+
->Option.getOr(Dict.make())
|
|
702
|
+
{
|
|
703
|
+
publishToAggregates: obj->strDictOf("publishToAggregates"),
|
|
704
|
+
readModelQueueUrls: obj->strDictOf("readModelQueueUrls"),
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
| exn =>
|
|
708
|
+
JsError.throwWithMessage("Failed to load queueUrls.json from asset zip: " ++ exnMessage(exn))
|
|
709
|
+
}
|
|
710
|
+
|
|
683
711
|
// The plugin's UI-fragment manifest — its own asset since the manifest no
|
|
684
712
|
// longer rides pluginDefinition (the UiFragmentRegistry slice owns fragment
|
|
685
713
|
// state). JSON null (plugins without a UI) and a missing file (older archives)
|
|
@@ -111,8 +111,7 @@ function parseHandlerConfig(rawJson) {
|
|
|
111
111
|
"schedulerQueueArn",
|
|
112
112
|
"schedulerQueueName",
|
|
113
113
|
"extensionPoints",
|
|
114
|
-
"extensions"
|
|
115
|
-
"publishToAggregates"
|
|
114
|
+
"extensions"
|
|
116
115
|
].forEach(k => {
|
|
117
116
|
if (Stdlib_Option.isNone(obj[k])) {
|
|
118
117
|
return Stdlib_JsError.throwWithMessage("HANDLER_CONFIG missing required field: " + k);
|
|
@@ -159,8 +158,6 @@ function parseHandlerConfig(rawJson) {
|
|
|
159
158
|
extensionPoints: extensionPoints,
|
|
160
159
|
connectExtension: connectExtension,
|
|
161
160
|
extensions: extensions,
|
|
162
|
-
publishToAggregates: strDictOf(obj, "publishToAggregates"),
|
|
163
|
-
readModelQueueUrls: strDictOf(obj, "readModelQueueUrls"),
|
|
164
161
|
readModelNamesForSourceName: readModelNamesForSourceName
|
|
165
162
|
};
|
|
166
163
|
}
|
|
@@ -495,14 +492,14 @@ function buildPublishToAggregates(map) {
|
|
|
495
492
|
]));
|
|
496
493
|
}
|
|
497
494
|
|
|
498
|
-
function extensionPublishDicts(config, ext) {
|
|
495
|
+
function extensionPublishDicts(config, names, ext) {
|
|
499
496
|
let resolve = (names, envVarNames) => Object.fromEntries(Stdlib_Array.filterMap(names, name => Stdlib_Option.map(Stdlib_Option.filter(Stdlib_Option.flatMap(envVarNames[name], envVarName => process.env[envVarName]), queueUrl => queueUrl !== ""), queueUrl => [
|
|
500
497
|
name,
|
|
501
498
|
makePublishJsons(queueUrl)
|
|
502
499
|
])));
|
|
503
500
|
return {
|
|
504
|
-
publishToAggregates: resolve(ext.aggregateNames,
|
|
505
|
-
publishToReadModels: resolve(ext.readModelNames,
|
|
501
|
+
publishToAggregates: resolve(ext.aggregateNames, names.publishToAggregates),
|
|
502
|
+
publishToReadModels: resolve(ext.readModelNames, names.readModelQueueUrls),
|
|
506
503
|
readModelNamesForSourceName: Object.fromEntries([[
|
|
507
504
|
ext.extensionPointName,
|
|
508
505
|
Stdlib_Option.getOr(config.readModelNamesForSourceName[ext.extensionPointName], [])
|
|
@@ -529,6 +526,19 @@ function loadPluginDefinition() {
|
|
|
529
526
|
}
|
|
530
527
|
}
|
|
531
528
|
|
|
529
|
+
function loadQueueUrlNames() {
|
|
530
|
+
try {
|
|
531
|
+
let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(assetPath("queueUrls.json"), "utf8"))), {});
|
|
532
|
+
return {
|
|
533
|
+
publishToAggregates: strDictOf(obj, "publishToAggregates"),
|
|
534
|
+
readModelQueueUrls: strDictOf(obj, "readModelQueueUrls")
|
|
535
|
+
};
|
|
536
|
+
} catch (raw_exn) {
|
|
537
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
538
|
+
return Stdlib_JsError.throwWithMessage("Failed to load queueUrls.json from asset zip: " + exnMessage(exn));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
532
542
|
function loadUiFragments() {
|
|
533
543
|
try {
|
|
534
544
|
let json = JSON.parse(Nodefs.readFileSync(assetPath("uiFragments.json"), "utf8"));
|
|
@@ -649,6 +659,7 @@ export {
|
|
|
649
659
|
warnSkippedExtension,
|
|
650
660
|
assetPath,
|
|
651
661
|
loadPluginDefinition,
|
|
662
|
+
loadQueueUrlNames,
|
|
652
663
|
loadUiFragments,
|
|
653
664
|
pushHandler,
|
|
654
665
|
makeSqsHandlerBundle,
|
|
@@ -113,9 +113,16 @@ let forDcbCommandTopic = (
|
|
|
113
113
|
envVars->Dict.set("LOG_LEVEL", level->Pulumi.Output.make->Pulumi.Output.asInput)
|
|
114
114
|
)
|
|
115
115
|
|
|
116
|
-
//
|
|
116
|
+
// The slice registry: an array of {spec, behavior} objects so the entry point
|
|
117
117
|
// can dynamically import both modules and apply the curried StateChangeSlice_Callback.Make
|
|
118
118
|
// functor (Make(Spec)(Behavior)).
|
|
119
|
+
//
|
|
120
|
+
// Shipped as a `sliceModules.json` asset rather than inline in HANDLER_CONFIG.
|
|
121
|
+
// Two absolute module specifiers per slice is the one term here that grows
|
|
122
|
+
// with the plugin, and a plugin with a dozen-odd slices carries enough of
|
|
123
|
+
// them to pass Lambda's 4KB UpdateFunctionConfiguration ceiling on the whole
|
|
124
|
+
// environment — the same wall `pluginDefinition` hit in PluginRuntime_Builder,
|
|
125
|
+
// and it fails at deploy with the registry already committed to the stack.
|
|
119
126
|
let sliceModulesJson =
|
|
120
127
|
slicePaths
|
|
121
128
|
->Array.map(((specPath, behaviorPath)) => {
|
|
@@ -172,7 +179,9 @@ let forDcbCommandTopic = (
|
|
|
172
179
|
Pulumi.Output.all4((dcbTableName, queue.id, pgConnectionFragment, inboundFragment))
|
|
173
180
|
->Pulumi.Output.apply(((table, queueUrl, pgFragment, inbFragment)) => {
|
|
174
181
|
let pluginNameJson = pluginName->JSON.stringifyAny->Option.getOr(`""`)
|
|
175
|
-
`{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson}
|
|
182
|
+
let json = `{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson}${pgFragment}${inbFragment}}`
|
|
183
|
+
Util_LambdaEnvBudget.check(~lambdaName=name, ~handlerConfigJson=json)
|
|
184
|
+
json
|
|
176
185
|
})
|
|
177
186
|
envVars->Dict.set("HANDLER_CONFIG", handlerConfigJson->Pulumi.Output.asInput)
|
|
178
187
|
|
|
@@ -205,9 +214,16 @@ let forDcbCommandTopic = (
|
|
|
205
214
|
Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-core"),
|
|
206
215
|
)
|
|
207
216
|
|
|
217
|
+
// Carries no Pulumi Output, so the archive is still built synchronously —
|
|
218
|
+
// the audit table names that do are in the inbound fragment, which stays in
|
|
219
|
+
// HANDLER_CONFIG.
|
|
220
|
+
let extraStringAssets = Dict.make()
|
|
221
|
+
extraStringAssets->Dict.set("sliceModules.json", `[${sliceModulesJson}]`)
|
|
222
|
+
|
|
208
223
|
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
209
224
|
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs",
|
|
210
225
|
~packageDirs,
|
|
226
|
+
~extraStringAssets,
|
|
211
227
|
)
|
|
212
228
|
|
|
213
229
|
cfg.sqsBatchSize->Option.forEach(CommandTopicChannel.SQS.setBatchSize)
|
|
@@ -11,6 +11,7 @@ import * as DcbBackend$ReventlessAws from "../DcbEventLog/DcbBackend.res.mjs";
|
|
|
11
11
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
12
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
13
13
|
import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
|
|
14
|
+
import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
|
|
14
15
|
import * as CommandTopicChannel_SQS$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS.res.mjs";
|
|
15
16
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
|
|
16
17
|
|
|
@@ -79,7 +80,9 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
|
|
|
79
80
|
inboundFragment
|
|
80
81
|
]).apply(param => {
|
|
81
82
|
let pluginNameJson = Stdlib_Option.getOr(JSON.stringify(pluginName), `""`);
|
|
82
|
-
|
|
83
|
+
let json = `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + param[2] + param[3] + `}`;
|
|
84
|
+
Util_LambdaEnvBudget$ReventlessAws.check(name, json, undefined);
|
|
85
|
+
return json;
|
|
83
86
|
});
|
|
84
87
|
envVars["HANDLER_CONFIG"] = handlerConfigJson;
|
|
85
88
|
let packageDirs = {};
|
|
@@ -97,7 +100,9 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
|
|
|
97
100
|
});
|
|
98
101
|
packageDirs["@reventlessdev/reventless-aws"] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws");
|
|
99
102
|
packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-core");
|
|
100
|
-
let
|
|
103
|
+
let extraStringAssets = {};
|
|
104
|
+
extraStringAssets["sliceModules.json"] = `[` + sliceModulesJson + `]`;
|
|
105
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs", packageDirs, extraStringAssets, undefined);
|
|
101
106
|
Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS$ReventlessAws.setBatchSize);
|
|
102
107
|
let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({
|
|
103
108
|
subnetIds: pgSelection.subnetIds,
|
|
@@ -503,27 +503,13 @@ module Make = (
|
|
|
503
503
|
})
|
|
504
504
|
dict->Dict.set("extensions", JSON.Encode.array(extensionsArr))
|
|
505
505
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
)
|
|
514
|
-
)
|
|
515
|
-
dict->Dict.set("publishToAggregates", JSON.Encode.object(publishToAggregatesDict))
|
|
516
|
-
|
|
517
|
-
let readModelQueueUrlsDict = Dict.make()
|
|
518
|
-
context.readModelQueueUrls
|
|
519
|
-
->Dict.keysToArray
|
|
520
|
-
->Array.forEach(rmName =>
|
|
521
|
-
readModelQueueUrlsDict->Dict.set(
|
|
522
|
-
rmName,
|
|
523
|
-
JSON.Encode.string(`PRM_${rmName}_QUEUE_URL`),
|
|
524
|
-
)
|
|
525
|
-
)
|
|
526
|
-
dict->Dict.set("readModelQueueUrls", JSON.Encode.object(readModelQueueUrlsDict))
|
|
506
|
+
// publishToAggregates / readModelQueueUrls live in queueUrls.json (see the
|
|
507
|
+
// asset bundle below). Both are one entry per target, so they grow with the
|
|
508
|
+
// plugin, and together with the PTA_/PRM_ vars they name they carried the
|
|
509
|
+
// catalog EventCollector past Lambda's 4KB environment ceiling. Only the
|
|
510
|
+
// maps move: they are built from Dict.keysToArray, so they hold no Pulumi
|
|
511
|
+
// Output and can be serialized here. The URLs themselves cannot follow —
|
|
512
|
+
// see the note on the aggregate walk further down.
|
|
527
513
|
|
|
528
514
|
let rmNamesForSourceDict = Dict.make()
|
|
529
515
|
context.readModelNamesForSourceName
|
|
@@ -536,7 +522,22 @@ module Make = (
|
|
|
536
522
|
)
|
|
537
523
|
dict->Dict.set("readModelNamesForSourceName", JSON.Encode.object(rmNamesForSourceDict))
|
|
538
524
|
|
|
539
|
-
JSON.Encode.object(dict)->JSON.stringify
|
|
525
|
+
let json = JSON.Encode.object(dict)->JSON.stringify
|
|
526
|
+
// The PTA_/PRM_ vars are the rest of this Lambda's environment and the
|
|
527
|
+
// half that grows with the plugin. Their names are known here; their
|
|
528
|
+
// values are still Outputs, so the budget check estimates those.
|
|
529
|
+
let queueUrlKeys = Array.concat(
|
|
530
|
+
context.publishToAggregates
|
|
531
|
+
->Dict.keysToArray
|
|
532
|
+
->Array.map(n => `PTA_${n}_QUEUE_URL`),
|
|
533
|
+
context.readModelQueueUrls->Dict.keysToArray->Array.map(n => `PRM_${n}_QUEUE_URL`),
|
|
534
|
+
)
|
|
535
|
+
Util_LambdaEnvBudget.check(
|
|
536
|
+
~lambdaName=name,
|
|
537
|
+
~handlerConfigJson=json,
|
|
538
|
+
~outputValuedKeys=queueUrlKeys,
|
|
539
|
+
)
|
|
540
|
+
json
|
|
540
541
|
})
|
|
541
542
|
envVars->Dict.set("HANDLER_CONFIG", handlerConfigJson->Pulumi.Output.asInput)
|
|
542
543
|
|
|
@@ -599,6 +600,29 @@ module Make = (
|
|
|
599
600
|
->Pulumi.Output.apply(((pluginDefinitionJson, uiFragmentsJson)) => {
|
|
600
601
|
let extraStringAssets = Dict.make()
|
|
601
602
|
extraStringAssets->Dict.set("pluginDefinition.json", pluginDefinitionJson)
|
|
603
|
+
// target name → the env var holding that target's queue URL. The
|
|
604
|
+
// indirection stays (the URLs are Outputs this builder must not resolve
|
|
605
|
+
// in bulk); only the naming map moves out of HANDLER_CONFIG.
|
|
606
|
+
let nameMap = (names: array<string>, prefix: string) => {
|
|
607
|
+
let d = Dict.make()
|
|
608
|
+
names
|
|
609
|
+
->Array.toSorted(String.compare)
|
|
610
|
+
->Array.forEach(n => d->Dict.set(n, JSON.Encode.string(`${prefix}_${n}_QUEUE_URL`)))
|
|
611
|
+
JSON.Encode.object(d)
|
|
612
|
+
}
|
|
613
|
+
let queueUrlsDict = Dict.make()
|
|
614
|
+
queueUrlsDict->Dict.set(
|
|
615
|
+
"publishToAggregates",
|
|
616
|
+
nameMap(context.publishToAggregates->Dict.keysToArray, "PTA"),
|
|
617
|
+
)
|
|
618
|
+
queueUrlsDict->Dict.set(
|
|
619
|
+
"readModelQueueUrls",
|
|
620
|
+
nameMap(context.readModelQueueUrls->Dict.keysToArray, "PRM"),
|
|
621
|
+
)
|
|
622
|
+
extraStringAssets->Dict.set(
|
|
623
|
+
"queueUrls.json",
|
|
624
|
+
JSON.Encode.object(queueUrlsDict)->JSON.stringify,
|
|
625
|
+
)
|
|
602
626
|
// The UI-fragment manifest no longer rides pluginDefinition — ship it as
|
|
603
627
|
// its own asset ("null" for plugins without a UI) so the bundled Connect
|
|
604
628
|
// extension can emit RegisterUiFragment in the handshake answer.
|
|
@@ -16,6 +16,7 @@ import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/sr
|
|
|
16
16
|
import * as EventCollector$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventCollector/EventCollector.res.mjs";
|
|
17
17
|
import * as EventLogBackend$ReventlessAws from "../../adapter/EventLog/EventLogBackend.res.mjs";
|
|
18
18
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
19
|
+
import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
|
|
19
20
|
import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
|
|
20
21
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
21
22
|
import * as StateChangeSliceRuntime_Builder_Single$ReventlessAws from "../../adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs";
|
|
@@ -323,22 +324,15 @@ function Make(EventCollectorChannel) {
|
|
|
323
324
|
return entryDict;
|
|
324
325
|
});
|
|
325
326
|
dict["extensions"] = extensionsArr;
|
|
326
|
-
let publishToAggregatesDict = {};
|
|
327
|
-
Object.keys(context.publishToAggregates).forEach(aggName => {
|
|
328
|
-
publishToAggregatesDict[aggName] = `PTA_` + aggName + `_QUEUE_URL`;
|
|
329
|
-
});
|
|
330
|
-
dict["publishToAggregates"] = publishToAggregatesDict;
|
|
331
|
-
let readModelQueueUrlsDict = {};
|
|
332
|
-
Object.keys(context.readModelQueueUrls).forEach(rmName => {
|
|
333
|
-
readModelQueueUrlsDict[rmName] = `PRM_` + rmName + `_QUEUE_URL`;
|
|
334
|
-
});
|
|
335
|
-
dict["readModelQueueUrls"] = readModelQueueUrlsDict;
|
|
336
327
|
let rmNamesForSourceDict = {};
|
|
337
328
|
Object.entries(context.readModelNamesForSourceName).forEach(param => {
|
|
338
329
|
rmNamesForSourceDict[param[0]] = param[1].map(prim => prim);
|
|
339
330
|
});
|
|
340
331
|
dict["readModelNamesForSourceName"] = rmNamesForSourceDict;
|
|
341
|
-
|
|
332
|
+
let json = JSON.stringify(dict);
|
|
333
|
+
let queueUrlKeys = Object.keys(context.publishToAggregates).map(n => `PTA_` + n + `_QUEUE_URL`).concat(Object.keys(context.readModelQueueUrls).map(n => `PRM_` + n + `_QUEUE_URL`));
|
|
334
|
+
Util_LambdaEnvBudget$ReventlessAws.check(name, json, queueUrlKeys);
|
|
335
|
+
return json;
|
|
342
336
|
});
|
|
343
337
|
envVars["HANDLER_CONFIG"] = handlerConfigJson;
|
|
344
338
|
let packageDirs = {};
|
|
@@ -374,6 +368,17 @@ function Make(EventCollectorChannel) {
|
|
|
374
368
|
]).apply(param => {
|
|
375
369
|
let extraStringAssets = {};
|
|
376
370
|
extraStringAssets["pluginDefinition.json"] = param[0];
|
|
371
|
+
let nameMap = (names, prefix) => {
|
|
372
|
+
let d = {};
|
|
373
|
+
names.toSorted(Primitive_string.compare).forEach(n => {
|
|
374
|
+
d[n] = prefix + `_` + n + `_QUEUE_URL`;
|
|
375
|
+
});
|
|
376
|
+
return d;
|
|
377
|
+
};
|
|
378
|
+
let queueUrlsDict = {};
|
|
379
|
+
queueUrlsDict["publishToAggregates"] = nameMap(Object.keys(context.publishToAggregates), "PTA");
|
|
380
|
+
queueUrlsDict["readModelQueueUrls"] = nameMap(Object.keys(context.readModelQueueUrls), "PRM");
|
|
381
|
+
extraStringAssets["queueUrls.json"] = JSON.stringify(queueUrlsDict);
|
|
377
382
|
extraStringAssets["uiFragments.json"] = param[1];
|
|
378
383
|
return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/EventCollectorEntryPoint.mjs", packageDirs, extraStringAssets, undefined);
|
|
379
384
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Deploy-time check on a Lambda's environment size.
|
|
2
|
+
//
|
|
3
|
+
// AWS caps the whole environment of a function at 4096 bytes and rejects the
|
|
4
|
+
// UpdateFunctionConfiguration call that exceeds it. That rejection lands in the
|
|
5
|
+
// middle of a `pulumi up`, after the surrounding resources have already moved,
|
|
6
|
+
// so the stack is left half-updated and the message names a byte count rather
|
|
7
|
+
// than the field that grew. This has now bitten three times: `pluginDefinition`
|
|
8
|
+
// inline in HANDLER_CONFIG, the DCB slice registry, and the EventCollector's
|
|
9
|
+
// publishToAggregates map plus the PTA_ vars it names.
|
|
10
|
+
//
|
|
11
|
+
// The check runs where HANDLER_CONFIG is still a plain string — inside the
|
|
12
|
+
// builders' `Pulumi.Output.apply` — because that is the last point anything is
|
|
13
|
+
// measurable. By the time the env reaches `RuntimeEnvironment_Lambda`, the big
|
|
14
|
+
// values are Outputs, and totalling them there would mean a bulk resolve of the
|
|
15
|
+
// very dict `PluginRuntime_Builder` documents as unsafe to resolve in bulk.
|
|
16
|
+
//
|
|
17
|
+
// So the total is part exact, part estimated:
|
|
18
|
+
// * exact — the fixed framework vars, HANDLER_CONFIG, and every env var NAME
|
|
19
|
+
// * estimated — one allowance per Output-valued env var, since the value (an SQS
|
|
20
|
+
// URL) is not resolved yet
|
|
21
|
+
//
|
|
22
|
+
// Exceeding the limit on the exact part alone is a certainty, and fails the
|
|
23
|
+
// deploy. Exceeding it only once the allowances are added is a likelihood, and
|
|
24
|
+
// warns: a wrong estimate must not block a deploy that would have succeeded.
|
|
25
|
+
|
|
26
|
+
let limit = 4096
|
|
27
|
+
|
|
28
|
+
// Environment, NODE_OPTIONS, ESM_FALLBACK_DIRS, LOG_LEVEL,
|
|
29
|
+
// REVENTLESS_ELEVATED_GROUPS — set for every Lambda by RuntimeEnvironment_Lambda.
|
|
30
|
+
let frameworkVarsBytes = 191
|
|
31
|
+
|
|
32
|
+
// An SQS queue URL: https://sqs.<region>.amazonaws.com/<account>/<name>[.fifo].
|
|
33
|
+
// Rounded up from the longest in the example platforms, so the estimate errs
|
|
34
|
+
// toward warning early rather than reporting a limit breach as headroom.
|
|
35
|
+
let outputValueAllowance = 100
|
|
36
|
+
|
|
37
|
+
let log = ReventlessCore.Logger.fromEnv()
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check one Lambda's environment against the 4KB limit.
|
|
41
|
+
*
|
|
42
|
+
* `~outputValuedKeys` are env vars whose value is still an unresolved Output —
|
|
43
|
+
* counted by name plus `outputValueAllowance`. Raises when the exact portion
|
|
44
|
+
* alone is over the limit; warns when only the estimate is.
|
|
45
|
+
*/
|
|
46
|
+
let check = (
|
|
47
|
+
~lambdaName: string,
|
|
48
|
+
~handlerConfigJson: string,
|
|
49
|
+
~outputValuedKeys: array<string>=[],
|
|
50
|
+
) => {
|
|
51
|
+
let handlerConfigBytes = String.length("HANDLER_CONFIG") + String.length(handlerConfigJson)
|
|
52
|
+
let keyBytes = outputValuedKeys->Array.reduce(0, (acc, k) => acc + String.length(k))
|
|
53
|
+
let exact = frameworkVarsBytes + handlerConfigBytes + keyBytes
|
|
54
|
+
let estimated = exact + outputValuedKeys->Array.length * outputValueAllowance
|
|
55
|
+
|
|
56
|
+
if exact > limit {
|
|
57
|
+
JsError.throwWithMessage(
|
|
58
|
+
`${lambdaName}: Lambda environment is ${exact->Int.toString} bytes before the ` ++
|
|
59
|
+
`${outputValuedKeys->Array.length->Int.toString} queue-URL vars are resolved, over the ` ++
|
|
60
|
+
`${limit->Int.toString}-byte limit. HANDLER_CONFIG is ${handlerConfigBytes->Int.toString} ` ++
|
|
61
|
+
`bytes of it — move the term that grows with the plugin into a code-archive asset ` ++
|
|
62
|
+
`(Util_Bundle.buildCodeArchive ~extraStringAssets), as pluginDefinition.json and ` ++
|
|
63
|
+
`sliceModules.json already are.`,
|
|
64
|
+
)
|
|
65
|
+
} else if estimated > limit {
|
|
66
|
+
log.warn(
|
|
67
|
+
~comp="Util_LambdaEnvBudget",
|
|
68
|
+
`${lambdaName}: Lambda environment is about ${estimated->Int.toString} bytes ` ++
|
|
69
|
+
`(${exact->Int.toString} exact + ${outputValuedKeys->Array.length->Int.toString} queue-URL ` ++
|
|
70
|
+
`vars), at or over the ${limit->Int.toString}-byte limit. The deploy may fail on ` ++
|
|
71
|
+
`UpdateFunctionConfiguration; move a plugin-sized term out of HANDLER_CONFIG.`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
|
+
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
6
|
+
|
|
7
|
+
let log = Logger$ReventlessCore.fromEnv();
|
|
8
|
+
|
|
9
|
+
function check(lambdaName, handlerConfigJson, outputValuedKeysOpt) {
|
|
10
|
+
let outputValuedKeys = outputValuedKeysOpt !== undefined ? outputValuedKeysOpt : [];
|
|
11
|
+
let handlerConfigBytes = "HANDLER_CONFIG".length + handlerConfigJson.length | 0;
|
|
12
|
+
let keyBytes = Stdlib_Array.reduce(outputValuedKeys, 0, (acc, k) => acc + k.length | 0);
|
|
13
|
+
let exact = (191 + handlerConfigBytes | 0) + keyBytes | 0;
|
|
14
|
+
let estimated = exact + (outputValuedKeys.length * 100 | 0) | 0;
|
|
15
|
+
if (exact > 4096) {
|
|
16
|
+
return Stdlib_JsError.throwWithMessage(lambdaName + `: Lambda environment is ` + exact.toString() + ` bytes before the ` + (outputValuedKeys.length.toString() + ` queue-URL vars are resolved, over the `) + ((4096).toString() + `-byte limit. HANDLER_CONFIG is ` + handlerConfigBytes.toString() + ` `) + `bytes of it — move the term that grows with the plugin into a code-archive asset (Util_Bundle.buildCodeArchive ~extraStringAssets), as pluginDefinition.json and sliceModules.json already are.`);
|
|
17
|
+
} else if (estimated > 4096) {
|
|
18
|
+
return log.warn("Util_LambdaEnvBudget", undefined, lambdaName + `: Lambda environment is about ` + estimated.toString() + ` bytes ` + (`(` + exact.toString() + ` exact + ` + outputValuedKeys.length.toString() + ` queue-URL `) + (`vars), at or over the ` + (4096).toString() + `-byte limit. The deploy may fail on `) + `UpdateFunctionConfiguration; move a plugin-sized term out of HANDLER_CONFIG.`);
|
|
19
|
+
} else {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let limit = 4096;
|
|
25
|
+
|
|
26
|
+
let frameworkVarsBytes = 191;
|
|
27
|
+
|
|
28
|
+
let outputValueAllowance = 100;
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
limit,
|
|
32
|
+
frameworkVarsBytes,
|
|
33
|
+
outputValueAllowance,
|
|
34
|
+
log,
|
|
35
|
+
check,
|
|
36
|
+
}
|
|
37
|
+
/* log Not a pure module */
|
|
@@ -107,7 +107,6 @@ let minimalConfig = (extras: array<(string, JSON.t)>): string =>
|
|
|
107
107
|
("schedulerQueueName", str("")),
|
|
108
108
|
("extensionPoints", JSON.Encode.array([])),
|
|
109
109
|
("extensions", JSON.Encode.array([])),
|
|
110
|
-
("publishToAggregates", obj([])),
|
|
111
110
|
],
|
|
112
111
|
extras,
|
|
113
112
|
)
|
|
@@ -139,7 +138,6 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
139
138
|
let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(minimalConfig([]))
|
|
140
139
|
expect(c.queueUrl)->toBe("q-url")
|
|
141
140
|
expect(c.connectExtension->Option.isNone)->toBe(true) // absent → None
|
|
142
|
-
expect(c.readModelQueueUrls->Dict.keysToArray->Array.length)->toBe(0)
|
|
143
141
|
expect(c.readModelNamesForSourceName->Dict.keysToArray->Array.length)->toBe(0)
|
|
144
142
|
})
|
|
145
143
|
|
|
@@ -198,7 +196,6 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
198
196
|
]),
|
|
199
197
|
]),
|
|
200
198
|
),
|
|
201
|
-
("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
|
|
202
199
|
(
|
|
203
200
|
"readModelNamesForSourceName",
|
|
204
201
|
obj([("Ordering.Orders", JSON.Encode.array([str("ProductDemands")]))]),
|
|
@@ -208,9 +205,21 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
208
205
|
let ep = c.extensionPoints->Array.getUnsafe(0)
|
|
209
206
|
expect(ep.eventTopicArn)->toBe("arn:ep")
|
|
210
207
|
expect(ep.aggregateNames)->toEqual(["Plugin"])
|
|
211
|
-
expect(c.publishToAggregates->Dict.get("Plugin"))->toEqual(Some("PTA_Plugin_QUEUE_URL"))
|
|
212
208
|
expect(c.readModelNamesForSourceName->Dict.get("Ordering.Orders"))->toEqual(
|
|
213
209
|
Some(["ProductDemands"]),
|
|
214
210
|
)
|
|
215
211
|
})
|
|
212
|
+
|
|
213
|
+
// The target→env-var maps left HANDLER_CONFIG for queueUrls.json, so a config
|
|
214
|
+
// still carrying them must parse rather than trip the required-field check —
|
|
215
|
+
// that is what a Lambda running an older archive sends.
|
|
216
|
+
testSync("ignores publishToAggregates left over in HANDLER_CONFIG", () => {
|
|
217
|
+
let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(
|
|
218
|
+
minimalConfig([
|
|
219
|
+
("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
|
|
220
|
+
("readModelQueueUrls", obj([("ProductDemands", str("PRM_ProductDemands_QUEUE_URL"))])),
|
|
221
|
+
]),
|
|
222
|
+
)
|
|
223
|
+
expect(c.queueUrl)->toBe("q-url")
|
|
224
|
+
})
|
|
216
225
|
})
|
|
@@ -210,10 +210,6 @@ function minimalConfig(extras) {
|
|
|
210
210
|
[
|
|
211
211
|
"extensions",
|
|
212
212
|
[]
|
|
213
|
-
],
|
|
214
|
-
[
|
|
215
|
-
"publishToAggregates",
|
|
216
|
-
Object.fromEntries([])
|
|
217
213
|
]
|
|
218
214
|
].concat(extras)));
|
|
219
215
|
}
|
|
@@ -238,7 +234,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
238
234
|
let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([]));
|
|
239
235
|
globalThis.expect(c.queueUrl).toBe("q-url");
|
|
240
236
|
globalThis.expect(Stdlib_Option.isNone(c.connectExtension)).toBe(true);
|
|
241
|
-
globalThis.expect(Object.keys(c.readModelQueueUrls).length).toBe(0);
|
|
242
237
|
globalThis.expect(Object.keys(c.readModelNamesForSourceName).length).toBe(0);
|
|
243
238
|
});
|
|
244
239
|
globalThis.test("connectExtension: JSON null (admin) maps to None; a record decodes", () => {
|
|
@@ -304,13 +299,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
304
299
|
]
|
|
305
300
|
])]
|
|
306
301
|
],
|
|
307
|
-
[
|
|
308
|
-
"publishToAggregates",
|
|
309
|
-
Object.fromEntries([[
|
|
310
|
-
"Plugin",
|
|
311
|
-
"PTA_Plugin_QUEUE_URL"
|
|
312
|
-
]])
|
|
313
|
-
],
|
|
314
302
|
[
|
|
315
303
|
"readModelNamesForSourceName",
|
|
316
304
|
Object.fromEntries([[
|
|
@@ -322,9 +310,27 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
322
310
|
let ep = c.extensionPoints[0];
|
|
323
311
|
globalThis.expect(ep.eventTopicArn).toBe("arn:ep");
|
|
324
312
|
globalThis.expect(ep.aggregateNames).toEqual(["Plugin"]);
|
|
325
|
-
globalThis.expect(c.publishToAggregates["Plugin"]).toEqual("PTA_Plugin_QUEUE_URL");
|
|
326
313
|
globalThis.expect(c.readModelNamesForSourceName["Ordering.Orders"]).toEqual(["ProductDemands"]);
|
|
327
314
|
});
|
|
315
|
+
globalThis.test("ignores publishToAggregates left over in HANDLER_CONFIG", () => {
|
|
316
|
+
let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([
|
|
317
|
+
[
|
|
318
|
+
"publishToAggregates",
|
|
319
|
+
Object.fromEntries([[
|
|
320
|
+
"Plugin",
|
|
321
|
+
"PTA_Plugin_QUEUE_URL"
|
|
322
|
+
]])
|
|
323
|
+
],
|
|
324
|
+
[
|
|
325
|
+
"readModelQueueUrls",
|
|
326
|
+
Object.fromEntries([[
|
|
327
|
+
"ProductDemands",
|
|
328
|
+
"PRM_ProductDemands_QUEUE_URL"
|
|
329
|
+
]])
|
|
330
|
+
]
|
|
331
|
+
]));
|
|
332
|
+
globalThis.expect(c.queueUrl).toBe("q-url");
|
|
333
|
+
});
|
|
328
334
|
});
|
|
329
335
|
|
|
330
336
|
let project = EventCollectorEntryPoint_Ops$ReventlessAws.projectPluginRow;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Guards the deploy-time check on a Lambda's 4KB environment.
|
|
2
|
+
//
|
|
3
|
+
// The check exists because the limit is enforced by AWS mid-`pulumi up`, on a
|
|
4
|
+
// call that names a byte count and not the field that grew. The two cases worth
|
|
5
|
+
// pinning are the ones that decide whether a deploy proceeds: an exact total
|
|
6
|
+
// already over the limit must stop it, and an over-limit total that only appears
|
|
7
|
+
// once the unresolved queue-URL vars are estimated must not — a wrong estimate
|
|
8
|
+
// blocking a good deploy would be worse than the failure it prevents.
|
|
9
|
+
|
|
10
|
+
open JestGlobals
|
|
11
|
+
|
|
12
|
+
let budget = Util_LambdaEnvBudget.limit
|
|
13
|
+
|
|
14
|
+
let throws = (f: unit => 'a): option<string> =>
|
|
15
|
+
try {
|
|
16
|
+
let _ = f()
|
|
17
|
+
None
|
|
18
|
+
} catch {
|
|
19
|
+
| exn => exn->JsExn.fromException->Option.flatMap(JsExn.message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("Util_LambdaEnvBudget.check", () => {
|
|
23
|
+
testSync("passes a small environment", () => {
|
|
24
|
+
let msg = throws(() =>
|
|
25
|
+
Util_LambdaEnvBudget.check(~lambdaName="Small", ~handlerConfigJson=`{"queueUrl":"q"}`)
|
|
26
|
+
)
|
|
27
|
+
expect(msg)->toEqual(None)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
testSync("throws when HANDLER_CONFIG alone exceeds the limit", () => {
|
|
31
|
+
let big = `{"x":"` ++ String.repeat("a", budget) ++ `"}`
|
|
32
|
+
let msg = throws(() => Util_LambdaEnvBudget.check(~lambdaName="TooBig", ~handlerConfigJson=big))
|
|
33
|
+
expect(msg->Option.isSome)->toBe(true)
|
|
34
|
+
// Names the Lambda and points at the fix, not just the byte count.
|
|
35
|
+
expect(msg->Option.mapOr(false, m => m->String.includes("TooBig")))->toBe(true)
|
|
36
|
+
expect(msg->Option.mapOr(false, m => m->String.includes("extraStringAssets")))->toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
testSync("counts the queue-URL var names toward the exact total", () => {
|
|
40
|
+
// Sized so HANDLER_CONFIG alone fits and the var NAMES alone push it over —
|
|
41
|
+
// the names are known at check time, so this must throw rather than warn.
|
|
42
|
+
let keys = Array.make(~length=40, "")->Array.mapWithIndex((_, i) =>
|
|
43
|
+
`PTA_AVeryLongAggregateNameNumber${i->Int.toString}_QUEUE_URL`
|
|
44
|
+
)
|
|
45
|
+
let keyBytes = keys->Array.reduce(0, (acc, k) => acc + String.length(k))
|
|
46
|
+
let fixed = Util_LambdaEnvBudget.frameworkVarsBytes + String.length("HANDLER_CONFIG")
|
|
47
|
+
// Just past the limit on the exact total alone — the names are enough.
|
|
48
|
+
let json = String.repeat("a", budget - fixed - keyBytes + 1)
|
|
49
|
+
let msg = throws(() =>
|
|
50
|
+
Util_LambdaEnvBudget.check(
|
|
51
|
+
~lambdaName="ManyTargets",
|
|
52
|
+
~handlerConfigJson=json,
|
|
53
|
+
~outputValuedKeys=keys,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
expect(msg->Option.isSome)->toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
testSync("only estimating over the limit warns rather than throwing", () => {
|
|
60
|
+
// Exact total sits under the limit; it is the per-URL allowance that crosses
|
|
61
|
+
// it. The deploy must be allowed to proceed and find out.
|
|
62
|
+
let keys = Array.make(~length=20, "")->Array.mapWithIndex((_, i) => `PTA_A${i->Int.toString}_URL`)
|
|
63
|
+
let exact =
|
|
64
|
+
Util_LambdaEnvBudget.frameworkVarsBytes +
|
|
65
|
+
String.length("HANDLER_CONFIG") +
|
|
66
|
+
keys->Array.reduce(0, (acc, k) => acc + String.length(k))
|
|
67
|
+
let room = budget - exact - 100
|
|
68
|
+
let json = String.repeat("a", room)
|
|
69
|
+
let msg = throws(() =>
|
|
70
|
+
Util_LambdaEnvBudget.check(
|
|
71
|
+
~lambdaName="Estimated",
|
|
72
|
+
~handlerConfigJson=json,
|
|
73
|
+
~outputValuedKeys=keys,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
expect(msg)->toEqual(None)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
|
+
import * as Util_LambdaEnvBudget$ReventlessAws from "../src/util/Util_LambdaEnvBudget.res.mjs";
|
|
8
|
+
|
|
9
|
+
function throws(f) {
|
|
10
|
+
try {
|
|
11
|
+
f();
|
|
12
|
+
return;
|
|
13
|
+
} catch (raw_exn) {
|
|
14
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
15
|
+
return Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
globalThis.describe("Util_LambdaEnvBudget.check", () => {
|
|
20
|
+
globalThis.test("passes a small environment", () => {
|
|
21
|
+
let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("Small", `{"queueUrl":"q"}`, undefined));
|
|
22
|
+
globalThis.expect(msg).toEqual(undefined);
|
|
23
|
+
});
|
|
24
|
+
globalThis.test("throws when HANDLER_CONFIG alone exceeds the limit", () => {
|
|
25
|
+
let big = `{"x":"` + "a".repeat(Util_LambdaEnvBudget$ReventlessAws.limit) + `"}`;
|
|
26
|
+
let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("TooBig", big, undefined));
|
|
27
|
+
globalThis.expect(Stdlib_Option.isSome(msg)).toBe(true);
|
|
28
|
+
globalThis.expect(Stdlib_Option.mapOr(msg, false, m => m.includes("TooBig"))).toBe(true);
|
|
29
|
+
globalThis.expect(Stdlib_Option.mapOr(msg, false, m => m.includes("extraStringAssets"))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
globalThis.test("counts the queue-URL var names toward the exact total", () => {
|
|
32
|
+
let keys = Stdlib_Array.make(40, "").map((param, i) => `PTA_AVeryLongAggregateNameNumber` + i.toString() + `_QUEUE_URL`);
|
|
33
|
+
let keyBytes = Stdlib_Array.reduce(keys, 0, (acc, k) => acc + k.length | 0);
|
|
34
|
+
let fixed = Util_LambdaEnvBudget$ReventlessAws.frameworkVarsBytes + "HANDLER_CONFIG".length | 0;
|
|
35
|
+
let json = "a".repeat(((Util_LambdaEnvBudget$ReventlessAws.limit - fixed | 0) - keyBytes | 0) + 1 | 0);
|
|
36
|
+
let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("ManyTargets", json, keys));
|
|
37
|
+
globalThis.expect(Stdlib_Option.isSome(msg)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
globalThis.test("only estimating over the limit warns rather than throwing", () => {
|
|
40
|
+
let keys = Stdlib_Array.make(20, "").map((param, i) => `PTA_A` + i.toString() + `_URL`);
|
|
41
|
+
let exact = (Util_LambdaEnvBudget$ReventlessAws.frameworkVarsBytes + "HANDLER_CONFIG".length | 0) + Stdlib_Array.reduce(keys, 0, (acc, k) => acc + k.length | 0) | 0;
|
|
42
|
+
let room = (Util_LambdaEnvBudget$ReventlessAws.limit - exact | 0) - 100 | 0;
|
|
43
|
+
let json = "a".repeat(room);
|
|
44
|
+
let msg = throws(() => Util_LambdaEnvBudget$ReventlessAws.check("Estimated", json, keys));
|
|
45
|
+
globalThis.expect(msg).toEqual(undefined);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let budget = Util_LambdaEnvBudget$ReventlessAws.limit;
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
budget,
|
|
53
|
+
throws,
|
|
54
|
+
}
|
|
55
|
+
/* Not a pure module */
|