@reventlessdev/reventless-seed-aws 1.0.0-alpha.11 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/compiler-info.json +1 -1
  4. package/lib/bs/src/ReventlessSeedAws_Quiesce.ast +0 -0
  5. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmi +0 -0
  6. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmj +0 -0
  7. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmt +0 -0
  8. package/lib/bs/src/ReventlessSeedAws_Quiesce.res +287 -0
  9. package/lib/bs/src/ReventlessSeedAws_Quiesce.res.mjs +205 -0
  10. package/lib/bs/src/ReventlessSeedAws_Reconcile.ast +0 -0
  11. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmi +0 -0
  12. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmt +0 -0
  13. package/lib/bs/src/ReventlessSeedAws_Reconcile.res +2 -6
  14. package/lib/bs/src/ReventlessSeedAws_Reconcile.res.mjs +2 -2
  15. package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
  16. package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
  17. package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
  18. package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
  19. package/lib/bs/src/ReventlessSeedAws_Reset.res +223 -73
  20. package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +212 -82
  21. package/lib/bs/tests/ObjectStoreReconcileTest.cmi +0 -0
  22. package/lib/bs/tests/ObjectStoreReconcileTest.cmt +0 -0
  23. package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
  24. package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
  25. package/lib/bs/tests/QuiesceResetTest.ast +0 -0
  26. package/lib/bs/tests/QuiesceResetTest.cmi +0 -0
  27. package/lib/bs/tests/QuiesceResetTest.cmj +0 -0
  28. package/lib/bs/tests/QuiesceResetTest.cmt +0 -0
  29. package/lib/bs/tests/QuiesceResetTest.res +133 -0
  30. package/lib/bs/tests/QuiesceResetTest.res.mjs +141 -0
  31. package/lib/ocaml/.compiler.log +2 -2
  32. package/lib/ocaml/ObjectStoreReconcileTest.cmi +0 -0
  33. package/lib/ocaml/ObjectStoreReconcileTest.cmt +0 -0
  34. package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
  35. package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
  36. package/lib/ocaml/QuiesceResetTest.ast +0 -0
  37. package/lib/ocaml/QuiesceResetTest.cmi +0 -0
  38. package/lib/ocaml/QuiesceResetTest.cmj +0 -0
  39. package/lib/ocaml/QuiesceResetTest.cmt +0 -0
  40. package/lib/ocaml/QuiesceResetTest.res +133 -0
  41. package/lib/ocaml/ReventlessSeedAws_Quiesce.ast +0 -0
  42. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmi +0 -0
  43. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmj +0 -0
  44. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmt +0 -0
  45. package/lib/ocaml/ReventlessSeedAws_Quiesce.res +287 -0
  46. package/lib/ocaml/ReventlessSeedAws_Reconcile.ast +0 -0
  47. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmi +0 -0
  48. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmt +0 -0
  49. package/lib/ocaml/ReventlessSeedAws_Reconcile.res +2 -6
  50. package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
  51. package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
  52. package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
  53. package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
  54. package/lib/ocaml/ReventlessSeedAws_Reset.res +223 -73
  55. package/package.json +4 -4
  56. package/src/ReventlessSeedAws_Quiesce.res +287 -0
  57. package/src/ReventlessSeedAws_Quiesce.res.mjs +205 -0
  58. package/src/ReventlessSeedAws_Reconcile.res +2 -6
  59. package/src/ReventlessSeedAws_Reconcile.res.mjs +2 -2
  60. package/src/ReventlessSeedAws_Reset.res +223 -73
  61. package/src/ReventlessSeedAws_Reset.res.mjs +212 -82
  62. package/tests/QuiesceResetTest.res +133 -0
  63. 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 (!arn.startsWith("arn:aws:dynamodb:")) {
114
- if (arn.startsWith("arn:aws:s3:::")) {
115
- return {
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
- let match = arn.split(":table/");
124
- if (match.length !== 2) {
132
+ if (!arn.startsWith("arn:aws:lambda:")) {
125
133
  return "Other";
126
134
  }
127
- let name = match[1];
128
- return {
129
- TAG: "Table",
130
- _0: name
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
- if (name.TAG === "Table") {
185
- tables.push(name._0);
186
- return;
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 match$1 = await discover(region, stack$1, platformProject);
661
- let platformBuckets = match$1[1];
720
+ let platformFound = await discover(region, stack$1, platformProject);
662
721
  selectedStores.forEach(s => {
663
- if (platformBuckets.includes(s.bucketName)) {
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 match$2 = await discover(region, stack$1, platform);
679
- let tables = match$2[0].toSorted(Primitive_string.compare);
680
- let buckets = match$2[1].filter(b => !storeBucketNames.includes(b)).toSorted(Primitive_string.compare);
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
- for (let i$1 = 0, i_finish$1 = resolvedList.length; i$1 < i_finish$1; ++i$1) {
741
- let r = resolvedList[i$1];
742
- if (r !== undefined) {
743
- Seed_Runner$ReventlessSeed.heading(`Emptying ` + r.target.label + ` …`);
744
- for (let j$3 = 0, j_finish$3 = r.tables.length; j$3 < j_finish$3; ++j$3) {
745
- let t$1 = r.tables[j$3];
746
- if (t$1 !== undefined) {
747
- await truncateTable(t$1);
748
- console.log(` truncated ` + t$1);
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
- for (let j$4 = 0, j_finish$4 = r.bucketCounts.length; j$4 < j_finish$4; ++j$4) {
752
- let match$3 = r.bucketCounts[j$4];
753
- if (match$3 !== undefined) {
754
- let b$1 = match$3[0];
755
- await emptyBucket(b$1, undefined);
756
- console.log(` emptied ` + b$1);
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
- for (let j$5 = 0, j_finish$5 = r.storeCounts.length; j$5 < j_finish$5; ++j$5) {
760
- let match$4 = r.storeCounts[j$5];
761
- if (match$4 !== undefined) {
762
- let s$1 = match$4[0];
763
- await emptyBucket(s$1.bucketName, s$1.keyPrefix + `/`);
764
- console.log(` emptied ` + s$1.qualified + ` — ` + match$4[1].toString() + ` object(s) removed from ` + (s$1.bucketName + `/` + s$1.keyPrefix + `/`));
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
- let remaining = 0;
770
- for (let i$2 = 0, i_finish$2 = resolvedList.length; i$2 < i_finish$2; ++i$2) {
771
- let r$1 = resolvedList[i$2];
772
- if (r$1 !== undefined) {
773
- for (let j$6 = 0, j_finish$6 = r$1.tables.length; j$6 < j_finish$6; ++j$6) {
774
- let t$2 = r$1.tables[j$6];
775
- if (t$2 !== undefined) {
776
- remaining = remaining + await countTable(t$2) | 0;
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
- for (let j$7 = 0, j_finish$7 = r$1.bucketCounts.length; j$7 < j_finish$7; ++j$7) {
780
- let match$5 = r$1.bucketCounts[j$7];
781
- if (match$5 !== undefined) {
782
- remaining = remaining + await countBucket(match$5[0], undefined) | 0;
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
- for (let j$8 = 0, j_finish$8 = r$1.storeCounts.length; j$8 < j_finish$8; ++j$8) {
786
- let match$6 = r$1.storeCounts[j$8];
787
- if (match$6 !== undefined) {
788
- let s$2 = match$6[0];
789
- let left = await countBucket(s$2.bucketName, s$2.keyPrefix + `/`);
790
- remaining = remaining + left | 0;
791
- console.log(` verified empty: ` + s$2.qualified + ` — ` + left.toString() + ` object(s) remain under ` + (s$2.bucketName + `/` + s$2.keyPrefix + `/`));
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
- if (remaining !== 0) {
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: remaining.toString() + ` item(s)/object(s) remain after the wipe — re-run to finish.`,
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$1 = Primitive_exceptions.internalToException(raw_message);
809
- if (message$1.RE_EXN_ID === Seed$ReventlessSeed.Failed) {
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$1._1);
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$1), Stdlib_JsExn.message), "unknown"));
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
  }
@@ -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
+ })