@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.
- package/CHANGELOG.md +15 -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 +4 -4
- 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
|
@@ -47,6 +47,8 @@ open ReventlessSeed
|
|
|
47
47
|
module Ddb = AwsSdk.DynamoDb
|
|
48
48
|
module Rgt = AwsSdk.ResourceGroupsTaggingApi
|
|
49
49
|
module S3 = AwsSdk.S3
|
|
50
|
+
module Lambda = AwsSdk.Lambda
|
|
51
|
+
module Quiesce = ReventlessSeedAws_Quiesce
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
|
|
@@ -140,9 +142,16 @@ let configValue = (cfg: dict<JSON.t>, key: string): option<string> =>
|
|
|
140
142
|
type resource =
|
|
141
143
|
| Table(string)
|
|
142
144
|
| Bucket(string)
|
|
145
|
+
| Function(string)
|
|
143
146
|
| Other
|
|
144
147
|
|
|
145
148
|
// arn:aws:dynamodb:<region>:<acct>:table/<name> | arn:aws:s3:::<bucket>
|
|
149
|
+
// | arn:aws:lambda:<region>:<acct>:function:<name>
|
|
150
|
+
//
|
|
151
|
+
// A Lambda ARN can carry a trailing `:<version|alias>` qualifier. The tagging
|
|
152
|
+
// API returns the unqualified form, but taking the segment after `:function:`
|
|
153
|
+
// up to the next colon rather than to the end of the string means a qualified
|
|
154
|
+
// one still yields the function name instead of a name no API accepts.
|
|
146
155
|
let classify = (arn: string): resource =>
|
|
147
156
|
if arn->String.startsWith("arn:aws:dynamodb:") {
|
|
148
157
|
switch arn->String.split(":table/") {
|
|
@@ -151,6 +160,16 @@ let classify = (arn: string): resource =>
|
|
|
151
160
|
}
|
|
152
161
|
} else if arn->String.startsWith("arn:aws:s3:::") {
|
|
153
162
|
Bucket(arn->String.slice(~start=String.length("arn:aws:s3:::"), ~end=String.length(arn)))
|
|
163
|
+
} else if arn->String.startsWith("arn:aws:lambda:") {
|
|
164
|
+
switch arn->String.split(":function:") {
|
|
165
|
+
| [_, rest] =>
|
|
166
|
+
switch rest->String.split(":") {
|
|
167
|
+
| [name] => Function(name)
|
|
168
|
+
| [name, _qualifier] => Function(name)
|
|
169
|
+
| _ => Other
|
|
170
|
+
}
|
|
171
|
+
| _ => Other
|
|
172
|
+
}
|
|
154
173
|
} else {
|
|
155
174
|
Other
|
|
156
175
|
}
|
|
@@ -158,15 +177,29 @@ let classify = (arn: string): resource =>
|
|
|
158
177
|
let tagValue = (tags: array<Rgt.GetResourcesCommand.tag>, key: string): option<string> =>
|
|
159
178
|
tags->Array.findMap(t => t.key == key ? Some(t.value) : None)
|
|
160
179
|
|
|
180
|
+
/** What one project's tags resolve to: the stores to empty, and the runtimes
|
|
181
|
+
that have to be held still while that happens. */
|
|
182
|
+
type discovered = {
|
|
183
|
+
tables: array<string>,
|
|
184
|
+
buckets: array<string>,
|
|
185
|
+
functions: array<string>,
|
|
186
|
+
}
|
|
187
|
+
|
|
161
188
|
// Discovery is scoped by BOTH tags (platform AND environment) — the Tagging API
|
|
162
189
|
// ANDs multiple TagFilters — so it only ever sees the target project's own stack,
|
|
163
190
|
// never a same-named stack from another project. Each returned resource is then
|
|
164
191
|
// re-checked to carry both tags (a mismatch aborts the whole run rather than
|
|
165
192
|
// being skipped) as a second, per-resource guard.
|
|
166
|
-
|
|
193
|
+
//
|
|
194
|
+
// Lambda functions are discovered alongside the stores, under the same tags and
|
|
195
|
+
// the same per-resource re-check. They are never deleted from — they are what
|
|
196
|
+
// the wipe has to hold still, and the tag scope that decides what may be emptied
|
|
197
|
+
// is exactly the scope that decides what may be held.
|
|
198
|
+
let discover = async (~region, ~stack, ~platform): discovered => {
|
|
167
199
|
let client = Rgt.client(~region, ())
|
|
168
200
|
let tables = []
|
|
169
201
|
let buckets = []
|
|
202
|
+
let functions = []
|
|
170
203
|
let rec loop = async (token: option<string>): unit => {
|
|
171
204
|
let out = await Rgt.GetResourcesCommand.send(
|
|
172
205
|
client,
|
|
@@ -175,7 +208,7 @@ let discover = async (~region, ~stack, ~platform): (array<string>, array<string>
|
|
|
175
208
|
{key: "reventless:platform", values: [platform]},
|
|
176
209
|
{key: "reventless:environment", values: [stack]},
|
|
177
210
|
],
|
|
178
|
-
resourceTypeFilters: ["dynamodb:table", "s3"],
|
|
211
|
+
resourceTypeFilters: ["dynamodb:table", "s3", "lambda:function"],
|
|
179
212
|
resourcesPerPage: 100,
|
|
180
213
|
paginationToken: ?token,
|
|
181
214
|
}),
|
|
@@ -201,6 +234,7 @@ let discover = async (~region, ~stack, ~platform): (array<string>, array<string>
|
|
|
201
234
|
switch classify(m.resourceARN) {
|
|
202
235
|
| Table(name) => tables->Array.push(name)
|
|
203
236
|
| Bucket(name) => buckets->Array.push(name)
|
|
237
|
+
| Function(name) => functions->Array.push(name)
|
|
204
238
|
| Other => ()
|
|
205
239
|
}
|
|
206
240
|
})
|
|
@@ -210,7 +244,7 @@ let discover = async (~region, ~stack, ~platform): (array<string>, array<string>
|
|
|
210
244
|
}
|
|
211
245
|
}
|
|
212
246
|
await loop(None)
|
|
213
|
-
|
|
247
|
+
{tables, buckets, functions}
|
|
214
248
|
}
|
|
215
249
|
|
|
216
250
|
// ── Declared object stores ────────────────────────────────────────────────────
|
|
@@ -517,6 +551,8 @@ type resolved = {
|
|
|
517
551
|
tableCounts: array<int>,
|
|
518
552
|
bucketCounts: array<(string, int)>,
|
|
519
553
|
storeCounts: array<(objectStore, int)>,
|
|
554
|
+
/** The project's Lambda functions — not wiped, held still while it is. */
|
|
555
|
+
functions: array<string>,
|
|
520
556
|
}
|
|
521
557
|
|
|
522
558
|
// Picks which targets to wipe. `domain` (every domain plugin) leads and is the
|
|
@@ -598,6 +634,49 @@ let gateTarget = (~target: target, ~backend, ~stack): string => {
|
|
|
598
634
|
}
|
|
599
635
|
}
|
|
600
636
|
|
|
637
|
+
/** Opt out of holding the platform still for the wipe. The hold needs Lambda
|
|
638
|
+
permissions the reset did not previously ask for, so credentials that lack
|
|
639
|
+
them have a way through — at the cost of the emptiness guarantee, which the
|
|
640
|
+
note printed alongside it says plainly. */
|
|
641
|
+
let noQuiesce = (): bool =>
|
|
642
|
+
switch Seed.Prompt.envValue("SEED_RESET_NO_QUIESCE")->Option.map(v =>
|
|
643
|
+
v->String.trim->String.toLowerCase
|
|
644
|
+
) {
|
|
645
|
+
| Some("1") | Some("true") | Some("yes") => true
|
|
646
|
+
| _ => false
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** What to say when a store is not empty after the wipe.
|
|
650
|
+
|
|
651
|
+
The old message reported only a total and advised re-running. That advice was
|
|
652
|
+
wrong in exactly the case that occurs: a runtime holding its state in memory
|
|
653
|
+
rewrites the store on every invocation, so every re-run reproduces the same
|
|
654
|
+
failure and the message sends the operator round the loop again.
|
|
655
|
+
|
|
656
|
+
So name the stores, and separate the two situations that are actually
|
|
657
|
+
different. Under a hold, nothing could start during the wipe, so the only
|
|
658
|
+
possible writer is an invocation that was already in flight — and a re-run
|
|
659
|
+
does clear that. Without a hold there is no such bound, and re-running is the
|
|
660
|
+
wrong advice. */
|
|
661
|
+
let refillMessage = (~refilled: array<(string, int)>, ~heldCount: int): string => {
|
|
662
|
+
let total = refilled->Array.reduce(0, (acc, (_, n)) => acc + n)
|
|
663
|
+
let lines =
|
|
664
|
+
refilled
|
|
665
|
+
->Array.map(((label, n)) => `\n ${n->Int.toString->String.padStart(8, " ")} ${label}`)
|
|
666
|
+
->Array.join("")
|
|
667
|
+
let diagnosis = if heldCount > 0 {
|
|
668
|
+
`\n\n The wipe deleted these and something wrote them back. All ${heldCount->Int.toString} runtime(s) ` ++
|
|
669
|
+
`in scope were held at zero concurrency for the whole wipe, so the writer was an invocation already ` ++
|
|
670
|
+
`in flight when the hold took effect. Re-running clears it.`
|
|
671
|
+
} else {
|
|
672
|
+
`\n\n The wipe deleted these and a running runtime wrote them back. The stack was NOT held ` ++
|
|
673
|
+
`(SEED_RESET_NO_QUIESCE), so nothing stopped it, and re-running will not help: the writer keeps the ` ++
|
|
674
|
+
`state in memory and restores it on every invocation, identical each time. Re-run without ` ++
|
|
675
|
+
`SEED_RESET_NO_QUIESCE so the runtimes are held and recycled.`
|
|
676
|
+
}
|
|
677
|
+
`${total->Int.toString} item(s)/object(s) came back after the wipe:${lines}${diagnosis}`
|
|
678
|
+
}
|
|
679
|
+
|
|
601
680
|
let reportAll = (resolvedList: array<resolved>, ~stack, ~region): int => {
|
|
602
681
|
Seed.Runner.heading(`Reset target: stack "${stack}" in ${region}`)
|
|
603
682
|
let total = ref(0)
|
|
@@ -747,9 +826,9 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
747
826
|
switch platformTarget {
|
|
748
827
|
| Some(pt) =>
|
|
749
828
|
let platformProject = projectName(~projectDir=pt.projectDir)
|
|
750
|
-
let
|
|
829
|
+
let platformFound = await discover(~region, ~stack, ~platform=platformProject)
|
|
751
830
|
selectedStores->Array.forEach(s =>
|
|
752
|
-
if !(
|
|
831
|
+
if !(platformFound.buckets->Array.includes(s.bucketName)) {
|
|
753
832
|
throw(
|
|
754
833
|
Seed.Failed(
|
|
755
834
|
`refusing: store "${s.qualified}" names bucket ${s.bucketName}, which does not ` ++
|
|
@@ -769,10 +848,10 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
769
848
|
switch selected->Array.get(i) {
|
|
770
849
|
| Some(target) =>
|
|
771
850
|
let platform = projectName(~projectDir=target.projectDir)
|
|
772
|
-
let
|
|
773
|
-
let tables = tables->Array.toSorted(String.compare)
|
|
851
|
+
let found = await discover(~region, ~stack, ~platform)
|
|
852
|
+
let tables = found.tables->Array.toSorted(String.compare)
|
|
774
853
|
let buckets =
|
|
775
|
-
buckets
|
|
854
|
+
found.buckets
|
|
776
855
|
->Array.filter(b => !(storeBucketNames->Array.includes(b)))
|
|
777
856
|
->Array.toSorted(String.compare)
|
|
778
857
|
let tableCounts = []
|
|
@@ -808,6 +887,7 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
808
887
|
tableCounts,
|
|
809
888
|
bucketCounts,
|
|
810
889
|
storeCounts,
|
|
890
|
+
functions: found.functions->Array.toSorted(String.compare),
|
|
811
891
|
})
|
|
812
892
|
| None => ()
|
|
813
893
|
}
|
|
@@ -848,85 +928,155 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
848
928
|
NodeProcess.exit(0)
|
|
849
929
|
}
|
|
850
930
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
931
|
+
// Hold the platform still before the first delete. A truncate is not
|
|
932
|
+
// durable while the runtimes that own the data are running — see
|
|
933
|
+
// ReventlessSeedAws_Quiesce for the mechanism and for why emptying the
|
|
934
|
+
// upstream stores first does not substitute for this.
|
|
935
|
+
let functionNames = resolvedList->Array.reduce([], (acc, r) =>
|
|
936
|
+
Array.concat(acc, r.functions->Array.filter(f => !(acc->Array.includes(f))))
|
|
937
|
+
)
|
|
938
|
+
let quiesce = !noQuiesce() && functionNames->Array.length > 0
|
|
939
|
+
let lambdaClient = Lambda.client(~region, ())
|
|
940
|
+
let held = if quiesce {
|
|
941
|
+
Seed.Runner.heading(
|
|
942
|
+
`Holding ${functionNames->Array.length->Int.toString} runtime(s) at zero concurrency …`,
|
|
943
|
+
)
|
|
944
|
+
await Quiesce.hold(~client=lambdaClient, ~functionNames)
|
|
945
|
+
} else {
|
|
946
|
+
if functionNames->Array.length > 0 {
|
|
947
|
+
Console.log("")
|
|
948
|
+
Console.log(
|
|
949
|
+
"Note: SEED_RESET_NO_QUIESCE is set — the stack's runtimes keep running through the wipe. " ++
|
|
950
|
+
"A runtime that holds state across invocations can write it straight back over an emptied store.",
|
|
951
|
+
)
|
|
952
|
+
}
|
|
953
|
+
[]
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// From here the reset owns the platform's concurrency, so every exit has
|
|
957
|
+
// to run through the release below — including the abort path. ReScript
|
|
958
|
+
// has no `finally`, so the wipe's outcome is captured as a value and
|
|
959
|
+
// re-raised after the stack has been handed back.
|
|
960
|
+
let outcome = try {
|
|
961
|
+
for i in 0 to resolvedList->Array.length - 1 {
|
|
962
|
+
switch resolvedList->Array.get(i) {
|
|
963
|
+
| Some(r) =>
|
|
964
|
+
Seed.Runner.heading(`Emptying ${r.target.label} …`)
|
|
965
|
+
for j in 0 to r.tables->Array.length - 1 {
|
|
966
|
+
switch r.tables->Array.get(j) {
|
|
967
|
+
| Some(t) =>
|
|
968
|
+
await truncateTable(t)
|
|
969
|
+
Console.log(` truncated ${t}`)
|
|
970
|
+
| None => ()
|
|
971
|
+
}
|
|
861
972
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
973
|
+
for j in 0 to r.bucketCounts->Array.length - 1 {
|
|
974
|
+
switch r.bucketCounts->Array.get(j) {
|
|
975
|
+
| Some((b, _)) =>
|
|
976
|
+
await emptyBucket(b)
|
|
977
|
+
Console.log(` emptied ${b}`)
|
|
978
|
+
| None => ()
|
|
979
|
+
}
|
|
869
980
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
)
|
|
881
|
-
|
|
981
|
+
// Report the count with the store, not just in the run total: "how
|
|
982
|
+
// many images went" is the question an operator is actually asking.
|
|
983
|
+
for j in 0 to r.storeCounts->Array.length - 1 {
|
|
984
|
+
switch r.storeCounts->Array.get(j) {
|
|
985
|
+
| Some((s, count)) =>
|
|
986
|
+
await emptyBucket(s.bucketName, ~prefix=`${s.keyPrefix}/`)
|
|
987
|
+
Console.log(
|
|
988
|
+
` emptied ${s.qualified} — ${count->Int.toString} object(s) removed from ` ++
|
|
989
|
+
`${s.bucketName}/${s.keyPrefix}/`,
|
|
990
|
+
)
|
|
991
|
+
| None => ()
|
|
992
|
+
}
|
|
882
993
|
}
|
|
994
|
+
| None => ()
|
|
883
995
|
}
|
|
884
|
-
| None => ()
|
|
885
996
|
}
|
|
886
|
-
}
|
|
887
997
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
998
|
+
// Prove empty — the inverse of the post-seed verify. Every store that
|
|
999
|
+
// still reads non-zero is named, because "12 items remain" without
|
|
1000
|
+
// saying where is the difference between a diagnosis and a mystery.
|
|
1001
|
+
let refilled = []
|
|
1002
|
+
for i in 0 to resolvedList->Array.length - 1 {
|
|
1003
|
+
switch resolvedList->Array.get(i) {
|
|
1004
|
+
| Some(r) =>
|
|
1005
|
+
for j in 0 to r.tables->Array.length - 1 {
|
|
1006
|
+
switch r.tables->Array.get(j) {
|
|
1007
|
+
| Some(t) =>
|
|
1008
|
+
let left = await countTable(t)
|
|
1009
|
+
if left != 0 {
|
|
1010
|
+
refilled->Array.push((t, left))
|
|
1011
|
+
}
|
|
1012
|
+
| None => ()
|
|
1013
|
+
}
|
|
897
1014
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1015
|
+
for j in 0 to r.bucketCounts->Array.length - 1 {
|
|
1016
|
+
switch r.bucketCounts->Array.get(j) {
|
|
1017
|
+
| Some((b, _)) =>
|
|
1018
|
+
let left = await countBucket(b)
|
|
1019
|
+
if left != 0 {
|
|
1020
|
+
refilled->Array.push((b, left))
|
|
1021
|
+
}
|
|
1022
|
+
| None => ()
|
|
1023
|
+
}
|
|
903
1024
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1025
|
+
// Per-store confirmation, so "every uploaded object is gone" is an
|
|
1026
|
+
// observed fact rather than an inference from a global total.
|
|
1027
|
+
for j in 0 to r.storeCounts->Array.length - 1 {
|
|
1028
|
+
switch r.storeCounts->Array.get(j) {
|
|
1029
|
+
| Some((s, _)) =>
|
|
1030
|
+
let left = await countBucket(s.bucketName, ~prefix=`${s.keyPrefix}/`)
|
|
1031
|
+
if left != 0 {
|
|
1032
|
+
refilled->Array.push((`${s.qualified} (${s.bucketName}/${s.keyPrefix}/)`, left))
|
|
1033
|
+
}
|
|
1034
|
+
Console.log(
|
|
1035
|
+
` verified empty: ${s.qualified} — ${left->Int.toString} object(s) remain under ` ++
|
|
1036
|
+
`${s.bucketName}/${s.keyPrefix}/`,
|
|
1037
|
+
)
|
|
1038
|
+
| None => ()
|
|
1039
|
+
}
|
|
917
1040
|
}
|
|
1041
|
+
| None => ()
|
|
918
1042
|
}
|
|
919
|
-
| None => ()
|
|
920
1043
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1044
|
+
Ok(refilled)
|
|
1045
|
+
} catch {
|
|
1046
|
+
| Seed.Failed(message) => Error(message)
|
|
1047
|
+
| exn =>
|
|
1048
|
+
Error(
|
|
1049
|
+
`the wipe failed: ${exn
|
|
1050
|
+
->JsExn.fromException
|
|
1051
|
+
->Option.flatMap(JsExn.message)
|
|
1052
|
+
->Option.getOr("unknown error")}`,
|
|
927
1053
|
)
|
|
928
1054
|
}
|
|
929
1055
|
|
|
1056
|
+
// Recycle while still held, so the container that eventually serves
|
|
1057
|
+
// traffic is created after the stores were emptied — then hand the stack
|
|
1058
|
+
// back. Both are best-effort and report rather than raise: they run on the
|
|
1059
|
+
// abort path too, where throwing would bury the reason the reset failed.
|
|
1060
|
+
let warnings = if held->Array.length > 0 {
|
|
1061
|
+
Seed.Runner.heading(`Recycling ${held->Array.length->Int.toString} runtime(s) …`)
|
|
1062
|
+
let recycled = await Quiesce.recycle(~client=lambdaClient, ~held)
|
|
1063
|
+
let released = await Quiesce.release(~client=lambdaClient, ~held)
|
|
1064
|
+
Array.concat(recycled, released)
|
|
1065
|
+
} else {
|
|
1066
|
+
[]
|
|
1067
|
+
}
|
|
1068
|
+
if warnings->Array.length > 0 {
|
|
1069
|
+
Console.error("")
|
|
1070
|
+
warnings->Array.forEach(w => Console.error(` warning: ${w}`))
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
switch outcome {
|
|
1074
|
+
| Error(message) => throw(Seed.Failed(message))
|
|
1075
|
+
| Ok(refilled) if refilled->Array.length > 0 =>
|
|
1076
|
+
throw(Seed.Failed(refillMessage(~refilled, ~heldCount=held->Array.length)))
|
|
1077
|
+
| Ok(_) => ()
|
|
1078
|
+
}
|
|
1079
|
+
|
|
930
1080
|
Console.log("")
|
|
931
1081
|
Console.log(`Reset complete — the selected scope of "${stack}" reads empty and is re-seedable.`)
|
|
932
1082
|
NodeProcess.exit(0)
|