@reventlessdev/reventless-seed-aws 1.0.0-alpha.3 → 1.0.0-alpha.30

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 (111) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/.sourcedirs.json +1 -0
  4. package/lib/bs/compiler-info.json +2 -2
  5. package/lib/bs/src/ReventlessSeedAws.ast +0 -0
  6. package/lib/bs/src/ReventlessSeedAws.cmi +0 -0
  7. package/lib/bs/src/ReventlessSeedAws.cmj +0 -0
  8. package/lib/bs/src/ReventlessSeedAws.cmt +0 -0
  9. package/lib/bs/src/ReventlessSeedAws.res +198 -134
  10. package/lib/bs/src/ReventlessSeedAws.res.mjs +57 -31
  11. package/lib/bs/src/ReventlessSeedAws_Quiesce.ast +0 -0
  12. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmi +0 -0
  13. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmj +0 -0
  14. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmt +0 -0
  15. package/lib/bs/src/ReventlessSeedAws_Quiesce.res +307 -0
  16. package/lib/bs/src/ReventlessSeedAws_Quiesce.res.mjs +217 -0
  17. package/lib/bs/src/ReventlessSeedAws_Reconcile.ast +0 -0
  18. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmi +0 -0
  19. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmj +0 -0
  20. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmt +0 -0
  21. package/lib/bs/src/ReventlessSeedAws_Reconcile.res +391 -0
  22. package/lib/bs/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
  23. package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
  24. package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
  25. package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
  26. package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
  27. package/lib/bs/src/ReventlessSeedAws_Reset.res +1103 -0
  28. package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +991 -0
  29. package/lib/bs/tests/EndpointResolutionTest.ast +0 -0
  30. package/lib/bs/tests/EndpointResolutionTest.cmi +0 -0
  31. package/lib/bs/tests/EndpointResolutionTest.cmj +0 -0
  32. package/lib/bs/tests/EndpointResolutionTest.cmt +0 -0
  33. package/lib/bs/tests/EndpointResolutionTest.res +83 -0
  34. package/lib/bs/tests/EndpointResolutionTest.res.mjs +115 -0
  35. package/lib/bs/tests/ObjectStoreReconcileTest.ast +0 -0
  36. package/lib/bs/tests/ObjectStoreReconcileTest.cmi +0 -0
  37. package/lib/bs/tests/ObjectStoreReconcileTest.cmj +0 -0
  38. package/lib/bs/tests/ObjectStoreReconcileTest.cmt +0 -0
  39. package/lib/bs/tests/ObjectStoreReconcileTest.res +83 -0
  40. package/lib/bs/tests/ObjectStoreReconcileTest.res.mjs +102 -0
  41. package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
  42. package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
  43. package/lib/bs/tests/ObjectStoreResetTest.cmj +0 -0
  44. package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
  45. package/lib/bs/tests/ObjectStoreResetTest.res +191 -0
  46. package/lib/bs/tests/ObjectStoreResetTest.res.mjs +211 -0
  47. package/lib/bs/tests/QuiesceResetTest.ast +0 -0
  48. package/lib/bs/tests/QuiesceResetTest.cmi +0 -0
  49. package/lib/bs/tests/QuiesceResetTest.cmj +0 -0
  50. package/lib/bs/tests/QuiesceResetTest.cmt +0 -0
  51. package/lib/bs/tests/QuiesceResetTest.res +169 -0
  52. package/lib/bs/tests/QuiesceResetTest.res.mjs +157 -0
  53. package/lib/ocaml/.compiler.log +2 -2
  54. package/lib/ocaml/EndpointResolutionTest.ast +0 -0
  55. package/lib/ocaml/EndpointResolutionTest.cmi +0 -0
  56. package/lib/ocaml/EndpointResolutionTest.cmj +0 -0
  57. package/lib/ocaml/EndpointResolutionTest.cmt +0 -0
  58. package/lib/ocaml/EndpointResolutionTest.res +83 -0
  59. package/lib/ocaml/ObjectStoreReconcileTest.ast +0 -0
  60. package/lib/ocaml/ObjectStoreReconcileTest.cmi +0 -0
  61. package/lib/ocaml/ObjectStoreReconcileTest.cmj +0 -0
  62. package/lib/ocaml/ObjectStoreReconcileTest.cmt +0 -0
  63. package/lib/ocaml/ObjectStoreReconcileTest.res +83 -0
  64. package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
  65. package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
  66. package/lib/ocaml/ObjectStoreResetTest.cmj +0 -0
  67. package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
  68. package/lib/ocaml/ObjectStoreResetTest.res +191 -0
  69. package/lib/ocaml/QuiesceResetTest.ast +0 -0
  70. package/lib/ocaml/QuiesceResetTest.cmi +0 -0
  71. package/lib/ocaml/QuiesceResetTest.cmj +0 -0
  72. package/lib/ocaml/QuiesceResetTest.cmt +0 -0
  73. package/lib/ocaml/QuiesceResetTest.res +169 -0
  74. package/lib/ocaml/ReventlessSeedAws.ast +0 -0
  75. package/lib/ocaml/ReventlessSeedAws.cmi +0 -0
  76. package/lib/ocaml/ReventlessSeedAws.cmj +0 -0
  77. package/lib/ocaml/ReventlessSeedAws.cmt +0 -0
  78. package/lib/ocaml/ReventlessSeedAws.res +198 -134
  79. package/lib/ocaml/ReventlessSeedAws_Quiesce.ast +0 -0
  80. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmi +0 -0
  81. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmj +0 -0
  82. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmt +0 -0
  83. package/lib/ocaml/ReventlessSeedAws_Quiesce.res +307 -0
  84. package/lib/ocaml/ReventlessSeedAws_Reconcile.ast +0 -0
  85. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmi +0 -0
  86. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmj +0 -0
  87. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmt +0 -0
  88. package/lib/ocaml/ReventlessSeedAws_Reconcile.res +391 -0
  89. package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
  90. package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
  91. package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
  92. package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
  93. package/lib/ocaml/ReventlessSeedAws_Reset.res +1103 -0
  94. package/package.json +7 -4
  95. package/rescript.json +9 -1
  96. package/src/ReventlessSeedAws.res +198 -134
  97. package/src/ReventlessSeedAws.res.mjs +57 -31
  98. package/src/ReventlessSeedAws_Quiesce.res +307 -0
  99. package/src/ReventlessSeedAws_Quiesce.res.mjs +217 -0
  100. package/src/ReventlessSeedAws_Reconcile.res +391 -0
  101. package/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
  102. package/src/ReventlessSeedAws_Reset.res +1103 -0
  103. package/src/ReventlessSeedAws_Reset.res.mjs +991 -0
  104. package/tests/EndpointResolutionTest.res +83 -0
  105. package/tests/EndpointResolutionTest.res.mjs +115 -0
  106. package/tests/ObjectStoreReconcileTest.res +83 -0
  107. package/tests/ObjectStoreReconcileTest.res.mjs +102 -0
  108. package/tests/ObjectStoreResetTest.res +191 -0
  109. package/tests/ObjectStoreResetTest.res.mjs +211 -0
  110. package/tests/QuiesceResetTest.res +169 -0
  111. package/tests/QuiesceResetTest.res.mjs +157 -0
@@ -4,6 +4,8 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Nodechild_process from "node:child_process";
6
6
  import * as Seed$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed.res.mjs";
7
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
+ import * as Seed_Client$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Client.res.mjs";
7
9
  import * as Seed_Prompt$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Prompt.res.mjs";
8
10
  import * as Seed_Connect$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Connect.res.mjs";
9
11
 
@@ -108,7 +110,7 @@ async function resolveStack(projectDir, backend, stack) {
108
110
  return await Seed_Prompt$ReventlessSeed.select("Stack:", stacks.map(s => [
109
111
  s,
110
112
  s
111
- ]), undefined);
113
+ ]), undefined, undefined);
112
114
  }
113
115
 
114
116
  async function fetchConfig(hostShellUrl) {
@@ -152,34 +154,28 @@ function resolveField(envKey, fromSource, human) {
152
154
  };
153
155
  }
154
156
 
155
- async function resolveEndpoints(projectDir, backend, stack) {
156
- let outputs = stackOutputs(projectDir, backend, stack);
157
- let hostShellUrl = Stdlib_Option.flatMap(field(outputs, "hostShellUrl"), asString);
158
- if (hostShellUrl !== undefined) {
159
- let cfg = await fetchConfig(hostShellUrl);
157
+ function endpointsFrom(stack, source) {
158
+ if (source.TAG === "HostShellConfig") {
159
+ let cfg = source._0;
160
160
  let fromCfg = key => Stdlib_Option.flatMap(field(cfg, key), asString);
161
- let endpoint = resolveField("REVENTLESS_GRAPHQL_ENDPOINT", fromCfg("apiEndpoint"), "apiEndpoint");
162
- let uploadEndpoint = resolveField("REVENTLESS_UPLOAD_ENDPOINT", fromCfg("uploadEndpoint"), "uploadEndpoint");
163
- let region = resolveField("AWS_REGION", fromCfg("region"), "region");
164
- let clientId = resolveField("COGNITO_CLIENT_ID", fromCfg("cognitoClientId"), "cognitoClientId");
165
- return [
166
- endpoint,
167
- uploadEndpoint,
168
- region,
169
- clientId
170
- ];
161
+ return {
162
+ graphql: resolveField("REVENTLESS_GRAPHQL_ENDPOINT", fromCfg("apiEndpoint"), "apiEndpoint"),
163
+ cognitoRegion: resolveField("AWS_REGION", fromCfg("region"), "region"),
164
+ cognitoClientId: resolveField("COGNITO_CLIENT_ID", fromCfg("cognitoClientId"), "cognitoClientId")
165
+ };
171
166
  }
167
+ let outputs = source._0;
172
168
  let out = key => Stdlib_Option.flatMap(field(outputs, key), asString);
173
169
  let match = Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_GRAPHQL_ENDPOINT");
174
170
  let match$1 = out("domainMergedApiEndpoint");
175
171
  let match$2 = out("domainApiEndpoint");
176
- let endpoint$1;
172
+ let tmp;
177
173
  if (match !== undefined) {
178
- endpoint$1 = match;
174
+ tmp = match;
179
175
  } else if (match$1 !== undefined) {
180
- endpoint$1 = match$1;
176
+ tmp = match$1;
181
177
  } else if (match$2 !== undefined) {
182
- endpoint$1 = match$2;
178
+ tmp = match$2;
183
179
  } else {
184
180
  throw {
185
181
  RE_EXN_ID: Seed$ReventlessSeed.Failed,
@@ -187,15 +183,24 @@ async function resolveEndpoints(projectDir, backend, stack) {
187
183
  Error: new Error()
188
184
  };
189
185
  }
190
- let region$1 = resolveField("AWS_REGION", out("cognitoRegion"), "cognitoRegion");
191
- let clientId$1 = resolveField("COGNITO_CLIENT_ID", out("cognitoUserPoolClientId"), "cognitoUserPoolClientId");
192
- let uploadEndpoint$1 = Stdlib_Option.getOr(Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_UPLOAD_ENDPOINT"), "");
193
- return [
194
- endpoint$1,
195
- uploadEndpoint$1,
196
- region$1,
197
- clientId$1
198
- ];
186
+ return {
187
+ graphql: tmp,
188
+ cognitoRegion: resolveField("AWS_REGION", out("cognitoRegion"), "cognitoRegion"),
189
+ cognitoClientId: resolveField("COGNITO_CLIENT_ID", out("cognitoUserPoolClientId"), "cognitoUserPoolClientId")
190
+ };
191
+ }
192
+
193
+ async function resolveEndpoints(projectDir, backend, stack) {
194
+ let outputs = stackOutputs(projectDir, backend, stack);
195
+ let hostShellUrl = Stdlib_Option.flatMap(field(outputs, "hostShellUrl"), asString);
196
+ let source = hostShellUrl !== undefined ? ({
197
+ TAG: "HostShellConfig",
198
+ _0: await fetchConfig(hostShellUrl)
199
+ }) : ({
200
+ TAG: "StackOutputs",
201
+ _0: outputs
202
+ });
203
+ return endpointsFrom(stack, source);
199
204
  }
200
205
 
201
206
  function cognito(region, clientId) {
@@ -276,14 +281,33 @@ function cognito(region, clientId) {
276
281
  };
277
282
  }
278
283
 
284
+ async function setActiveRole(client, role) {
285
+ let argument = role !== undefined ? JSON.stringify(role) : "null";
286
+ try {
287
+ await Seed_Client$ReventlessSeed.gql(client, `mutation { Platform_SetActiveRole(activeRole: ` + argument + `) { activeRole availableRoles } }`, "Platform_SetActiveRole");
288
+ return;
289
+ } catch (raw_message) {
290
+ let message = Primitive_exceptions.internalToException(raw_message);
291
+ if (message.RE_EXN_ID === Seed$ReventlessSeed.Failed) {
292
+ let message$1 = message._1;
293
+ throw {
294
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
295
+ _1: message$1.includes("Platform_SetActiveRole") ? `this deployment offers no role switch — \`Platform_SetActiveRole\` is a split-API field, and a unified-mode API does not carry it. Change the role in the host shell instead.\n ` + message$1 : message$1,
296
+ Error: new Error()
297
+ };
298
+ }
299
+ throw message;
300
+ }
301
+ }
302
+
279
303
  function connect($staropt$star, stack, backend, param) {
280
304
  return async () => {
281
305
  let projectDir = $staropt$star !== undefined ? $staropt$star : ".";
282
306
  let url = Seed_Prompt$ReventlessSeed.envValue("SEED_PULUMI_BACKEND");
283
307
  let backend$1 = url !== undefined ? url : backend;
284
308
  let stackName = await resolveStack(projectDir, backend$1, stack);
285
- let match = await resolveEndpoints(projectDir, backend$1, stackName);
286
- return await Seed_Connect$ReventlessSeed.make(stackName, match[0], match[1], cognito(match[2], match[3]), undefined);
309
+ let eps = await resolveEndpoints(projectDir, backend$1, stackName);
310
+ return await Seed_Connect$ReventlessSeed.make(stackName, eps.graphql, cognito(eps.cognitoRegion, eps.cognitoClientId), setActiveRole, undefined);
287
311
  };
288
312
  }
289
313
 
@@ -298,8 +322,10 @@ export {
298
322
  resolveStack,
299
323
  fetchConfig,
300
324
  resolveField,
325
+ endpointsFrom,
301
326
  resolveEndpoints,
302
327
  cognito,
328
+ setActiveRole,
303
329
  connect,
304
330
  }
305
331
  /* node:child_process Not a pure module */
@@ -0,0 +1,307 @@
1
+ // Taking a deployed stack's runtimes out of service for the length of a wipe,
2
+ // and putting them back exactly as they were.
3
+ //
4
+ // Why a wipe needs this at all: a truncate is not durable while the runtimes
5
+ // that own the data are running. A slice runtime keeps its TODO list in a
6
+ // module-level dict for the lifetime of its execution environment and re-saves
7
+ // every row it holds at the end of each invocation — including the scheduled
8
+ // sweep, which carries no events. Emptying the table under a warm container
9
+ // therefore succeeds and is then undone, byte for byte, by the next sweep: the
10
+ // rows are not recomputed from anything, so their ids and timestamps come back
11
+ // identical. Emptying the upstream stores first does not help, because nothing
12
+ // upstream is read.
13
+ //
14
+ // The answer is to remove the contention rather than to outrun it, which takes
15
+ // two distinct steps, in this order:
16
+ //
17
+ // 1. HOLD — reserve 0 concurrency on every function in scope, so no new
18
+ // invocation can start while the wipe and its verification run. This alone
19
+ // is not enough: a reservation stops invocations, it does not discard the
20
+ // execution environment that holds the state.
21
+ // 2. RECYCLE — change each function's configuration once. A configuration
22
+ // change is the documented way to make Lambda discard existing execution
23
+ // environments, so whatever a warm container was holding is gone before it
24
+ // can be invoked again. Done while still held, so the container that
25
+ // eventually replaces it starts from the emptied store.
26
+ //
27
+ // The recycle is a round trip — a marker variable added, then the original map
28
+ // put back — so the function ends the run byte-identical to how it started and
29
+ // `pulumi preview` reports no drift. The second update recycles again, which is
30
+ // harmless: it happens while the hold is still on and the store is already
31
+ // empty.
32
+ //
33
+ // Everything acquired here is released on the failure path as well as the
34
+ // success path, and the release never throws: a reset that aborted mid-wipe
35
+ // must still hand the stack back. Release failures come back as warnings for
36
+ // the caller to print, because a function left at zero concurrency is a stack
37
+ // left switched off, and that has to be said out loud rather than raised as one
38
+ // more exception on top of whatever already went wrong.
39
+
40
+ module Lambda = AwsSdk.Lambda
41
+
42
+ /** The variable a recycle adds and then removes. Named for what it is, so an
43
+ operator who finds one left behind by a killed run knows it is inert and can
44
+ be deleted (or left for the next `pulumi up` to remove). */
45
+ let markerKey = "REVENTLESS_RESET_RECYCLE"
46
+
47
+ /** What one function looked like before the reset touched it. */
48
+ type held = {
49
+ functionName: string,
50
+ /** `None` = the function had no reservation at all. That is a different state
51
+ from a reservation of 0, and restoring it needs
52
+ `DeleteFunctionConcurrency` rather than a put. */
53
+ priorReserved: option<int>,
54
+ /** The variable map as it was, flattened: a function with no `Environment`
55
+ block at all reads as an empty map and is restored to an empty map. */
56
+ priorVariables: dict<string>,
57
+ }
58
+
59
+ /** The Lambda control plane is rate-limited well below the data plane, and a
60
+ reset touches every function in a project at once. Six at a time keeps a
61
+ forty-function stack under a minute per pass without risking a throttle. */
62
+ let concurrency = 6
63
+
64
+ let mapBounded = async (items: array<'a>, ~limit: int, f: 'a => promise<'b>): array<'b> => {
65
+ let results = Array.make(~length=items->Array.length, None)
66
+ let next = ref(0)
67
+ let worker = async () => {
68
+ let running = ref(true)
69
+ while running.contents {
70
+ let i = next.contents
71
+ if i >= items->Array.length {
72
+ running := false
73
+ } else {
74
+ next := i + 1
75
+ let r = await f(items->Array.getUnsafe(i))
76
+ results->Array.set(i, Some(r))
77
+ }
78
+ }
79
+ }
80
+ let _ = await Array.fromInitializer(~length=Math.Int.min(limit, items->Array.length), _ =>
81
+ worker()
82
+ )->Promise.all
83
+ results->Array.filterMap(x => x)
84
+ }
85
+
86
+ let errorText = (exn: exn): string =>
87
+ exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown error")
88
+
89
+ /** A throttle and a missing permission both surface as a failure to hold, and
90
+ they call for opposite responses — wait and re-run, versus go and fix the
91
+ policy. The control plane says "Rate exceeded"; the SDK says
92
+ "TooManyRequestsException". Match on either. */
93
+ let isThrottle = (reason: string): bool => {
94
+ let r = reason->String.toLowerCase
95
+ r->String.includes("rate exceeded") ||
96
+ r->String.includes("toomanyrequests") ||
97
+ r->String.includes("throttl")
98
+ }
99
+
100
+ // An update is rejected while another is still settling, and a stack that was
101
+ // deployed moments ago can still be settling. Poll rather than assume.
102
+ let awaitSettled = async (~client, ~functionName: string): unit => {
103
+ let deadline = 60
104
+ let rec poll = async (attempt: int): unit =>
105
+ if attempt < deadline {
106
+ let out = await Lambda.GetFunctionConfigurationCommand.send(
107
+ client,
108
+ Lambda.GetFunctionConfigurationCommand.make({functionName: functionName}),
109
+ )
110
+ switch out.lastUpdateStatus {
111
+ | Some("InProgress") =>
112
+ await ReventlessSeed.Seed.Client.sleep(1000)
113
+ await poll(attempt + 1)
114
+ | _ => ()
115
+ }
116
+ }
117
+ await poll(0)
118
+ }
119
+
120
+ let updateVariables = async (~client, ~functionName: string, ~variables: dict<string>): unit => {
121
+ await awaitSettled(~client, ~functionName)
122
+ let _ = await Lambda.UpdateFunctionConfigurationCommand.send(
123
+ client,
124
+ Lambda.UpdateFunctionConfigurationCommand.make({
125
+ functionName,
126
+ environment: {variables: variables},
127
+ }),
128
+ )
129
+ await awaitSettled(~client, ~functionName)
130
+ }
131
+
132
+ /** Put one function's reservation back exactly as it was — a value it had, or no
133
+ reservation at all. Never throws: a function left reserved at 0 is a function
134
+ left switched off, which has to be said rather than raised. */
135
+ let restoreConcurrency = async (~client, ~h: held): option<string> =>
136
+ switch await (
137
+ switch h.priorReserved {
138
+ | Some(n) =>
139
+ Lambda.PutFunctionConcurrencyCommand.send(
140
+ client,
141
+ Lambda.PutFunctionConcurrencyCommand.make({
142
+ functionName: h.functionName,
143
+ reservedConcurrentExecutions: n,
144
+ }),
145
+ )->Promise.thenResolve(_ => ())
146
+ | None =>
147
+ Lambda.DeleteFunctionConcurrencyCommand.send(
148
+ client,
149
+ Lambda.DeleteFunctionConcurrencyCommand.make({functionName: h.functionName}),
150
+ )->Promise.thenResolve(_ => ())
151
+ }
152
+ ) {
153
+ | () => None
154
+ | exception exn =>
155
+ Some(
156
+ `LEFT SWITCHED OFF: could not restore concurrency on ${h.functionName} (${errorText(
157
+ exn,
158
+ )}). ` ++ `It is still reserved at 0 and will not run until that is undone.`,
159
+ )
160
+ }
161
+
162
+ /**
163
+ Hand the stack back: original environment first (which also drops the marker),
164
+ then the original concurrency.
165
+
166
+ That order matters. The environment restore is itself a configuration change, so
167
+ it recycles a second time; doing it while the function is still held means the
168
+ container that finally serves traffic is created after the restore, not during
169
+ it.
170
+
171
+ Never throws, for either step and for any function — this runs on the abort path
172
+ too, where an exception would replace the reason the reset failed with a
173
+ secondary one.
174
+ */
175
+ let release = async (~client, ~held: array<held>): array<string> => {
176
+ let warnings = await held->mapBounded(~limit=concurrency, async h => {
177
+ let envWarning = switch await updateVariables(
178
+ ~client,
179
+ ~functionName=h.functionName,
180
+ ~variables=h.priorVariables,
181
+ ) {
182
+ | () => None
183
+ | exception exn =>
184
+ Some(
185
+ `could not restore the environment of ${h.functionName} (${errorText(
186
+ exn,
187
+ )}) — it may still ` ++
188
+ `carry ${markerKey}, which is inert and is removed by the next deploy.`,
189
+ )
190
+ }
191
+ let concurrencyWarning = await restoreConcurrency(~client, ~h)
192
+ [envWarning, concurrencyWarning]->Array.filterMap(w => w)
193
+ })
194
+ warnings->Array.flat
195
+ }
196
+
197
+ /**
198
+ Discard every held function's execution environments, so nothing that loaded
199
+ state before the wipe can write it back afterwards.
200
+
201
+ Never throws. A function that cannot be recycled is the one case where the reset
202
+ genuinely cannot promise the store stays empty, so it comes back as a warning
203
+ naming the function — the caller reports it rather than swallowing it.
204
+ */
205
+ let recycle = async (~client, ~held: array<held>): array<string> => {
206
+ let warnings = await held->mapBounded(~limit=concurrency, async h => {
207
+ let marked = h.priorVariables->Dict.copy
208
+ marked->Dict.set(markerKey, "1")
209
+ switch await updateVariables(~client, ~functionName=h.functionName, ~variables=marked) {
210
+ | () => None
211
+ | exception exn =>
212
+ Some(
213
+ `could not recycle ${h.functionName} (${errorText(
214
+ exn,
215
+ )}) — a warm container may still hold ` ++ `pre-wipe state and write it back. Redeploy or update that function to clear it.`,
216
+ )
217
+ }
218
+ })
219
+ warnings->Array.filterMap(w => w)
220
+ }
221
+
222
+ /**
223
+ Take every named function out of service, recording what to put back.
224
+
225
+ Throws if any function cannot be held — before a single delete has been issued,
226
+ so a reset that cannot hold the platform still has the option of not starting.
227
+ The message names the IAM actions involved, because a missing permission is much
228
+ the likeliest cause and "AccessDenied" on its own does not say which one.
229
+
230
+ Crucially, it hands back whatever it did manage to take before throwing. The
231
+ functions are held concurrently, so a failure part-way through the batch leaves
232
+ earlier ones already at zero — and a reset that aborted while silently switching
233
+ off half a stack would be a worse failure than the one it was reporting. Only the
234
+ reservations are rolled back: this runs before `recycle`, so no environment has
235
+ been touched yet.
236
+ */
237
+ let hold = async (~client, ~functionNames: array<string>): array<held> => {
238
+ let attempts = await functionNames->mapBounded(~limit=concurrency, async functionName =>
239
+ switch await (
240
+ async () => {
241
+ let concurrencyOut = await Lambda.GetFunctionConcurrencyCommand.send(
242
+ client,
243
+ Lambda.GetFunctionConcurrencyCommand.make({functionName: functionName}),
244
+ )
245
+ let configOut = await Lambda.GetFunctionConfigurationCommand.send(
246
+ client,
247
+ Lambda.GetFunctionConfigurationCommand.make({functionName: functionName}),
248
+ )
249
+ let held = {
250
+ functionName,
251
+ priorReserved: concurrencyOut.reservedConcurrentExecutions,
252
+ priorVariables: configOut.environment
253
+ ->Option.flatMap(e => e.variables)
254
+ ->Option.getOr(Dict.make()),
255
+ }
256
+ let _ = await Lambda.PutFunctionConcurrencyCommand.send(
257
+ client,
258
+ Lambda.PutFunctionConcurrencyCommand.make({
259
+ functionName,
260
+ reservedConcurrentExecutions: 0,
261
+ }),
262
+ )
263
+ held
264
+ }
265
+ )() {
266
+ | held => Ok(held)
267
+ | exception exn => Error((functionName, errorText(exn)))
268
+ }
269
+ )
270
+
271
+ let taken = attempts->Array.filterMap(a =>
272
+ switch a {
273
+ | Ok(h) => Some(h)
274
+ | Error(_) => None
275
+ }
276
+ )
277
+ switch attempts->Array.findMap(a =>
278
+ switch a {
279
+ | Error(failure) => Some(failure)
280
+ | Ok(_) => None
281
+ }
282
+ ) {
283
+ | None => taken
284
+ | Some((functionName, reason)) =>
285
+ let rollback = await taken->mapBounded(~limit=concurrency, h => restoreConcurrency(~client, ~h))
286
+ let stranded = rollback->Array.filterMap(w => w)
287
+ let advice = if reason->isThrottle {
288
+ `The Lambda control plane throttled the hold. That budget is per account and region and is ` ++
289
+ `shared with anything else using it — a deploy, an inspector sync, another stack in the same ` ++
290
+ `region — so this is transient and nothing is wrong with the credentials. Wait for whatever ` ++ `else is running to finish and re-run.`
291
+ } else {
292
+ `The reset holds every runtime in scope for the length of the wipe, which needs ` ++
293
+ `lambda:GetFunctionConcurrency, lambda:GetFunctionConfiguration, lambda:PutFunctionConcurrency, ` ++ `lambda:DeleteFunctionConcurrency and lambda:UpdateFunctionConfiguration.`
294
+ }
295
+ throw(
296
+ ReventlessSeed.Seed.Failed(
297
+ `could not hold Lambda function ${functionName} at zero concurrency (${reason}) — nothing was ` ++
298
+ `deleted. ${advice} Set SEED_RESET_NO_QUIESCE=1 to wipe without the hold — but a running ` ++
299
+ `runtime can then write back what the wipe removes.` ++
300
+ switch stranded {
301
+ | [] => ""
302
+ | messages => `\n\n ` ++ messages->Array.join("\n ")
303
+ },
304
+ ),
305
+ )
306
+ }
307
+ }
@@ -0,0 +1,217 @@
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_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as Seed$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed.res.mjs";
8
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
9
+ import * as ClientLambda from "@aws-sdk/client-lambda";
10
+ import * as Seed_Client$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Client.res.mjs";
11
+
12
+ let markerKey = "REVENTLESS_RESET_RECYCLE";
13
+
14
+ async function mapBounded(items, limit, f) {
15
+ let results = Stdlib_Array.make(items.length, undefined);
16
+ let next = {
17
+ contents: 0
18
+ };
19
+ let worker = async () => {
20
+ let running = true;
21
+ while (running) {
22
+ let i = next.contents;
23
+ if (i >= items.length) {
24
+ running = false;
25
+ } else {
26
+ next.contents = i + 1 | 0;
27
+ let r = await f(items[i]);
28
+ results[i] = Primitive_option.some(r);
29
+ }
30
+ };
31
+ };
32
+ await Promise.all(Stdlib_Array.fromInitializer(Math.min(limit, items.length), param => worker()));
33
+ return Stdlib_Array.filterMap(results, x => x);
34
+ }
35
+
36
+ function errorText(exn) {
37
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown error");
38
+ }
39
+
40
+ function isThrottle(reason) {
41
+ let r = reason.toLowerCase();
42
+ if (r.includes("rate exceeded") || r.includes("toomanyrequests")) {
43
+ return true;
44
+ } else {
45
+ return r.includes("throttl");
46
+ }
47
+ }
48
+
49
+ async function awaitSettled(client, functionName) {
50
+ let poll = async attempt => {
51
+ if (attempt >= 60) {
52
+ return;
53
+ }
54
+ let out = await client.send(new ClientLambda.GetFunctionConfigurationCommand({
55
+ FunctionName: functionName
56
+ }));
57
+ let match = out.LastUpdateStatus;
58
+ if (match === undefined) {
59
+ return;
60
+ }
61
+ if (match !== "InProgress") {
62
+ return;
63
+ }
64
+ await Seed_Client$ReventlessSeed.sleep(1000);
65
+ return await poll(attempt + 1 | 0);
66
+ };
67
+ return await poll(0);
68
+ }
69
+
70
+ async function updateVariables(client, functionName, variables) {
71
+ await awaitSettled(client, functionName);
72
+ await client.send(new ClientLambda.UpdateFunctionConfigurationCommand({
73
+ FunctionName: functionName,
74
+ Environment: {
75
+ Variables: variables
76
+ }
77
+ }));
78
+ return await awaitSettled(client, functionName);
79
+ }
80
+
81
+ async function restoreConcurrency(client, h) {
82
+ try {
83
+ let n = h.priorReserved;
84
+ await (
85
+ n !== undefined ? client.send(new ClientLambda.PutFunctionConcurrencyCommand({
86
+ FunctionName: h.functionName,
87
+ ReservedConcurrentExecutions: n
88
+ })).then(param => {}) : client.send(new ClientLambda.DeleteFunctionConcurrencyCommand({
89
+ FunctionName: h.functionName
90
+ })).then(param => {})
91
+ );
92
+ return;
93
+ } catch (raw_exn) {
94
+ let exn = Primitive_exceptions.internalToException(raw_exn);
95
+ return `LEFT SWITCHED OFF: could not restore concurrency on ` + h.functionName + ` (` + errorText(exn) + `). It is still reserved at 0 and will not run until that is undone.`;
96
+ }
97
+ }
98
+
99
+ async function release(client, held) {
100
+ return (await mapBounded(held, 6, async h => {
101
+ let envWarning;
102
+ try {
103
+ await updateVariables(client, h.functionName, h.priorVariables);
104
+ envWarning = undefined;
105
+ } catch (raw_exn) {
106
+ let exn = Primitive_exceptions.internalToException(raw_exn);
107
+ envWarning = `could not restore the environment of ` + h.functionName + ` (` + errorText(exn) + `) — it may still ` + (`carry ` + markerKey + `, which is inert and is removed by the next deploy.`);
108
+ }
109
+ let concurrencyWarning = await restoreConcurrency(client, h);
110
+ return Stdlib_Array.filterMap([
111
+ envWarning,
112
+ concurrencyWarning
113
+ ], w => w);
114
+ })).flat();
115
+ }
116
+
117
+ async function recycle(client, held) {
118
+ let warnings = await mapBounded(held, 6, async h => {
119
+ let marked = Object.assign({}, h.priorVariables);
120
+ marked[markerKey] = "1";
121
+ try {
122
+ await updateVariables(client, h.functionName, marked);
123
+ return;
124
+ } catch (raw_exn) {
125
+ let exn = Primitive_exceptions.internalToException(raw_exn);
126
+ return `could not recycle ` + h.functionName + ` (` + errorText(exn) + `) — a warm container may still hold pre-wipe state and write it back. Redeploy or update that function to clear it.`;
127
+ }
128
+ });
129
+ return Stdlib_Array.filterMap(warnings, w => w);
130
+ }
131
+
132
+ async function hold(client, functionNames) {
133
+ let attempts = await mapBounded(functionNames, 6, async functionName => {
134
+ let held;
135
+ try {
136
+ held = await (async () => {
137
+ let concurrencyOut = await client.send(new ClientLambda.GetFunctionConcurrencyCommand({
138
+ FunctionName: functionName
139
+ }));
140
+ let configOut = await client.send(new ClientLambda.GetFunctionConfigurationCommand({
141
+ FunctionName: functionName
142
+ }));
143
+ let held_priorReserved = concurrencyOut.ReservedConcurrentExecutions;
144
+ let held_priorVariables = Stdlib_Option.getOr(Stdlib_Option.flatMap(configOut.Environment, e => e.Variables), {});
145
+ let held = {
146
+ functionName: functionName,
147
+ priorReserved: held_priorReserved,
148
+ priorVariables: held_priorVariables
149
+ };
150
+ await client.send(new ClientLambda.PutFunctionConcurrencyCommand({
151
+ FunctionName: functionName,
152
+ ReservedConcurrentExecutions: 0
153
+ }));
154
+ return held;
155
+ })();
156
+ } catch (raw_exn) {
157
+ let exn = Primitive_exceptions.internalToException(raw_exn);
158
+ return {
159
+ TAG: "Error",
160
+ _0: [
161
+ functionName,
162
+ errorText(exn)
163
+ ]
164
+ };
165
+ }
166
+ return {
167
+ TAG: "Ok",
168
+ _0: held
169
+ };
170
+ });
171
+ let taken = Stdlib_Array.filterMap(attempts, a => {
172
+ if (a.TAG === "Ok") {
173
+ return a._0;
174
+ }
175
+ });
176
+ let match = Stdlib_Array.findMap(attempts, a => {
177
+ if (a.TAG === "Ok") {
178
+ return;
179
+ } else {
180
+ return a._0;
181
+ }
182
+ });
183
+ if (match === undefined) {
184
+ return taken;
185
+ }
186
+ let reason = match[1];
187
+ let rollback = await mapBounded(taken, 6, h => restoreConcurrency(client, h));
188
+ let stranded = Stdlib_Array.filterMap(rollback, w => w);
189
+ let advice = isThrottle(reason) ? `The Lambda control plane throttled the hold. That budget is per account and region and is shared with anything else using it — a deploy, an inspector sync, another stack in the same region — so this is transient and nothing is wrong with the credentials. Wait for whatever else is running to finish and re-run.` : `The reset holds every runtime in scope for the length of the wipe, which needs lambda:GetFunctionConcurrency, lambda:GetFunctionConfiguration, lambda:PutFunctionConcurrency, lambda:DeleteFunctionConcurrency and lambda:UpdateFunctionConfiguration.`;
190
+ throw {
191
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
192
+ _1: `could not hold Lambda function ` + match[0] + ` at zero concurrency (` + reason + `) — nothing was ` + (`deleted. ` + advice + ` Set SEED_RESET_NO_QUIESCE=1 to wipe without the hold — but a running `) + `runtime can then write back what the wipe removes.` + (
193
+ stranded.length !== 0 ? `\n\n ` + stranded.join("\n ") : ""
194
+ ),
195
+ Error: new Error()
196
+ };
197
+ }
198
+
199
+ let Lambda;
200
+
201
+ let concurrency = 6;
202
+
203
+ export {
204
+ Lambda,
205
+ markerKey,
206
+ concurrency,
207
+ mapBounded,
208
+ errorText,
209
+ isThrottle,
210
+ awaitSettled,
211
+ updateVariables,
212
+ restoreConcurrency,
213
+ release,
214
+ recycle,
215
+ hold,
216
+ }
217
+ /* Stdlib_JsExn Not a pure module */