@reventlessdev/reventless-seed-aws 1.0.0-alpha.12 → 1.0.0-alpha.13
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 +7 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/ReventlessSeedAws_Quiesce.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.res +287 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.res.mjs +205 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.res +2 -6
- package/lib/bs/src/ReventlessSeedAws_Reconcile.res.mjs +2 -2
- package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res +223 -73
- package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +212 -82
- package/lib/bs/tests/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
- package/lib/bs/tests/QuiesceResetTest.ast +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmi +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmj +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmt +0 -0
- package/lib/bs/tests/QuiesceResetTest.res +133 -0
- package/lib/bs/tests/QuiesceResetTest.res.mjs +141 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
- package/lib/ocaml/QuiesceResetTest.ast +0 -0
- package/lib/ocaml/QuiesceResetTest.cmi +0 -0
- package/lib/ocaml/QuiesceResetTest.cmj +0 -0
- package/lib/ocaml/QuiesceResetTest.cmt +0 -0
- package/lib/ocaml/QuiesceResetTest.res +133 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.res +287 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.res +2 -6
- package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.res +223 -73
- package/package.json +2 -2
- package/src/ReventlessSeedAws_Quiesce.res +287 -0
- package/src/ReventlessSeedAws_Quiesce.res.mjs +205 -0
- package/src/ReventlessSeedAws_Reconcile.res +2 -6
- package/src/ReventlessSeedAws_Reconcile.res.mjs +2 -2
- package/src/ReventlessSeedAws_Reset.res +223 -73
- package/src/ReventlessSeedAws_Reset.res.mjs +212 -82
- package/tests/QuiesceResetTest.res +133 -0
- package/tests/QuiesceResetTest.res.mjs +141 -0
|
@@ -4,6 +4,7 @@ import * as Nodefs from "node:fs";
|
|
|
4
4
|
import * as S3$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/S3.res.mjs";
|
|
5
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
6
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
7
|
+
import * as Lambda$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/Lambda.res.mjs";
|
|
7
8
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
9
|
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
9
10
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
@@ -15,6 +16,7 @@ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_excep
|
|
|
15
16
|
import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
|
|
16
17
|
import * as ClientDynamodb from "@aws-sdk/client-dynamodb";
|
|
17
18
|
import * as DynamoDb_DynamoDb$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DynamoDb.res.mjs";
|
|
19
|
+
import * as ReventlessSeedAws_Quiesce from "./ReventlessSeedAws_Quiesce.res.mjs";
|
|
18
20
|
import * as Seed_Prompt$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Prompt.res.mjs";
|
|
19
21
|
import * as Seed_Runner$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Runner.res.mjs";
|
|
20
22
|
import * as DynamoDb_DocumentClient$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DocumentClient.res.mjs";
|
|
@@ -110,25 +112,52 @@ function configValue(cfg, key) {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
function classify(arn) {
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
TAG: "Bucket",
|
|
117
|
-
_0: arn.slice("arn:aws:s3:::".length, arn.length)
|
|
118
|
-
};
|
|
119
|
-
} else {
|
|
115
|
+
if (arn.startsWith("arn:aws:dynamodb:")) {
|
|
116
|
+
let match = arn.split(":table/");
|
|
117
|
+
if (match.length !== 2) {
|
|
120
118
|
return "Other";
|
|
121
119
|
}
|
|
120
|
+
let name = match[1];
|
|
121
|
+
return {
|
|
122
|
+
TAG: "Table",
|
|
123
|
+
_0: name
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (arn.startsWith("arn:aws:s3:::")) {
|
|
127
|
+
return {
|
|
128
|
+
TAG: "Bucket",
|
|
129
|
+
_0: arn.slice("arn:aws:s3:::".length, arn.length)
|
|
130
|
+
};
|
|
122
131
|
}
|
|
123
|
-
|
|
124
|
-
if (match.length !== 2) {
|
|
132
|
+
if (!arn.startsWith("arn:aws:lambda:")) {
|
|
125
133
|
return "Other";
|
|
126
134
|
}
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
let match$1 = arn.split(":function:");
|
|
136
|
+
if (match$1.length !== 2) {
|
|
137
|
+
return "Other";
|
|
138
|
+
}
|
|
139
|
+
let rest = match$1[1];
|
|
140
|
+
let match$2 = rest.split(":");
|
|
141
|
+
let len = match$2.length;
|
|
142
|
+
if (len >= 3) {
|
|
143
|
+
return "Other";
|
|
144
|
+
}
|
|
145
|
+
switch (len) {
|
|
146
|
+
case 0 :
|
|
147
|
+
return "Other";
|
|
148
|
+
case 1 :
|
|
149
|
+
let name$1 = match$2[0];
|
|
150
|
+
return {
|
|
151
|
+
TAG: "Function",
|
|
152
|
+
_0: name$1
|
|
153
|
+
};
|
|
154
|
+
case 2 :
|
|
155
|
+
let name$2 = match$2[0];
|
|
156
|
+
return {
|
|
157
|
+
TAG: "Function",
|
|
158
|
+
_0: name$2
|
|
159
|
+
};
|
|
160
|
+
}
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
function tagValue(tags, key) {
|
|
@@ -143,6 +172,7 @@ async function discover(region, stack, platform) {
|
|
|
143
172
|
let client = ResourceGroupsTaggingApi$AwsSdk.client(region, undefined);
|
|
144
173
|
let tables = [];
|
|
145
174
|
let buckets = [];
|
|
175
|
+
let functions = [];
|
|
146
176
|
let loop = async token => {
|
|
147
177
|
let out = await client.send(new ClientResourceGroupsTaggingApi.GetResourcesCommand({
|
|
148
178
|
TagFilters: [
|
|
@@ -157,7 +187,8 @@ async function discover(region, stack, platform) {
|
|
|
157
187
|
],
|
|
158
188
|
ResourceTypeFilters: [
|
|
159
189
|
"dynamodb:table",
|
|
160
|
-
"s3"
|
|
190
|
+
"s3",
|
|
191
|
+
"lambda:function"
|
|
161
192
|
],
|
|
162
193
|
ResourcesPerPage: 100,
|
|
163
194
|
PaginationToken: token
|
|
@@ -181,11 +212,17 @@ async function discover(region, stack, platform) {
|
|
|
181
212
|
if (typeof name !== "object") {
|
|
182
213
|
return;
|
|
183
214
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
215
|
+
switch (name.TAG) {
|
|
216
|
+
case "Table" :
|
|
217
|
+
tables.push(name._0);
|
|
218
|
+
return;
|
|
219
|
+
case "Bucket" :
|
|
220
|
+
buckets.push(name._0);
|
|
221
|
+
return;
|
|
222
|
+
case "Function" :
|
|
223
|
+
functions.push(name._0);
|
|
224
|
+
return;
|
|
187
225
|
}
|
|
188
|
-
buckets.push(name._0);
|
|
189
226
|
});
|
|
190
227
|
let t = out.PaginationToken;
|
|
191
228
|
if (t !== undefined && t !== "") {
|
|
@@ -193,10 +230,11 @@ async function discover(region, stack, platform) {
|
|
|
193
230
|
}
|
|
194
231
|
};
|
|
195
232
|
await loop(undefined);
|
|
196
|
-
return
|
|
197
|
-
tables,
|
|
198
|
-
buckets
|
|
199
|
-
|
|
233
|
+
return {
|
|
234
|
+
tables: tables,
|
|
235
|
+
buckets: buckets,
|
|
236
|
+
functions: functions
|
|
237
|
+
};
|
|
200
238
|
}
|
|
201
239
|
|
|
202
240
|
function splitQualified(key) {
|
|
@@ -552,6 +590,28 @@ function gateTarget(target, backend, stack) {
|
|
|
552
590
|
}
|
|
553
591
|
}
|
|
554
592
|
|
|
593
|
+
function noQuiesce() {
|
|
594
|
+
let match = Stdlib_Option.map(Seed_Prompt$ReventlessSeed.envValue("SEED_RESET_NO_QUIESCE"), v => v.trim().toLowerCase());
|
|
595
|
+
if (match === undefined) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
switch (match) {
|
|
599
|
+
case "1" :
|
|
600
|
+
case "true" :
|
|
601
|
+
case "yes" :
|
|
602
|
+
return true;
|
|
603
|
+
default:
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function refillMessage(refilled, heldCount) {
|
|
609
|
+
let total = Stdlib_Array.reduce(refilled, 0, (acc, param) => acc + param[1] | 0);
|
|
610
|
+
let lines = refilled.map(param => `\n ` + param[1].toString().padStart(8, " ") + ` ` + param[0]).join("");
|
|
611
|
+
let diagnosis = heldCount > 0 ? `\n\n The wipe deleted these and something wrote them back. All ` + heldCount.toString() + ` runtime(s) in scope were held at zero concurrency for the whole wipe, so the writer was an invocation already in flight when the hold took effect. Re-running clears it.` : `\n\n The wipe deleted these and a running runtime wrote them back. The stack was NOT held (SEED_RESET_NO_QUIESCE), so nothing stopped it, and re-running will not help: the writer keeps the state in memory and restores it on every invocation, identical each time. Re-run without SEED_RESET_NO_QUIESCE so the runtimes are held and recycled.`;
|
|
612
|
+
return total.toString() + ` item(s)/object(s) came back after the wipe:` + lines + diagnosis;
|
|
613
|
+
}
|
|
614
|
+
|
|
555
615
|
function reportAll(resolvedList, stack, region) {
|
|
556
616
|
Seed_Runner$ReventlessSeed.heading(`Reset target: stack "` + stack + `" in ` + region);
|
|
557
617
|
let total = {
|
|
@@ -657,10 +717,9 @@ function run(stack, backend, targets, param) {
|
|
|
657
717
|
process.env["AWS_REGION"] = region;
|
|
658
718
|
if (selectedStores.length !== 0 && platformTarget !== undefined) {
|
|
659
719
|
let platformProject = projectName(platformTarget.projectDir);
|
|
660
|
-
let
|
|
661
|
-
let platformBuckets = match$1[1];
|
|
720
|
+
let platformFound = await discover(region, stack$1, platformProject);
|
|
662
721
|
selectedStores.forEach(s => {
|
|
663
|
-
if (
|
|
722
|
+
if (platformFound.buckets.includes(s.bucketName)) {
|
|
664
723
|
return;
|
|
665
724
|
}
|
|
666
725
|
throw {
|
|
@@ -675,9 +734,9 @@ function run(stack, backend, targets, param) {
|
|
|
675
734
|
let target = selected[i];
|
|
676
735
|
if (target !== undefined) {
|
|
677
736
|
let platform = projectName(target.projectDir);
|
|
678
|
-
let
|
|
679
|
-
let tables =
|
|
680
|
-
let buckets =
|
|
737
|
+
let found = await discover(region, stack$1, platform);
|
|
738
|
+
let tables = found.tables.toSorted(Primitive_string.compare);
|
|
739
|
+
let buckets = found.buckets.filter(b => !storeBucketNames.includes(b)).toSorted(Primitive_string.compare);
|
|
681
740
|
let tableCounts = [];
|
|
682
741
|
for (let j = 0, j_finish = tables.length; j < j_finish; ++j) {
|
|
683
742
|
let t = tables[j];
|
|
@@ -712,7 +771,8 @@ function run(stack, backend, targets, param) {
|
|
|
712
771
|
tables: tables,
|
|
713
772
|
tableCounts: tableCounts,
|
|
714
773
|
bucketCounts: bucketCounts,
|
|
715
|
-
storeCounts: storeCounts
|
|
774
|
+
storeCounts: storeCounts,
|
|
775
|
+
functions: found.functions.toSorted(Primitive_string.compare)
|
|
716
776
|
});
|
|
717
777
|
}
|
|
718
778
|
}
|
|
@@ -737,66 +797,128 @@ function run(stack, backend, targets, param) {
|
|
|
737
797
|
console.log(interactive ? "Dry run — nothing was deleted." : `Dry run — nothing was deleted. Set REVENTLESS_WIPE_CONFIRM=` + stack$1 + ` to empty this scope non-interactively.`);
|
|
738
798
|
process.exit(0);
|
|
739
799
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
800
|
+
let functionNames = Stdlib_Array.reduce(resolvedList, [], (acc, r) => acc.concat(r.functions.filter(f => !acc.includes(f))));
|
|
801
|
+
let quiesce = !noQuiesce() && functionNames.length !== 0;
|
|
802
|
+
let lambdaClient = Lambda$AwsSdk.client(region, undefined);
|
|
803
|
+
let held = quiesce ? (Seed_Runner$ReventlessSeed.heading(`Holding ` + functionNames.length.toString() + ` runtime(s) at zero concurrency …`), await ReventlessSeedAws_Quiesce.hold(lambdaClient, functionNames)) : (functionNames.length !== 0 ? (console.log(""), console.log("Note: SEED_RESET_NO_QUIESCE is set — the stack's runtimes keep running through the wipe. A runtime that holds state across invocations can write it straight back over an emptied store."), undefined) : undefined, []);
|
|
804
|
+
let outcome;
|
|
805
|
+
try {
|
|
806
|
+
for (let i$1 = 0, i_finish$1 = resolvedList.length; i$1 < i_finish$1; ++i$1) {
|
|
807
|
+
let r = resolvedList[i$1];
|
|
808
|
+
if (r !== undefined) {
|
|
809
|
+
Seed_Runner$ReventlessSeed.heading(`Emptying ` + r.target.label + ` …`);
|
|
810
|
+
for (let j$3 = 0, j_finish$3 = r.tables.length; j$3 < j_finish$3; ++j$3) {
|
|
811
|
+
let t$1 = r.tables[j$3];
|
|
812
|
+
if (t$1 !== undefined) {
|
|
813
|
+
await truncateTable(t$1);
|
|
814
|
+
console.log(` truncated ` + t$1);
|
|
815
|
+
}
|
|
749
816
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
817
|
+
for (let j$4 = 0, j_finish$4 = r.bucketCounts.length; j$4 < j_finish$4; ++j$4) {
|
|
818
|
+
let match$1 = r.bucketCounts[j$4];
|
|
819
|
+
if (match$1 !== undefined) {
|
|
820
|
+
let b$1 = match$1[0];
|
|
821
|
+
await emptyBucket(b$1, undefined);
|
|
822
|
+
console.log(` emptied ` + b$1);
|
|
823
|
+
}
|
|
757
824
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
825
|
+
for (let j$5 = 0, j_finish$5 = r.storeCounts.length; j$5 < j_finish$5; ++j$5) {
|
|
826
|
+
let match$2 = r.storeCounts[j$5];
|
|
827
|
+
if (match$2 !== undefined) {
|
|
828
|
+
let s$1 = match$2[0];
|
|
829
|
+
await emptyBucket(s$1.bucketName, s$1.keyPrefix + `/`);
|
|
830
|
+
console.log(` emptied ` + s$1.qualified + ` — ` + match$2[1].toString() + ` object(s) removed from ` + (s$1.bucketName + `/` + s$1.keyPrefix + `/`));
|
|
831
|
+
}
|
|
765
832
|
}
|
|
766
833
|
}
|
|
767
834
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
835
|
+
let refilled = [];
|
|
836
|
+
for (let i$2 = 0, i_finish$2 = resolvedList.length; i$2 < i_finish$2; ++i$2) {
|
|
837
|
+
let r$1 = resolvedList[i$2];
|
|
838
|
+
if (r$1 !== undefined) {
|
|
839
|
+
for (let j$6 = 0, j_finish$6 = r$1.tables.length; j$6 < j_finish$6; ++j$6) {
|
|
840
|
+
let t$2 = r$1.tables[j$6];
|
|
841
|
+
if (t$2 !== undefined) {
|
|
842
|
+
let left = await countTable(t$2);
|
|
843
|
+
if (left !== 0) {
|
|
844
|
+
refilled.push([
|
|
845
|
+
t$2,
|
|
846
|
+
left
|
|
847
|
+
]);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
777
850
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
851
|
+
for (let j$7 = 0, j_finish$7 = r$1.bucketCounts.length; j$7 < j_finish$7; ++j$7) {
|
|
852
|
+
let match$3 = r$1.bucketCounts[j$7];
|
|
853
|
+
if (match$3 !== undefined) {
|
|
854
|
+
let b$2 = match$3[0];
|
|
855
|
+
let left$1 = await countBucket(b$2, undefined);
|
|
856
|
+
if (left$1 !== 0) {
|
|
857
|
+
refilled.push([
|
|
858
|
+
b$2,
|
|
859
|
+
left$1
|
|
860
|
+
]);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
783
863
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
864
|
+
for (let j$8 = 0, j_finish$8 = r$1.storeCounts.length; j$8 < j_finish$8; ++j$8) {
|
|
865
|
+
let match$4 = r$1.storeCounts[j$8];
|
|
866
|
+
if (match$4 !== undefined) {
|
|
867
|
+
let s$2 = match$4[0];
|
|
868
|
+
let left$2 = await countBucket(s$2.bucketName, s$2.keyPrefix + `/`);
|
|
869
|
+
if (left$2 !== 0) {
|
|
870
|
+
refilled.push([
|
|
871
|
+
s$2.qualified + ` (` + s$2.bucketName + `/` + s$2.keyPrefix + `/)`,
|
|
872
|
+
left$2
|
|
873
|
+
]);
|
|
874
|
+
}
|
|
875
|
+
console.log(` verified empty: ` + s$2.qualified + ` — ` + left$2.toString() + ` object(s) remain under ` + (s$2.bucketName + `/` + s$2.keyPrefix + `/`));
|
|
876
|
+
}
|
|
792
877
|
}
|
|
793
878
|
}
|
|
794
879
|
}
|
|
880
|
+
outcome = {
|
|
881
|
+
TAG: "Ok",
|
|
882
|
+
_0: refilled
|
|
883
|
+
};
|
|
884
|
+
} catch (raw_message) {
|
|
885
|
+
let message$1 = Primitive_exceptions.internalToException(raw_message);
|
|
886
|
+
outcome = message$1.RE_EXN_ID === Seed$ReventlessSeed.Failed ? ({
|
|
887
|
+
TAG: "Error",
|
|
888
|
+
_0: message$1._1
|
|
889
|
+
}) : ({
|
|
890
|
+
TAG: "Error",
|
|
891
|
+
_0: `the wipe failed: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(message$1), Stdlib_JsExn.message), "unknown error")
|
|
892
|
+
});
|
|
795
893
|
}
|
|
796
|
-
|
|
894
|
+
let warnings;
|
|
895
|
+
if (held.length !== 0) {
|
|
896
|
+
Seed_Runner$ReventlessSeed.heading(`Recycling ` + held.length.toString() + ` runtime(s) …`);
|
|
897
|
+
let recycled = await ReventlessSeedAws_Quiesce.recycle(lambdaClient, held);
|
|
898
|
+
let released = await ReventlessSeedAws_Quiesce.release(lambdaClient, held);
|
|
899
|
+
warnings = recycled.concat(released);
|
|
900
|
+
} else {
|
|
901
|
+
warnings = [];
|
|
902
|
+
}
|
|
903
|
+
if (warnings.length !== 0) {
|
|
904
|
+
console.error("");
|
|
905
|
+
warnings.forEach(w => {
|
|
906
|
+
console.error(` warning: ` + w);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (outcome.TAG === "Ok") {
|
|
910
|
+
let refilled$1 = outcome._0;
|
|
911
|
+
if (refilled$1.length !== 0) {
|
|
912
|
+
throw {
|
|
913
|
+
RE_EXN_ID: Seed$ReventlessSeed.Failed,
|
|
914
|
+
_1: refillMessage(refilled$1, held.length),
|
|
915
|
+
Error: new Error()
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
797
919
|
throw {
|
|
798
920
|
RE_EXN_ID: Seed$ReventlessSeed.Failed,
|
|
799
|
-
_1:
|
|
921
|
+
_1: outcome._0,
|
|
800
922
|
Error: new Error()
|
|
801
923
|
};
|
|
802
924
|
}
|
|
@@ -804,18 +926,18 @@ function run(stack, backend, targets, param) {
|
|
|
804
926
|
console.log(`Reset complete — the selected scope of "` + stack$1 + `" reads empty and is re-seedable.`);
|
|
805
927
|
process.exit(0);
|
|
806
928
|
return;
|
|
807
|
-
} catch (raw_message) {
|
|
808
|
-
let message$
|
|
809
|
-
if (message$
|
|
929
|
+
} catch (raw_message$1) {
|
|
930
|
+
let message$2 = Primitive_exceptions.internalToException(raw_message$1);
|
|
931
|
+
if (message$2.RE_EXN_ID === Seed$ReventlessSeed.Failed) {
|
|
810
932
|
Seed_Prompt$ReventlessSeed.close();
|
|
811
933
|
console.error("");
|
|
812
|
-
console.error(`Reset aborted — ` + message$
|
|
934
|
+
console.error(`Reset aborted — ` + message$2._1);
|
|
813
935
|
process.exit(1);
|
|
814
936
|
} else {
|
|
815
937
|
Seed_Prompt$ReventlessSeed.close();
|
|
816
938
|
console.error("");
|
|
817
939
|
console.error("Reset aborted with an unexpected error:");
|
|
818
|
-
console.error(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(message$
|
|
940
|
+
console.error(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(message$2), Stdlib_JsExn.message), "unknown"));
|
|
819
941
|
process.exit(1);
|
|
820
942
|
}
|
|
821
943
|
return;
|
|
@@ -830,10 +952,16 @@ let Rgt;
|
|
|
830
952
|
|
|
831
953
|
let S3;
|
|
832
954
|
|
|
955
|
+
let Lambda;
|
|
956
|
+
|
|
957
|
+
let Quiesce;
|
|
958
|
+
|
|
833
959
|
export {
|
|
834
960
|
Ddb,
|
|
835
961
|
Rgt,
|
|
836
962
|
S3,
|
|
963
|
+
Lambda,
|
|
964
|
+
Quiesce,
|
|
837
965
|
projectName,
|
|
838
966
|
field,
|
|
839
967
|
asString,
|
|
@@ -855,6 +983,8 @@ export {
|
|
|
855
983
|
pluginOf,
|
|
856
984
|
chooseScope,
|
|
857
985
|
gateTarget,
|
|
986
|
+
noQuiesce,
|
|
987
|
+
refillMessage,
|
|
858
988
|
reportAll,
|
|
859
989
|
run,
|
|
860
990
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Holding a stack's runtimes still for the length of a wipe: what gets held, and
|
|
2
|
+
// what the reset says when a store comes back anyway.
|
|
3
|
+
//
|
|
4
|
+
// A truncate is not durable while the runtimes that own the data are running — a
|
|
5
|
+
// slice runtime keeps its TODO list in memory for the life of its execution
|
|
6
|
+
// environment and re-saves every row it holds at the end of each invocation,
|
|
7
|
+
// including the scheduled sweep that carries no events. So the reset now
|
|
8
|
+
// discovers the stack's Lambda functions alongside its stores and holds them at
|
|
9
|
+
// zero concurrency across the wipe.
|
|
10
|
+
//
|
|
11
|
+
// The pure half of that is what these pin: which ARNs resolve to a function to
|
|
12
|
+
// hold, the bounded fan-out the control-plane calls go through, and the failure
|
|
13
|
+
// message — which used to say "re-run to finish" for a condition re-running
|
|
14
|
+
// could not clear. The impure half is the Lambda control-plane calls themselves.
|
|
15
|
+
|
|
16
|
+
open JestGlobals
|
|
17
|
+
|
|
18
|
+
module Reset = ReventlessSeedAws_Reset
|
|
19
|
+
module Quiesce = ReventlessSeedAws_Quiesce
|
|
20
|
+
|
|
21
|
+
describe("classify", () => {
|
|
22
|
+
testSync("reads the function name out of a Lambda ARN", () =>
|
|
23
|
+
expect(Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllAutomationSlices-28b116c"))
|
|
24
|
+
->toEqual(Reset.Function("AllAutomationSlices-28b116c"))
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// The tagging API returns unqualified ARNs, but a qualified one must still
|
|
28
|
+
// yield the function name — every Lambda control-plane call the hold makes
|
|
29
|
+
// takes a name, and "Name:1" is not one.
|
|
30
|
+
testSync("drops a version or alias qualifier", () =>
|
|
31
|
+
expect(Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllReadModels-0287438:42"))
|
|
32
|
+
->toEqual(Reset.Function("AllReadModels-0287438"))
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
testSync("still classifies tables and buckets", () => {
|
|
36
|
+
expect(Reset.classify("arn:aws:dynamodb:eu-west-1:123456789012:table/Products-0ba6849"))
|
|
37
|
+
->toEqual(Reset.Table("Products-0ba6849"))
|
|
38
|
+
expect(Reset.classify("arn:aws:s3:::alpha-product-images"))->toEqual(
|
|
39
|
+
Reset.Bucket("alpha-product-images"),
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
testSync("ignores anything else the tag scope returns", () =>
|
|
44
|
+
expect(Reset.classify("arn:aws:sqs:eu-west-1:123456789012:OrderingDcbCmdTopic-4b3658c"))
|
|
45
|
+
->toEqual(Reset.Other)
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("mapBounded", () => {
|
|
50
|
+
// Results must line up with their inputs regardless of which worker finished
|
|
51
|
+
// first: the hold pairs each result with the function it came from, and a
|
|
52
|
+
// shuffled array would restore the wrong concurrency to the wrong function.
|
|
53
|
+
test("keeps results in input order under out-of-order completion", async () => {
|
|
54
|
+
let delays = [40, 0, 30, 10, 20, 5]
|
|
55
|
+
let out = await delays->Quiesce.mapBounded(~limit=3, async ms => {
|
|
56
|
+
await ReventlessSeed.Seed.Client.sleep(ms)
|
|
57
|
+
ms
|
|
58
|
+
})
|
|
59
|
+
expect(out)->toEqual(delays)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("never runs more than the limit at once", async () => {
|
|
63
|
+
let inFlight = ref(0)
|
|
64
|
+
let peak = ref(0)
|
|
65
|
+
let _ = await Array.make(~length=12, 0)->Quiesce.mapBounded(~limit=4, async _ => {
|
|
66
|
+
inFlight := inFlight.contents + 1
|
|
67
|
+
peak := Math.Int.max(peak.contents, inFlight.contents)
|
|
68
|
+
await ReventlessSeed.Seed.Client.sleep(5)
|
|
69
|
+
inFlight := inFlight.contents - 1
|
|
70
|
+
})
|
|
71
|
+
expect(peak.contents)->toBe(4)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("an empty list spawns no workers and returns nothing", async () => {
|
|
75
|
+
let out = await []->Quiesce.mapBounded(~limit=4, async x => x)
|
|
76
|
+
expect(out->Array.length)->toBe(0)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("refillMessage", () => {
|
|
81
|
+
// The whole point of the change: the operator has to be able to see WHICH
|
|
82
|
+
// store came back, not just a total.
|
|
83
|
+
testSync("names every store that came back, with its count", () => {
|
|
84
|
+
let message = Reset.refillMessage(
|
|
85
|
+
~refilled=[("AutoShipOrderTodo-61f39a2", 12), ("GeocodeCustomerAddressTodo-50d9488", 1)],
|
|
86
|
+
~heldCount=41,
|
|
87
|
+
)
|
|
88
|
+
expect(message->String.includes("AutoShipOrderTodo-61f39a2"))->toBe(true)
|
|
89
|
+
expect(message->String.includes("GeocodeCustomerAddressTodo-50d9488"))->toBe(true)
|
|
90
|
+
expect(message->String.startsWith("13 item(s)/object(s) came back"))->toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Under a hold nothing could START during the wipe, so the only writer left is
|
|
94
|
+
// an invocation already running when the hold landed — and that one really
|
|
95
|
+
// does go away. This is the only case where "re-run" is honest advice.
|
|
96
|
+
testSync("advises re-running only when the runtimes were held", () => {
|
|
97
|
+
let message = Reset.refillMessage(~refilled=[("AutoShipOrderTodo-61f39a2", 12)], ~heldCount=41)
|
|
98
|
+
expect(message->String.includes("already in flight"))->toBe(true)
|
|
99
|
+
expect(message->String.includes("Re-running clears it"))->toBe(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Without a hold, the writer holds the rows in memory and restores them every
|
|
103
|
+
// invocation — the exact condition the old "re-run to finish" sent operators
|
|
104
|
+
// round in circles on. It must not appear here.
|
|
105
|
+
testSync("refuses to advise re-running when the stack was not held", () => {
|
|
106
|
+
let message = Reset.refillMessage(~refilled=[("AutoShipOrderTodo-61f39a2", 12)], ~heldCount=0)
|
|
107
|
+
expect(message->String.includes("re-running will not help"->String.toLowerCase))->toBe(true)
|
|
108
|
+
expect(message->String.includes("SEED_RESET_NO_QUIESCE"))->toBe(true)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe("noQuiesce", () => {
|
|
113
|
+
let set = v => NodeProcess.env->Dict.set("SEED_RESET_NO_QUIESCE", v)
|
|
114
|
+
|
|
115
|
+
// Fail-closed: the hold is what makes the wipe durable, so anything other than
|
|
116
|
+
// a deliberate opt-out keeps it on.
|
|
117
|
+
testSync("defaults to holding the platform", () => {
|
|
118
|
+
set("")
|
|
119
|
+
expect(Reset.noQuiesce())->toBe(false)
|
|
120
|
+
set("0")
|
|
121
|
+
expect(Reset.noQuiesce())->toBe(false)
|
|
122
|
+
set("no")
|
|
123
|
+
expect(Reset.noQuiesce())->toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
testSync("accepts the spellings an operator would actually type", () => {
|
|
127
|
+
["1", "true", "TRUE", " yes "]->Array.forEach(v => {
|
|
128
|
+
set(v)
|
|
129
|
+
expect(Reset.noQuiesce())->toBe(true)
|
|
130
|
+
})
|
|
131
|
+
set("")
|
|
132
|
+
})
|
|
133
|
+
})
|