@reventlessdev/reventless-core 3.0.0-alpha.294 → 3.0.0-alpha.296
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 +39 -0
- package/package.json +8 -8
- package/src/admin/Platform_ComponentDefinitionsApi.res +3 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/components/AutomationSlice/AutomationSlice.res +2 -0
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +133 -8
- package/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs +89 -7
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +136 -3
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +122 -4
- package/src/components/AutomationSlice/AutomationSlice_Route.res +3 -1
- package/src/components/AutomationSlice/AutomationSlice_Route.res.mjs +2 -1
- package/src/components/AutomationSlice/AutomationSlice_Schedules.res +149 -0
- package/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs +193 -0
- package/src/components/Dcb/Dcb_Builder.res +28 -4
- package/src/components/Dcb/Dcb_Builder.res.mjs +27 -5
- package/src/plugin/component/Plugin_Builder.res +10 -3
- package/src/plugin/component/Plugin_Builder.res.mjs +8 -2
- package/src/plugin/component/Plugin_Structure.res +7 -0
- package/src/plugin/component/Plugin_Structure.res.mjs +5 -2
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res +1 -1
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +1 -1
- package/src/util/ScheduleOps.res +1 -55
- package/src/util/ScheduleOps.res.mjs +10 -57
- package/src/util/ScheduleTime.res +266 -0
- package/src/util/ScheduleTime.res.mjs +451 -0
- package/tests/admin/AdminApiSchemaDriftTest.res +1 -0
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +4 -1
- package/tests/automationslice/AutomationSliceSchedulesTest.res +143 -0
- package/tests/automationslice/AutomationSliceSchedulesTest.res.mjs +247 -0
- package/tests/util/ScheduleTimeTest.res +256 -0
- package/tests/util/ScheduleTimeTest.res.mjs +532 -0
|
@@ -93,8 +93,35 @@ type runtime = {
|
|
|
93
93
|
todoItems: Dict.t<todoRow>,
|
|
94
94
|
phase1: phase1,
|
|
95
95
|
phase2: phase2,
|
|
96
|
+
/** The slice's schedules, by key: what its schedules store holds. */
|
|
97
|
+
schedules: Dict.t<AutomationSlice_Schedules.row>,
|
|
98
|
+
/** What phase 1 changed in `schedules` since the last call, for the platform to
|
|
99
|
+
follow once the rows are stored. */
|
|
100
|
+
takeScheduleChanges: unit => array<AutomationSlice_Schedules.platformChange>,
|
|
101
|
+
/** Folds the given events' `reschedule` into empty rows, in order: a rebuild. */
|
|
102
|
+
rebuildSchedules: array<JSON.t> => unit,
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
/** A tick as the runtime receives an event: an envelope from the `Clock`
|
|
106
|
+
source, recorded at the scheduled time. */
|
|
107
|
+
let tickEvent = (~key, ~at) =>
|
|
108
|
+
JSON.Encode.object(
|
|
109
|
+
Dict.fromArray([
|
|
110
|
+
("id", key->JSON.Encode.string),
|
|
111
|
+
(
|
|
112
|
+
"meta",
|
|
113
|
+
{
|
|
114
|
+
...Message.generateMeta(~service=Reventless.Clock.name),
|
|
115
|
+
time: at->Date.fromTime->Date.toISOString,
|
|
116
|
+
}->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema),
|
|
117
|
+
),
|
|
118
|
+
(
|
|
119
|
+
"event",
|
|
120
|
+
Reventless.Clock.Tick({key, at})->Reventless.Util_Sury.toJson(Reventless.Clock.eventSchema),
|
|
121
|
+
),
|
|
122
|
+
]),
|
|
123
|
+
)
|
|
124
|
+
|
|
98
125
|
module type T = {
|
|
99
126
|
module Spec: Reventless.AutomationSlice.Spec
|
|
100
127
|
module Automation: Reventless.AutomationSlice.Automation with module Spec := Spec
|
|
@@ -108,6 +135,10 @@ module type T = {
|
|
|
108
135
|
|
|
109
136
|
/** Phase 2: process all pending items, publishing commands via publishJsons. */
|
|
110
137
|
let phase2: phase2
|
|
138
|
+
|
|
139
|
+
let schedules: Dict.t<AutomationSlice_Schedules.row>
|
|
140
|
+
let takeScheduleChanges: unit => array<AutomationSlice_Schedules.platformChange>
|
|
141
|
+
let rebuildSchedules: array<JSON.t> => unit
|
|
111
142
|
}
|
|
112
143
|
|
|
113
144
|
module Make = (
|
|
@@ -118,6 +149,20 @@ module Make = (
|
|
|
118
149
|
module Automation = Automation
|
|
119
150
|
|
|
120
151
|
let todoItems: Dict.t<todoRow> = Dict.make()
|
|
152
|
+
let schedules: Dict.t<AutomationSlice_Schedules.row> = Dict.make()
|
|
153
|
+
let scheduleChanges: ref<array<AutomationSlice_Schedules.platformChange>> = ref([])
|
|
154
|
+
|
|
155
|
+
let takeScheduleChanges = () => {
|
|
156
|
+
let changes = scheduleChanges.contents
|
|
157
|
+
scheduleChanges := []
|
|
158
|
+
changes
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Only a slice that maps the Clock can receive the ticks a schedule sends.
|
|
162
|
+
let readsClock =
|
|
163
|
+
Automation.mappings->Array.some((module(M: Automation.Mapping)) =>
|
|
164
|
+
M.sourceName == Reventless.Clock.name
|
|
165
|
+
)
|
|
121
166
|
|
|
122
167
|
let now = () => Date.make()->Date.toISOString
|
|
123
168
|
|
|
@@ -133,6 +178,10 @@ module Make = (
|
|
|
133
178
|
// the slice here makes the target's own events unmatchable, silently.
|
|
134
179
|
let makeMeta = (): Reventless.Message.meta => Message.generateMeta(~service=Spec.targetName)
|
|
135
180
|
|
|
181
|
+
/** How long a row may stay `Processing` before its command is sent again. */
|
|
182
|
+
let processingTimeoutMs =
|
|
183
|
+
15. *. 60. *. 1000.
|
|
184
|
+
|
|
136
185
|
/** Record a failed attempt. The row is marked `Abandoned` at the moment the
|
|
137
186
|
budget runs out rather than being left to fall out of the next pass's
|
|
138
187
|
filter, so a reader never has to compare `retryCount` against a constant
|
|
@@ -168,6 +217,13 @@ module Make = (
|
|
|
168
217
|
|
|
169
218
|
module Route = AutomationSlice_Route.Make(Spec, Automation)
|
|
170
219
|
|
|
220
|
+
// An event's recorded time, from its envelope's ISO `meta.time`.
|
|
221
|
+
let recordedAtOf = time =>
|
|
222
|
+
switch Date.fromString(time)->Date.getTime {
|
|
223
|
+
| millis if Float.isNaN(millis) => None
|
|
224
|
+
| millis => Some(millis)
|
|
225
|
+
}
|
|
226
|
+
|
|
171
227
|
let apply = ({collected, resolved}: AutomationSlice_Route.routed<Spec.todoItem>) => {
|
|
172
228
|
// Collect — append new items keyed by ID; first writer wins (idempotent).
|
|
173
229
|
collected->Array.forEach(((id, item)) => {
|
|
@@ -194,9 +250,39 @@ module Make = (
|
|
|
194
250
|
)
|
|
195
251
|
}
|
|
196
252
|
|
|
197
|
-
let
|
|
253
|
+
let reschedule = (
|
|
254
|
+
routed: array<AutomationSlice_Route.routed<Spec.todoItem>>,
|
|
255
|
+
~setBy,
|
|
256
|
+
~recordedAt,
|
|
257
|
+
) =>
|
|
258
|
+
routed->Array.forEach(r =>
|
|
259
|
+
r.rescheduled->Array.forEach(change =>
|
|
260
|
+
if !readsClock {
|
|
261
|
+
EffectLogger.logError(
|
|
262
|
+
~comp=`AutomationSlice(${Spec.name})`,
|
|
263
|
+
`a mapping changed a schedule, but no mapping reads the Clock, so no tick would reach the slice; the change is skipped`,
|
|
264
|
+
)->Effect.runSync
|
|
265
|
+
} else {
|
|
266
|
+
switch schedules->AutomationSlice_Schedules.apply(change, ~setBy, ~recordedAt) {
|
|
267
|
+
| Ok(platformChange) =>
|
|
268
|
+
scheduleChanges := scheduleChanges.contents->Array.concat([platformChange])
|
|
269
|
+
| Error(message) =>
|
|
270
|
+
EffectLogger.logError(
|
|
271
|
+
~comp=`AutomationSlice(${Spec.name})`,
|
|
272
|
+
`${message}; the change is skipped`,
|
|
273
|
+
)->Effect.runSync
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
let phase1 = (events: array<JSON.t>, sliceCtx: Reventless.AutomationSlice.context) => {
|
|
198
280
|
events->Array.forEach(json => {
|
|
199
281
|
let context = json->Message.decode(Reventless.Message.contextSchema)
|
|
282
|
+
let ctx = switch recordedAtOf(context.meta.time) {
|
|
283
|
+
| Some(recordedAt) => {...sliceCtx, recordedAt}
|
|
284
|
+
| None => sliceCtx
|
|
285
|
+
}
|
|
200
286
|
let sourceName = context.meta.service
|
|
201
287
|
// Events arrive as `{id, meta, event}` envelopes (see Message.composeEventJson').
|
|
202
288
|
// Per-mapping decoders work on the inner `event` payload (TAG + fields at
|
|
@@ -206,16 +292,63 @@ module Make = (
|
|
|
206
292
|
| None => json
|
|
207
293
|
}
|
|
208
294
|
// Multiple mappings can share a sourceName; all matching dispatches run.
|
|
209
|
-
Route.route(eventPayload, ~sourceName, ~sourceId=context.id, ctx)
|
|
295
|
+
let routed = Route.route(eventPayload, ~sourceName, ~sourceId=context.id, ctx)
|
|
296
|
+
routed->Array.forEach(apply)
|
|
297
|
+
reschedule(
|
|
298
|
+
routed,
|
|
299
|
+
~setBy=context.meta.msgId,
|
|
300
|
+
~recordedAt=ctx.recordedAt->Option.getOr(Date.now()),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
// A tick's envelope is addressed by its key.
|
|
304
|
+
if sourceName == Reventless.Clock.name {
|
|
305
|
+
schedules->AutomationSlice_Schedules.fired(~key=context.id)
|
|
306
|
+
}
|
|
210
307
|
})
|
|
211
308
|
}
|
|
212
309
|
|
|
310
|
+
let rebuildSchedules = (events: array<JSON.t>) => {
|
|
311
|
+
schedules->Dict.keysToArray->Array.forEach(key => schedules->Dict.delete(key))
|
|
312
|
+
events->Array.forEach(json => {
|
|
313
|
+
let context = json->Message.decode(Reventless.Message.contextSchema)
|
|
314
|
+
let eventPayload = switch json->JSON.Decode.object {
|
|
315
|
+
| Some(dict) => dict->Dict.get("event")->Option.getOr(json)
|
|
316
|
+
| None => json
|
|
317
|
+
}
|
|
318
|
+
let recordedAt = recordedAtOf(context.meta.time)->Option.getOr(Date.now())
|
|
319
|
+
Route.route(
|
|
320
|
+
eventPayload,
|
|
321
|
+
~sourceName=context.meta.service,
|
|
322
|
+
~sourceId=context.id,
|
|
323
|
+
{environment: "", platformName: "", pluginName: "", sliceName: Spec.name, recordedAt},
|
|
324
|
+
)->reschedule(~setBy=context.meta.msgId, ~recordedAt)
|
|
325
|
+
})
|
|
326
|
+
scheduleChanges := []
|
|
327
|
+
}
|
|
328
|
+
|
|
213
329
|
let phase2 = async (publishJsons: ReventlessInfra.CommandTopic.publishJsons) => {
|
|
330
|
+
let currentTime = await Clock.currentTimeMillis->Effect.runPromise
|
|
214
331
|
// A `Failed` row at or past the ceiling is one an older build left stranded,
|
|
215
332
|
// or one whose Spec lowered `maxRetries` under it. Either way it is abandoned
|
|
216
333
|
// in fact, so say so rather than letting it sit in a status that promises a
|
|
217
334
|
// retry no pass will make.
|
|
218
335
|
let newlyAbandoned: array<string> = []
|
|
336
|
+
// A command published but never answered leaves its row `Processing`. After
|
|
337
|
+
// the timeout that counts as a failed attempt, so this pass sends it again
|
|
338
|
+
// while the budget lasts.
|
|
339
|
+
todoItems
|
|
340
|
+
->Dict.toArray
|
|
341
|
+
->Array.forEach(((id, row)) =>
|
|
342
|
+
switch (row.status, row.processedAt->Option.map(t => Date.fromString(t)->Date.getTime)) {
|
|
343
|
+
| (Processing, Some(processedAt)) if currentTime -. processedAt >= processingTimeoutMs =>
|
|
344
|
+
let updated = row->afterFailedAttempt
|
|
345
|
+
todoItems->Dict.set(id, updated)
|
|
346
|
+
if updated.status == Abandoned {
|
|
347
|
+
newlyAbandoned->Array.push(id)
|
|
348
|
+
}
|
|
349
|
+
| _ => ()
|
|
350
|
+
}
|
|
351
|
+
)
|
|
219
352
|
todoItems
|
|
220
353
|
->Dict.toArray
|
|
221
354
|
->Array.forEach(((id, row)) =>
|
|
@@ -247,7 +380,7 @@ module Make = (
|
|
|
247
380
|
}
|
|
248
381
|
|
|
249
382
|
item->Option.forEach(item => {
|
|
250
|
-
switch Automation.process(id, item) {
|
|
383
|
+
switch Automation.process(id, item, ~now=currentTime) {
|
|
251
384
|
| Some((targetId, command)) =>
|
|
252
385
|
todoItems->Dict.set(id, {...row, status: Processing, processedAt: now()})
|
|
253
386
|
let commandJson = try command
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
5
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
6
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
7
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
8
|
+
import * as Clock from "effect/Clock";
|
|
7
9
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
10
|
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
|
|
9
12
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
13
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
11
14
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
@@ -13,6 +16,7 @@ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/U
|
|
|
13
16
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
14
17
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
15
18
|
import * as AutomationSlice_Route$ReventlessCore from "./AutomationSlice_Route.res.mjs";
|
|
19
|
+
import * as AutomationSlice_Schedules$ReventlessCore from "./AutomationSlice_Schedules.res.mjs";
|
|
16
20
|
|
|
17
21
|
let todoStatusSchema = Sury.union([
|
|
18
22
|
Sury.literal("Pending"),
|
|
@@ -44,10 +48,43 @@ function todoRowSchemaFor(itemSchema) {
|
|
|
44
48
|
}));
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
function tickEvent(key, at) {
|
|
52
|
+
let newrecord = {...Message$ReventlessCore.generateMeta(Clock$Reventless.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined)};
|
|
53
|
+
return Object.fromEntries([
|
|
54
|
+
[
|
|
55
|
+
"id",
|
|
56
|
+
key
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"meta",
|
|
60
|
+
Util_Sury$Reventless.toJson((newrecord.time = new Date(at).toISOString(), newrecord), Message$Reventless.metaSchema)
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"event",
|
|
64
|
+
Util_Sury$Reventless.toJson({
|
|
65
|
+
TAG: "Tick",
|
|
66
|
+
key: key,
|
|
67
|
+
at: at
|
|
68
|
+
}, Clock$Reventless.eventSchema)
|
|
69
|
+
]
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
function Make(Spec) {
|
|
48
74
|
return Automation => {
|
|
49
75
|
let todoItems = {};
|
|
76
|
+
let schedules = {};
|
|
77
|
+
let scheduleChanges = {
|
|
78
|
+
contents: []
|
|
79
|
+
};
|
|
80
|
+
let takeScheduleChanges = () => {
|
|
81
|
+
let changes = scheduleChanges.contents;
|
|
82
|
+
scheduleChanges.contents = [];
|
|
83
|
+
return changes;
|
|
84
|
+
};
|
|
85
|
+
let readsClock = Automation.mappings.some(M => M.sourceName === Clock$Reventless.name);
|
|
50
86
|
let makeMeta = () => Message$ReventlessCore.generateMeta(Spec.targetName, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
87
|
+
let processingTimeoutMs = 15 * 60 * 1000;
|
|
51
88
|
let afterFailedAttempt = row => {
|
|
52
89
|
let retryCount = row.retryCount + 1 | 0;
|
|
53
90
|
let newrecord = {...row};
|
|
@@ -56,6 +93,14 @@ function Make(Spec) {
|
|
|
56
93
|
return newrecord;
|
|
57
94
|
};
|
|
58
95
|
let Route = AutomationSlice_Route$ReventlessCore.Make(Spec)(Automation);
|
|
96
|
+
let recordedAtOf = time => {
|
|
97
|
+
let millis = new Date(time).getTime();
|
|
98
|
+
if (Number.isNaN(millis)) {
|
|
99
|
+
return;
|
|
100
|
+
} else {
|
|
101
|
+
return millis;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
59
104
|
let apply = param => {
|
|
60
105
|
param.collected.forEach(param => {
|
|
61
106
|
let id = param[0];
|
|
@@ -82,17 +127,86 @@ function Make(Spec) {
|
|
|
82
127
|
todoItems[id] = newrecord;
|
|
83
128
|
}));
|
|
84
129
|
};
|
|
85
|
-
let
|
|
130
|
+
let reschedule = (routed, setBy, recordedAt) => {
|
|
131
|
+
routed.forEach(r => {
|
|
132
|
+
r.rescheduled.forEach(change => {
|
|
133
|
+
if (!readsClock) {
|
|
134
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `a mapping changed a schedule, but no mapping reads the Clock, so no tick would reach the slice; the change is skipped`));
|
|
135
|
+
}
|
|
136
|
+
let platformChange = AutomationSlice_Schedules$ReventlessCore.apply(schedules, change, setBy, recordedAt);
|
|
137
|
+
if (platformChange.TAG === "Ok") {
|
|
138
|
+
scheduleChanges.contents = scheduleChanges.contents.concat([platformChange._0]);
|
|
139
|
+
return;
|
|
140
|
+
} else {
|
|
141
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, platformChange._0 + `; the change is skipped`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
let phase1 = (events, sliceCtx) => {
|
|
86
147
|
events.forEach(json => {
|
|
87
148
|
let context = Message$ReventlessCore.decode(json, Message$Reventless.contextSchema);
|
|
149
|
+
let recordedAt = recordedAtOf(context.meta.time);
|
|
150
|
+
let ctx;
|
|
151
|
+
if (recordedAt !== undefined) {
|
|
152
|
+
let newrecord = {...sliceCtx};
|
|
153
|
+
newrecord.recordedAt = recordedAt;
|
|
154
|
+
ctx = newrecord;
|
|
155
|
+
} else {
|
|
156
|
+
ctx = sliceCtx;
|
|
157
|
+
}
|
|
88
158
|
let sourceName = context.meta.service;
|
|
89
159
|
let dict = Stdlib_JSON.Decode.object(json);
|
|
90
160
|
let eventPayload = dict !== undefined ? Stdlib_Option.getOr(dict["event"], json) : json;
|
|
91
|
-
Route.route(eventPayload, sourceName, context.id, ctx)
|
|
161
|
+
let routed = Route.route(eventPayload, sourceName, context.id, ctx);
|
|
162
|
+
routed.forEach(apply);
|
|
163
|
+
reschedule(routed, context.meta.msgId, Stdlib_Option.getOr(ctx.recordedAt, Date.now()));
|
|
164
|
+
if (sourceName === Clock$Reventless.name) {
|
|
165
|
+
return AutomationSlice_Schedules$ReventlessCore.fired(schedules, context.id);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
let rebuildSchedules = events => {
|
|
170
|
+
Object.keys(schedules).forEach(key => Stdlib_Dict.$$delete(schedules, key));
|
|
171
|
+
events.forEach(json => {
|
|
172
|
+
let context = Message$ReventlessCore.decode(json, Message$Reventless.contextSchema);
|
|
173
|
+
let dict = Stdlib_JSON.Decode.object(json);
|
|
174
|
+
let eventPayload = dict !== undefined ? Stdlib_Option.getOr(dict["event"], json) : json;
|
|
175
|
+
let recordedAt = Stdlib_Option.getOr(recordedAtOf(context.meta.time), Date.now());
|
|
176
|
+
reschedule(Route.route(eventPayload, context.meta.service, context.id, {
|
|
177
|
+
environment: "",
|
|
178
|
+
platformName: "",
|
|
179
|
+
pluginName: "",
|
|
180
|
+
sliceName: Spec.name,
|
|
181
|
+
recordedAt: recordedAt
|
|
182
|
+
}), context.meta.msgId, recordedAt);
|
|
92
183
|
});
|
|
184
|
+
scheduleChanges.contents = [];
|
|
93
185
|
};
|
|
94
186
|
let phase2 = async publishJsons => {
|
|
187
|
+
let currentTime = await Effect.runPromise(Clock.currentTimeMillis);
|
|
95
188
|
let newlyAbandoned = [];
|
|
189
|
+
Object.entries(todoItems).forEach(param => {
|
|
190
|
+
let row = param[1];
|
|
191
|
+
let id = param[0];
|
|
192
|
+
let match = row.status;
|
|
193
|
+
let match$1 = Stdlib_Option.map(row.processedAt, t => new Date(t).getTime());
|
|
194
|
+
if (match !== "Processing") {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (match$1 === undefined) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (currentTime - match$1 < processingTimeoutMs) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let updated = afterFailedAttempt(row);
|
|
204
|
+
todoItems[id] = updated;
|
|
205
|
+
if (updated.status === "Abandoned") {
|
|
206
|
+
newlyAbandoned.push(id);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
96
210
|
Object.entries(todoItems).forEach(param => {
|
|
97
211
|
let row = param[1];
|
|
98
212
|
if (!(row.status === "Failed" && row.retryCount >= Spec.maxRetries)) {
|
|
@@ -128,7 +242,7 @@ function Make(Spec) {
|
|
|
128
242
|
item = undefined;
|
|
129
243
|
}
|
|
130
244
|
Stdlib_Option.forEach(item, item => {
|
|
131
|
-
let match = Automation.process(id, item);
|
|
245
|
+
let match = Automation.process(id, item, currentTime);
|
|
132
246
|
if (match === undefined) {
|
|
133
247
|
return;
|
|
134
248
|
}
|
|
@@ -225,7 +339,10 @@ function Make(Spec) {
|
|
|
225
339
|
Spec: Spec,
|
|
226
340
|
todoItems: todoItems,
|
|
227
341
|
phase1: phase1,
|
|
228
|
-
phase2: phase2
|
|
342
|
+
phase2: phase2,
|
|
343
|
+
schedules: schedules,
|
|
344
|
+
takeScheduleChanges: takeScheduleChanges,
|
|
345
|
+
rebuildSchedules: rebuildSchedules
|
|
229
346
|
};
|
|
230
347
|
};
|
|
231
348
|
}
|
|
@@ -234,6 +351,7 @@ export {
|
|
|
234
351
|
todoStatusSchema,
|
|
235
352
|
todoRowSchema,
|
|
236
353
|
todoRowSchemaFor,
|
|
354
|
+
tickEvent,
|
|
237
355
|
Make,
|
|
238
356
|
}
|
|
239
357
|
/* todoStatusSchema Not a pure module */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Decode one source event and run its mappings' `collect`/`resolve`, with no
|
|
1
|
+
// Decode one source event and run its mappings' `collect`/`resolve`/`reschedule`, with no
|
|
2
2
|
// side effects. Shared by the runtime callback, which applies the result to its
|
|
3
3
|
// to-do list, and by the GWT sweep, which must route exactly as the runtime does.
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
type routed<'item> = {
|
|
7
7
|
collected: array<(string, 'item)>,
|
|
8
8
|
resolved: option<string>,
|
|
9
|
+
rescheduled: array<Reventless.AutomationSlice.scheduleChange>,
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
module Make = (
|
|
@@ -31,6 +32,7 @@ module Make = (
|
|
|
31
32
|
decoder.decode(~eventType, ~data=dataDict)->Option.map(event => {
|
|
32
33
|
collected: M.collect(event, ~sourceId, ctx),
|
|
33
34
|
resolved: M.resolve(event),
|
|
35
|
+
rescheduled: M.reschedule(event),
|
|
34
36
|
})
|
|
35
37
|
}
|
|
36
38
|
{sourceName: M.sourceName, handle}
|
|
@@ -13,7 +13,8 @@ function Make(Spec) {
|
|
|
13
13
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
14
14
|
return Stdlib_Option.map(decoder.decode(match[0], match[1]), event => ({
|
|
15
15
|
collected: M.collect(event, sourceId, ctx),
|
|
16
|
-
resolved: M.resolve(event)
|
|
16
|
+
resolved: M.resolve(event),
|
|
17
|
+
rescheduled: M.reschedule(event)
|
|
17
18
|
}));
|
|
18
19
|
};
|
|
19
20
|
return {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The schedules of one automation slice: a row per key, kept in the slice's schedules
|
|
2
|
+
// store. The rows are the truth and the platform's schedules follow them. Shared by
|
|
3
|
+
// the runtime and the GWT DSL, so a scenario folds `reschedule` as the runtime does.
|
|
4
|
+
|
|
5
|
+
/** The `setBy` of the row a spec's declared `schedule` writes. */
|
|
6
|
+
let declared = "declared"
|
|
7
|
+
|
|
8
|
+
@schema
|
|
9
|
+
type row = {
|
|
10
|
+
key: string,
|
|
11
|
+
/** The time as the JSON text of `Reventless.Schedule.timeSchema`, an `After` set by an event
|
|
12
|
+
already counted from when the event was recorded. Absent when an event cleared
|
|
13
|
+
the slice-wide key, so a deploy does not put the declared time back. */
|
|
14
|
+
time?: string,
|
|
15
|
+
/** The id of the event that set or cleared it, or `declared`. */
|
|
16
|
+
setBy: string,
|
|
17
|
+
setAt: string,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What the platform is told after a row changed. */
|
|
21
|
+
type platformChange =
|
|
22
|
+
| Put({key: string, time: Reventless.Schedule.time})
|
|
23
|
+
| Remove({key: string})
|
|
24
|
+
|
|
25
|
+
let encodeTime = (time: Reventless.Schedule.time) =>
|
|
26
|
+
time->Reventless.Util_Sury.toJson(Reventless.Schedule.timeSchema)->JSON.stringify
|
|
27
|
+
let decodeTime = (text: string): Reventless.Schedule.time =>
|
|
28
|
+
text->JSON.parseOrThrow->Reventless.Util_Sury.fromJson(Reventless.Schedule.timeSchema)
|
|
29
|
+
let timeOf = (row: row) => row.time->Option.map(decodeTime)
|
|
30
|
+
|
|
31
|
+
let iso = millis => millis->Date.fromTime->Date.toISOString
|
|
32
|
+
|
|
33
|
+
// The slice-wide key keeps a row saying it was cleared; any other key's row goes.
|
|
34
|
+
let clearRow = (rows: dict<row>, ~key, ~setBy, ~setAt) =>
|
|
35
|
+
if key == Reventless.Clock.defaultKey {
|
|
36
|
+
rows->Dict.set(key, {key, setBy, setAt})
|
|
37
|
+
} else {
|
|
38
|
+
rows->Dict.delete(key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
Applies one change an event made, recorded at `recordedAt`, and returns what the
|
|
43
|
+
platform is told. A time that cannot fire is refused with every problem it has, and
|
|
44
|
+
changes nothing.
|
|
45
|
+
*/
|
|
46
|
+
let apply = (
|
|
47
|
+
rows: dict<row>,
|
|
48
|
+
change: Reventless.AutomationSlice.scheduleChange,
|
|
49
|
+
~setBy,
|
|
50
|
+
~recordedAt,
|
|
51
|
+
): result<platformChange, string> =>
|
|
52
|
+
switch change {
|
|
53
|
+
| Set({key, time: wanted}) =>
|
|
54
|
+
switch ScheduleTime.errors(wanted) {
|
|
55
|
+
| [] =>
|
|
56
|
+
let resolved = wanted->ScheduleTime.resolveOnce(~from=recordedAt)
|
|
57
|
+
rows->Dict.set(key, {key, time: encodeTime(resolved), setBy, setAt: iso(recordedAt)})
|
|
58
|
+
Ok(Put({key, time: resolved}))
|
|
59
|
+
| errs => Error(`schedule ${key}: ${errs->Array.join("; ")}`)
|
|
60
|
+
}
|
|
61
|
+
| Clear({key: cleared}) =>
|
|
62
|
+
clearRow(rows, ~key=cleared, ~setBy, ~setAt=iso(recordedAt))
|
|
63
|
+
Ok(Remove({key: cleared}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A tick of `key` arrived: a once-only schedule has fired, so its row goes. */
|
|
67
|
+
let fired = (rows: dict<row>, ~key) =>
|
|
68
|
+
rows
|
|
69
|
+
->Dict.get(key)
|
|
70
|
+
->Option.forEach(row =>
|
|
71
|
+
switch row->timeOf {
|
|
72
|
+
| Some(Once(_)) => clearRow(rows, ~key, ~setBy=row.setBy, ~setAt=row.setAt)
|
|
73
|
+
| _ => ()
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
Brings the spec's declared time into the rows under `Reventless.Clock.defaultKey`: written when
|
|
79
|
+
no event has set or cleared that key, replaced when the declaration changed, and
|
|
80
|
+
removed when the spec no longer declares one.
|
|
81
|
+
*/
|
|
82
|
+
let declare = (rows: dict<row>, declaredTime: option<Reventless.Schedule.time>, ~now) => {
|
|
83
|
+
let key = Reventless.Clock.defaultKey
|
|
84
|
+
let write = time =>
|
|
85
|
+
rows->Dict.set(key, {key, time: encodeTime(time), setBy: declared, setAt: iso(now)})
|
|
86
|
+
switch (declaredTime, rows->Dict.get(key)) {
|
|
87
|
+
| (Some(time), None) => write(time)
|
|
88
|
+
| (Some(time), Some(row)) if row.setBy == declared && row.time != Some(encodeTime(time)) =>
|
|
89
|
+
write(time)
|
|
90
|
+
| (None, Some(row)) if row.setBy == declared => rows->Dict.delete(key)
|
|
91
|
+
| _ => ()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A once-only time already past fires no more.
|
|
96
|
+
let isPast = (time: Reventless.Schedule.time, ~now) =>
|
|
97
|
+
switch time {
|
|
98
|
+
| Once(_) => time->ScheduleTime.nextFiring(~after=now)->Option.isNone
|
|
99
|
+
| _ => false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
Brings the platform in line with the rows, after the declared time is brought in: a
|
|
104
|
+
schedule missing from the platform is put, the declared one is put again in case its
|
|
105
|
+
time changed, and one the rows do not hold is removed. A once-only time already past
|
|
106
|
+
is not put again, and its row goes. A row an event changed reached the platform when
|
|
107
|
+
it changed, so it is not put again.
|
|
108
|
+
*/
|
|
109
|
+
let reconcile = async (
|
|
110
|
+
rows: dict<row>,
|
|
111
|
+
platform: ReventlessInfra.AutomationSlice.platformSchedules,
|
|
112
|
+
~declaredTime,
|
|
113
|
+
~now,
|
|
114
|
+
) => {
|
|
115
|
+
declare(rows, declaredTime, ~now)
|
|
116
|
+
let live =
|
|
117
|
+
rows
|
|
118
|
+
->Dict.valuesToArray
|
|
119
|
+
->Array.filterMap(row =>
|
|
120
|
+
switch row->timeOf {
|
|
121
|
+
| Some(time) if isPast(time, ~now) =>
|
|
122
|
+
clearRow(rows, ~key=row.key, ~setBy=row.setBy, ~setAt=row.setAt)
|
|
123
|
+
None
|
|
124
|
+
| Some(time) => Some((row, time))
|
|
125
|
+
| None => None
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
let held = await platform.names()
|
|
129
|
+
for i in 0 to live->Array.length - 1 {
|
|
130
|
+
let (row, time) = live->Array.getUnsafe(i)
|
|
131
|
+
if row.setBy == declared || !(held->Array.includes(platform.nameOf(row.key))) {
|
|
132
|
+
await platform.put(~key=row.key, time)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
let wanted = live->Array.map(((row, _)) => platform.nameOf(row.key))
|
|
136
|
+
let extra = held->Array.filter(name => !(wanted->Array.includes(name)))
|
|
137
|
+
for i in 0 to extra->Array.length - 1 {
|
|
138
|
+
await platform.removeName(extra->Array.getUnsafe(i))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Tells the platform what the rows' changes were, in order. */
|
|
143
|
+
let tell = async (platform: ReventlessInfra.AutomationSlice.platformSchedules, changes) =>
|
|
144
|
+
for i in 0 to changes->Array.length - 1 {
|
|
145
|
+
switch changes->Array.getUnsafe(i) {
|
|
146
|
+
| Put({key, time}) => await platform.put(~key, time)
|
|
147
|
+
| Remove({key}) => await platform.removeName(platform.nameOf(key))
|
|
148
|
+
}
|
|
149
|
+
}
|