@systemfsoftware/effect-daemon-spec 0.1.0
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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/effect-daemon-spec.d.ts +410 -0
- package/dist/index.d.ts +338 -0
- package/dist/index.mjs +737 -0
- package/package.json +76 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { Array as Array$1, Cause, Clock, Context, Duration, Effect, Either, Exit, Fiber, HashMap, Layer, Match, Metric, Option, Predicate, Ref, Schedule, Schema, Scope, Stream } from "effect";
|
|
2
|
+
import { dual } from "effect/Function";
|
|
3
|
+
import "effect/Schema";
|
|
4
|
+
//#region src/backoff.ts
|
|
5
|
+
const cappedBackoff = (base, cap) => {
|
|
6
|
+
const ceiling = Duration.decode(cap);
|
|
7
|
+
return Schedule.exponential(base).pipe(Schedule.jittered, Schedule.modifyDelay((_, delay) => Duration.min(delay, ceiling)));
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/daemon-health.schema.ts
|
|
11
|
+
var DynamicLimitExceeded = class extends Schema.TaggedError()("DynamicLimitExceeded", { limit: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)) }) {};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/daemon-metrics.ts
|
|
14
|
+
const supervisorRestartsCounter = Metric.counter("app.daemon.supervisor.restart", { description: "Daemon supervisor restart count" });
|
|
15
|
+
const supervisorExhaustionsCounter = Metric.counter("app.daemon.supervisor.exhaustion", { description: "Daemon supervisor exhaustion count" });
|
|
16
|
+
const supervisorChildrenGauge = Metric.gauge("app.daemon.supervisor.children", { description: "Daemon supervisor current child count (dynamic supervisors only)" });
|
|
17
|
+
const healthStateGauge = Metric.gauge("app.daemon.health.state", { description: "Daemon health latch open (1) or closed (0); tagged by daemon name and latch (ready | healthy | paused)" });
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/daemon-policy.schema.ts
|
|
20
|
+
const IntensityConfig = Schema.Struct({
|
|
21
|
+
restarts: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)),
|
|
22
|
+
window: Schema.DurationFromSelf
|
|
23
|
+
});
|
|
24
|
+
const IntensityTypeId = Symbol.for("@systemfsoftware/effect-daemon-spec/Intensity");
|
|
25
|
+
var BoundedIntensity = class extends Schema.TaggedClass()("Bounded", IntensityConfig.fields) {
|
|
26
|
+
[IntensityTypeId] = IntensityTypeId;
|
|
27
|
+
};
|
|
28
|
+
var UnboundedIntensity = class extends Schema.TaggedClass()("Unbounded", {}) {
|
|
29
|
+
[IntensityTypeId] = IntensityTypeId;
|
|
30
|
+
};
|
|
31
|
+
const Intensity = Schema.Union(BoundedIntensity, UnboundedIntensity);
|
|
32
|
+
var ChildPolicyConfig = class extends Schema.Class("ChildPolicyConfig")({
|
|
33
|
+
restart: Schema.optional(Schema.Literal("permanent", "transient", "temporary")),
|
|
34
|
+
intensity: Schema.optional(IntensityConfig)
|
|
35
|
+
}) {};
|
|
36
|
+
Schema.Class("SupervisorPolicyConfig")({
|
|
37
|
+
intensity: Schema.optional(IntensityConfig),
|
|
38
|
+
cooldown: Schema.optional(Schema.DurationFromSelf)
|
|
39
|
+
});
|
|
40
|
+
var LockPolicyConfig = class extends Schema.Class("LockPolicyConfig")({
|
|
41
|
+
mode: Schema.optional(Schema.Literal("none", "required", "optional")),
|
|
42
|
+
key: Schema.optional(Schema.String)
|
|
43
|
+
}) {};
|
|
44
|
+
var TickPolicyConfig = class extends Schema.Class("TickPolicyConfig")({
|
|
45
|
+
spanName: Schema.optional(Schema.String),
|
|
46
|
+
tickTimeout: Schema.DurationFromSelf,
|
|
47
|
+
startLogLevel: Schema.optional(Schema.Literal("debug", "info"))
|
|
48
|
+
}) {};
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/daemon-reporter.ts
|
|
51
|
+
var DaemonReporter = class DaemonReporter extends Context.Tag("@systemfsoftware/effect-daemon-spec/daemon-reporter/DaemonReporter")() {
|
|
52
|
+
static Noop = Layer.succeed(DaemonReporter, DaemonReporter.of({
|
|
53
|
+
onRestart: () => Effect.void,
|
|
54
|
+
onExhausted: () => Effect.void
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/daemon-spec.ts
|
|
59
|
+
const WorkerTypeId = Symbol.for("@systemfsoftware/effect-daemon/Worker");
|
|
60
|
+
const SupervisorTypeId = Symbol.for("@systemfsoftware/effect-daemon/Supervisor");
|
|
61
|
+
const DynamicSpecTypeId = Symbol.for("@systemfsoftware/effect-daemon/DynamicSpec");
|
|
62
|
+
const isWorker = (x) => WorkerTypeId in x;
|
|
63
|
+
const isSupervisor = (x) => SupervisorTypeId in x;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/daemon.ts
|
|
66
|
+
const make$1 = (common, loop) => ({
|
|
67
|
+
[WorkerTypeId]: WorkerTypeId,
|
|
68
|
+
name: common.name,
|
|
69
|
+
loop,
|
|
70
|
+
child: common.child ?? {},
|
|
71
|
+
tick: common.tick,
|
|
72
|
+
tickHooks: common.tickHooks ?? {},
|
|
73
|
+
lock: common.lock
|
|
74
|
+
});
|
|
75
|
+
const poll = (opts) => {
|
|
76
|
+
if (typeof opts.prereq === "undefined") return make$1(opts, {
|
|
77
|
+
_tag: "Poll",
|
|
78
|
+
gate: Effect.succeed(Option.some(Effect.asVoid(opts.work))),
|
|
79
|
+
interval: opts.interval
|
|
80
|
+
});
|
|
81
|
+
const { prereq, work } = opts;
|
|
82
|
+
return make$1(opts, {
|
|
83
|
+
_tag: "Poll",
|
|
84
|
+
gate: Effect.map(prereq, Option.map((data) => work(data))),
|
|
85
|
+
interval: opts.interval
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const stream = (opts) => make$1(opts, {
|
|
89
|
+
_tag: "Stream",
|
|
90
|
+
stream: opts.stream
|
|
91
|
+
});
|
|
92
|
+
const subscription = (opts) => make$1(opts, {
|
|
93
|
+
_tag: "Subscription",
|
|
94
|
+
acquire: Effect.asVoid(opts.acquire)
|
|
95
|
+
});
|
|
96
|
+
const Daemon = {
|
|
97
|
+
poll,
|
|
98
|
+
stream,
|
|
99
|
+
subscription
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/leader-lock.schema.ts
|
|
103
|
+
var LeaderLockNotAcquired = class extends Schema.TaggedError()("LeaderLockNotAcquired", { key: Schema.String }) {};
|
|
104
|
+
var LeaderLockInfraError = class extends Schema.TaggedError()("LeaderLockInfraError", {
|
|
105
|
+
key: Schema.String,
|
|
106
|
+
cause: Schema.Unknown
|
|
107
|
+
}) {};
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/leader-lock.ts
|
|
110
|
+
var LeaderLock = class LeaderLock extends Context.Tag("@systemfsoftware/effect-daemon-spec/leader-lock/LeaderLock")() {
|
|
111
|
+
static Noop = Layer.succeed(LeaderLock, LeaderLock.of({ withLock: (_key, self) => Effect.map(self, Option.some) }));
|
|
112
|
+
};
|
|
113
|
+
const withLeaderLock = dual(2, (self, options) => Effect.gen(function* () {
|
|
114
|
+
const out = yield* (yield* LeaderLock).withLock(options.key, self);
|
|
115
|
+
if (Option.isSome(out)) return out.value;
|
|
116
|
+
return yield* Match.value(options.mode).pipe(Match.when("required", () => Effect.fail(new LeaderLockNotAcquired({ key: options.key }))), Match.when("optional", () => Effect.void), Match.exhaustive);
|
|
117
|
+
}));
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/lock-primitive.schema.ts
|
|
120
|
+
var LockPrimitiveError = class extends Schema.TaggedError()("LockPrimitiveError", {
|
|
121
|
+
key: Schema.String,
|
|
122
|
+
cause: Schema.Unknown
|
|
123
|
+
}) {};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/lock-primitive.ts
|
|
126
|
+
var LockPrimitive = class extends Context.Tag("@systemfsoftware/effect-daemon-spec/lock-primitive/LockPrimitive")() {};
|
|
127
|
+
const LeaderLockFromPrimitive = Layer.effect(LeaderLock, Effect.gen(function* () {
|
|
128
|
+
const primitive = yield* LockPrimitive;
|
|
129
|
+
return LeaderLock.of({ withLock: (key, self) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
130
|
+
const scope = yield* Scope.make();
|
|
131
|
+
if (!(yield* restore(primitive.tryAcquire(key)).pipe(Scope.extend(scope), Effect.mapError((cause) => new LeaderLockInfraError({
|
|
132
|
+
key,
|
|
133
|
+
cause
|
|
134
|
+
})), Effect.onError(() => Scope.close(scope, Exit.void))))) {
|
|
135
|
+
yield* Scope.close(scope, Exit.void);
|
|
136
|
+
return Option.none();
|
|
137
|
+
}
|
|
138
|
+
const result = yield* restore(self).pipe(Effect.ensuring(Scope.close(scope, Exit.void)));
|
|
139
|
+
return Option.some(result);
|
|
140
|
+
})) });
|
|
141
|
+
}));
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/internal/intensity-window.ts
|
|
144
|
+
const isWithinWindow = (now, windowMillis) => (t) => now - t <= windowMillis;
|
|
145
|
+
const pruneTimestamps = (ts, now, windowMillis) => ts.filter(isWithinWindow(now, windowMillis));
|
|
146
|
+
const recordTimestamp = (ts, now, windowMillis) => [now, ...pruneTimestamps(ts, now, windowMillis)];
|
|
147
|
+
const exceedsRestarts = (count, restarts) => count > restarts;
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/internal/intensity.ts
|
|
150
|
+
const neverExceeds = {
|
|
151
|
+
record: Effect.void,
|
|
152
|
+
isExceeded: Effect.succeed(false),
|
|
153
|
+
count: Effect.succeed(0)
|
|
154
|
+
};
|
|
155
|
+
const boundedTracker = (restarts, window) => Effect.gen(function* () {
|
|
156
|
+
const windowMillis = Duration.toMillis(window);
|
|
157
|
+
const timestamps = yield* Ref.make([]);
|
|
158
|
+
const prune = (now) => Ref.modify(timestamps, (ts) => {
|
|
159
|
+
const active = pruneTimestamps(ts, now, windowMillis);
|
|
160
|
+
return [active, active];
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
record: Effect.gen(function* () {
|
|
164
|
+
const now = yield* Clock.currentTimeMillis;
|
|
165
|
+
yield* Ref.update(timestamps, (ts) => recordTimestamp(ts, now, windowMillis));
|
|
166
|
+
}),
|
|
167
|
+
isExceeded: Effect.gen(function* () {
|
|
168
|
+
return exceedsRestarts((yield* prune(yield* Clock.currentTimeMillis)).length, restarts);
|
|
169
|
+
}),
|
|
170
|
+
count: Effect.gen(function* () {
|
|
171
|
+
return (yield* prune(yield* Clock.currentTimeMillis)).length;
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
const make = (intensity) => Match.value(intensity).pipe(Match.tag("Unbounded", () => Effect.succeed(neverExceeds)), Match.tag("Bounded", ({ restarts, window }) => boundedTracker(restarts, window)), Match.exhaustive);
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/internal/restart-decision.schema.ts
|
|
178
|
+
const RestartDecisionTypeId = Symbol.for("@systemfsoftware/effect-daemon/RestartDecision");
|
|
179
|
+
var Continue = class extends Schema.TaggedClass()("Continue", {}) {
|
|
180
|
+
[RestartDecisionTypeId] = RestartDecisionTypeId;
|
|
181
|
+
};
|
|
182
|
+
var Restart = class extends Schema.TaggedClass()("Restart", { indices: Schema.NonEmptyArray(Schema.Int) }) {
|
|
183
|
+
[RestartDecisionTypeId] = RestartDecisionTypeId;
|
|
184
|
+
};
|
|
185
|
+
var Exhausted = class extends Schema.TaggedClass()("Exhausted", {}) {
|
|
186
|
+
[RestartDecisionTypeId] = RestartDecisionTypeId;
|
|
187
|
+
};
|
|
188
|
+
Schema.Union(Continue, Restart, Exhausted);
|
|
189
|
+
const RestartStrategy = Schema.Literal("one_for_one", "one_for_all", "rest_for_one");
|
|
190
|
+
Schema.Struct({
|
|
191
|
+
strategy: RestartStrategy,
|
|
192
|
+
totalChildren: Schema.Int.pipe(Schema.between(1, 10)),
|
|
193
|
+
failedIndex: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)),
|
|
194
|
+
exitSuccess: Schema.Boolean,
|
|
195
|
+
intensityExceeded: Schema.Boolean
|
|
196
|
+
}).pipe(Schema.filter((s) => s.failedIndex < s.totalChildren, { message: () => "failedIndex must be < totalChildren" }), Schema.annotations({ arbitrary: () => (fc) => fc.integer({
|
|
197
|
+
min: 1,
|
|
198
|
+
max: 10
|
|
199
|
+
}).chain((totalChildren) => fc.integer({
|
|
200
|
+
min: 0,
|
|
201
|
+
max: totalChildren - 1
|
|
202
|
+
}).chain((failedIndex) => fc.record({
|
|
203
|
+
strategy: fc.constantFrom("one_for_one", "one_for_all", "rest_for_one"),
|
|
204
|
+
totalChildren: fc.constant(totalChildren),
|
|
205
|
+
failedIndex: fc.constant(failedIndex),
|
|
206
|
+
exitSuccess: fc.boolean(),
|
|
207
|
+
intensityExceeded: fc.boolean()
|
|
208
|
+
}))) }));
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/internal/restart-decision.strategy.ts
|
|
211
|
+
const restartIndicesFor = (strategy, failedIndex, total) => {
|
|
212
|
+
if (strategy === "one_for_one") return [failedIndex];
|
|
213
|
+
if (strategy === "one_for_all") return [0, ...Array.from({ length: Math.max(0, total - 1) }, (_, i) => i + 1)];
|
|
214
|
+
return [failedIndex, ...Array.from({ length: Math.max(0, total - failedIndex - 1) }, (_, i) => failedIndex + 1 + i)];
|
|
215
|
+
};
|
|
216
|
+
const decideRestart = (input) => {
|
|
217
|
+
if (input.exitSuccess) return new Continue();
|
|
218
|
+
if (input.intensityExceeded) return new Exhausted();
|
|
219
|
+
return new Restart({ indices: restartIndicesFor(input.strategy, input.failedIndex, input.totalChildren) });
|
|
220
|
+
};
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/internal/supervise-index.ts
|
|
223
|
+
const failedIndexOf = (startIdx, failedOffset) => startIdx + failedOffset;
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/internal/supervision-context.strategy.ts
|
|
226
|
+
const openAllReady = (ctx) => Effect.gen(function* () {
|
|
227
|
+
yield* Effect.yieldNow();
|
|
228
|
+
yield* Effect.forEach(ctx.booted, (b) => b.health.ready.await, { concurrency: "unbounded" });
|
|
229
|
+
yield* Effect.zipRight(ctx.health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "ready"), 1));
|
|
230
|
+
});
|
|
231
|
+
const raceForExit = (fibers) => Effect.raceAll(fibers.map((f, idx) => f.await.pipe(Effect.map((exit) => [idx, exit]))));
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/internal/supervise-tree.strategy.ts
|
|
234
|
+
const EpochStepTypeId = Symbol.for("@systemfsoftware/effect-daemon/EpochStep");
|
|
235
|
+
var StopEpoch = class extends Schema.TaggedClass()("StopEpoch", {}) {
|
|
236
|
+
[EpochStepTypeId] = EpochStepTypeId;
|
|
237
|
+
};
|
|
238
|
+
var RestartEpoch = class extends Schema.TaggedClass()("RestartEpoch", {}) {
|
|
239
|
+
[EpochStepTypeId] = EpochStepTypeId;
|
|
240
|
+
};
|
|
241
|
+
var CooldownEpoch = class extends Schema.TaggedClass()("CooldownEpoch", {}) {
|
|
242
|
+
[EpochStepTypeId] = EpochStepTypeId;
|
|
243
|
+
};
|
|
244
|
+
Schema.Union(StopEpoch, RestartEpoch, CooldownEpoch);
|
|
245
|
+
const SupervisionEpochResultTypeId = Symbol.for("@systemfsoftware/effect-daemon/SupervisionEpochResult");
|
|
246
|
+
var StopSupervision = class extends Schema.TaggedClass()("StopSupervision", {}) {
|
|
247
|
+
[SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
|
|
248
|
+
};
|
|
249
|
+
var ContinueSupervision = class extends Schema.TaggedClass()("ContinueSupervision", {}) {
|
|
250
|
+
[SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
|
|
251
|
+
};
|
|
252
|
+
Schema.Union(StopSupervision, ContinueSupervision);
|
|
253
|
+
const handleExhausted = (ctx, cause) => Effect.gen(function* () {
|
|
254
|
+
yield* Effect.zipRight(ctx.health.healthy.close, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "healthy"), 0));
|
|
255
|
+
yield* ctx.reportExhausted(cause);
|
|
256
|
+
return new CooldownEpoch();
|
|
257
|
+
});
|
|
258
|
+
const handleRestart = (ctx, cause, onSignal) => Effect.gen(function* () {
|
|
259
|
+
yield* ctx.reportRestart(cause);
|
|
260
|
+
yield* onSignal;
|
|
261
|
+
return new RestartEpoch();
|
|
262
|
+
});
|
|
263
|
+
const reopenHealthyAfterCooldown = (ctx) => Effect.zipRight(ctx.health.healthy.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "healthy"), 1));
|
|
264
|
+
const runSupervisionEpochWithBackoff = (attempt, ctx) => Effect.gen(function* () {
|
|
265
|
+
const driver = yield* Schedule.driver(ctx.policy.backoff);
|
|
266
|
+
const loop = () => Effect.gen(function* () {
|
|
267
|
+
const step = yield* attempt.pipe(Effect.scoped);
|
|
268
|
+
return yield* Match.value(step).pipe(Match.tag("StopEpoch", () => Effect.succeed(new StopSupervision())), Match.tag("CooldownEpoch", () => Effect.gen(function* () {
|
|
269
|
+
yield* Effect.sleep(Duration.decode(ctx.policy.cooldown));
|
|
270
|
+
yield* reopenHealthyAfterCooldown(ctx);
|
|
271
|
+
return new ContinueSupervision();
|
|
272
|
+
})), Match.tag("RestartEpoch", () => Effect.gen(function* () {
|
|
273
|
+
const stepped = yield* Effect.either(driver.next(void 0));
|
|
274
|
+
if (Either.isLeft(stepped)) return new StopSupervision();
|
|
275
|
+
return yield* loop();
|
|
276
|
+
})), Match.exhaustive);
|
|
277
|
+
});
|
|
278
|
+
return yield* loop();
|
|
279
|
+
});
|
|
280
|
+
const superviseChild = (ctx, child, idx) => Effect.gen(function* () {
|
|
281
|
+
const childIntensityOpt = yield* Option.match(Option.fromNullable(child.childPolicy.intensity), {
|
|
282
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
283
|
+
onSome: (cfg) => Effect.map(make(new BoundedIntensity(cfg)), Option.some)
|
|
284
|
+
});
|
|
285
|
+
const loop = () => Effect.gen(function* () {
|
|
286
|
+
const supIntensity = yield* make(ctx.policy.intensity);
|
|
287
|
+
const epochResult = yield* runSupervisionEpochWithBackoff(Effect.gen(function* () {
|
|
288
|
+
yield* ctx.health.paused.await;
|
|
289
|
+
const fiber = yield* Effect.forkScoped(child.run);
|
|
290
|
+
const exit = yield* Fiber.await(fiber);
|
|
291
|
+
if (!Exit.isSuccess(exit)) {
|
|
292
|
+
if (child.childPolicy.restart === "temporary") return new StopEpoch();
|
|
293
|
+
if (yield* Option.match(childIntensityOpt, {
|
|
294
|
+
onNone: () => Effect.succeed(false),
|
|
295
|
+
onSome: (ci) => Effect.gen(function* () {
|
|
296
|
+
yield* ci.record;
|
|
297
|
+
return yield* ci.isExceeded;
|
|
298
|
+
})
|
|
299
|
+
})) return new StopEpoch();
|
|
300
|
+
yield* supIntensity.record;
|
|
301
|
+
const decision = decideRestart({
|
|
302
|
+
strategy: "one_for_one",
|
|
303
|
+
exitSuccess: false,
|
|
304
|
+
intensityExceeded: yield* supIntensity.isExceeded,
|
|
305
|
+
failedIndex: idx,
|
|
306
|
+
totalChildren: ctx.booted.length
|
|
307
|
+
});
|
|
308
|
+
return yield* Match.value(decision).pipe(Match.tag("Continue", () => Effect.succeed(new StopEpoch())), Match.tag("Exhausted", () => handleExhausted(ctx, exit.cause)), Match.tag("Restart", () => handleRestart(ctx, exit.cause, Effect.void)), Match.exhaustive);
|
|
309
|
+
}
|
|
310
|
+
return new StopEpoch();
|
|
311
|
+
}), ctx);
|
|
312
|
+
return yield* Match.value(epochResult).pipe(Match.tag("ContinueSupervision", () => loop()), Match.tag("StopSupervision", () => Effect.void), Match.exhaustive);
|
|
313
|
+
});
|
|
314
|
+
return yield* loop();
|
|
315
|
+
});
|
|
316
|
+
const runIndependent = (ctx) => Effect.gen(function* () {
|
|
317
|
+
const fibers = yield* Effect.forEach(ctx.booted, (child, childIdx) => Effect.forkScoped(superviseChild(ctx, child, childIdx)));
|
|
318
|
+
yield* Effect.yieldNow();
|
|
319
|
+
yield* openAllReady(ctx);
|
|
320
|
+
yield* Effect.forEach(fibers, (f) => Fiber.await(f), { concurrency: "unbounded" });
|
|
321
|
+
});
|
|
322
|
+
const runGroup = (strategy, ctx) => Effect.gen(function* () {
|
|
323
|
+
const loop = () => Effect.gen(function* () {
|
|
324
|
+
const intensity = yield* make(ctx.policy.intensity);
|
|
325
|
+
const childIntensityTrackers = yield* Effect.forEach(ctx.booted, (b) => Option.match(Option.fromNullable(b.childPolicy.intensity), {
|
|
326
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
327
|
+
onSome: (cfg) => Effect.map(make(new BoundedIntensity(cfg)), Option.some)
|
|
328
|
+
}));
|
|
329
|
+
const cursor = yield* Ref.make(0);
|
|
330
|
+
const epochResult = yield* runSupervisionEpochWithBackoff(Effect.gen(function* () {
|
|
331
|
+
yield* ctx.health.paused.await;
|
|
332
|
+
const startIdx = yield* Ref.get(cursor);
|
|
333
|
+
const slice = ctx.booted.slice(startIdx);
|
|
334
|
+
const fibers = yield* Effect.forEach(slice, (c) => Effect.forkScoped(c.run));
|
|
335
|
+
yield* Effect.yieldNow();
|
|
336
|
+
yield* Effect.forkScoped(openAllReady(ctx));
|
|
337
|
+
const [failedOffset, firstExit] = yield* raceForExit(fibers);
|
|
338
|
+
if (!Exit.isSuccess(firstExit)) {
|
|
339
|
+
const failedIdx = failedIndexOf(startIdx, failedOffset);
|
|
340
|
+
const failedBootedOpt = Option.fromNullable(ctx.booted[failedIdx]);
|
|
341
|
+
if (Option.isNone(failedBootedOpt)) return new StopEpoch();
|
|
342
|
+
if (failedBootedOpt.value.childPolicy.restart === "temporary") return new StopEpoch();
|
|
343
|
+
const cIntForFailed = Option.flatten(Array$1.get(childIntensityTrackers, failedIdx));
|
|
344
|
+
if (yield* Option.match(cIntForFailed, {
|
|
345
|
+
onNone: () => Effect.succeed(false),
|
|
346
|
+
onSome: (cInt) => Effect.gen(function* () {
|
|
347
|
+
yield* cInt.record;
|
|
348
|
+
return yield* cInt.isExceeded;
|
|
349
|
+
})
|
|
350
|
+
})) return new StopEpoch();
|
|
351
|
+
yield* intensity.record;
|
|
352
|
+
const decision = decideRestart({
|
|
353
|
+
strategy,
|
|
354
|
+
exitSuccess: false,
|
|
355
|
+
intensityExceeded: yield* intensity.isExceeded,
|
|
356
|
+
failedIndex: failedIdx,
|
|
357
|
+
totalChildren: ctx.booted.length
|
|
358
|
+
});
|
|
359
|
+
return yield* Match.value(decision).pipe(Match.tag("Continue", () => Effect.succeed(new StopEpoch())), Match.tag("Exhausted", () => handleExhausted(ctx, firstExit.cause)), Match.tag("Restart", (restartDecision) => handleRestart(ctx, firstExit.cause, Ref.set(cursor, restartDecision.indices[0]))), Match.exhaustive);
|
|
360
|
+
}
|
|
361
|
+
return new StopEpoch();
|
|
362
|
+
}), ctx);
|
|
363
|
+
return yield* Match.value(epochResult).pipe(Match.tag("ContinueSupervision", () => loop()), Match.tag("StopSupervision", () => Effect.void), Match.exhaustive);
|
|
364
|
+
});
|
|
365
|
+
return yield* loop();
|
|
366
|
+
});
|
|
367
|
+
const superviseTree = (strategy, ctx) => Match.value(strategy).pipe(Match.when("one_for_one", () => runIndependent(ctx)), Match.when("one_for_all", () => runGroup("one_for_all", ctx)), Match.when("rest_for_one", () => runGroup("rest_for_one", ctx)), Match.exhaustive);
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/internal/supervisor-runtime.ts
|
|
370
|
+
const buildSupervisorBody = (sup, health, booted) => Effect.gen(function* () {
|
|
371
|
+
const policy = yield* sup.supervision;
|
|
372
|
+
const intensityEff = make(policy.intensity);
|
|
373
|
+
const reportRestart = (cause) => Effect.gen(function* () {
|
|
374
|
+
const reporter = yield* DaemonReporter;
|
|
375
|
+
yield* Metric.increment(Metric.tagged(supervisorRestartsCounter, "supervisor", sup.name));
|
|
376
|
+
yield* reporter.onRestart(sup.name, cause);
|
|
377
|
+
yield* Option.match(Option.fromNullable(sup.reporter.onRestart), {
|
|
378
|
+
onNone: () => Effect.void,
|
|
379
|
+
onSome: (fn) => fn(cause)
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
const reportExhausted = (cause) => Effect.gen(function* () {
|
|
383
|
+
const reporter = yield* DaemonReporter;
|
|
384
|
+
yield* Metric.increment(Metric.tagged(supervisorExhaustionsCounter, "supervisor", sup.name));
|
|
385
|
+
yield* reporter.onExhausted(sup.name, cause);
|
|
386
|
+
yield* Option.match(Option.fromNullable(sup.reporter.onExhausted), {
|
|
387
|
+
onNone: () => Effect.void,
|
|
388
|
+
onSome: (fn) => fn(cause)
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
const runStrategy = superviseTree(sup.strategy, {
|
|
392
|
+
name: sup.name,
|
|
393
|
+
booted,
|
|
394
|
+
health,
|
|
395
|
+
policy,
|
|
396
|
+
reportRestart,
|
|
397
|
+
reportExhausted,
|
|
398
|
+
intensityEff
|
|
399
|
+
});
|
|
400
|
+
yield* Effect.andThen(health.paused.await, runStrategy);
|
|
401
|
+
});
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/internal/worker-loop.ts
|
|
404
|
+
const applySpanAttributes = (hooks) => {
|
|
405
|
+
const { spanAttributes } = hooks;
|
|
406
|
+
return (effect) => {
|
|
407
|
+
if (typeof spanAttributes === "undefined") return effect;
|
|
408
|
+
return Effect.tap(effect, () => Effect.orElse(spanAttributes, () => Effect.succeed({})).pipe(Effect.flatMap(Effect.annotateCurrentSpan)));
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
const applyTrackDuration = (hooks) => {
|
|
412
|
+
const { trackDuration } = hooks;
|
|
413
|
+
return (effect) => {
|
|
414
|
+
if (typeof trackDuration === "undefined") return effect;
|
|
415
|
+
return Metric.trackDuration(effect, trackDuration);
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
const applyTimeout = (config) => {
|
|
419
|
+
return (effect) => Effect.timeoutFail(effect, {
|
|
420
|
+
duration: config.tickTimeout,
|
|
421
|
+
onTimeout: () => new Cause.TimeoutException()
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
const applyInnerRetry = (hooks) => {
|
|
425
|
+
const { innerRetry } = hooks;
|
|
426
|
+
return (effect) => {
|
|
427
|
+
if (typeof innerRetry === "undefined") return effect;
|
|
428
|
+
return Effect.retry(effect, innerRetry);
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
const buildPollTick = (worker, health, gate) => {
|
|
432
|
+
const { tick, tickHooks } = worker;
|
|
433
|
+
const spanName = tick.spanName ?? "daemon.tick";
|
|
434
|
+
const withSpanAttrs = applySpanAttributes(tickHooks);
|
|
435
|
+
const withDuration = applyTrackDuration(tickHooks);
|
|
436
|
+
const withTimeout = applyTimeout(tick);
|
|
437
|
+
const withInnerRetry = applyInnerRetry(tickHooks);
|
|
438
|
+
const runWork = (work) => work.pipe(withSpanAttrs, withDuration).pipe(Effect.withSpan(spanName, {
|
|
439
|
+
root: true,
|
|
440
|
+
attributes: { "daemon.name": worker.name }
|
|
441
|
+
}), Effect.withLogSpan(spanName));
|
|
442
|
+
return withInnerRetry(withTimeout(Effect.andThen(health.paused.await, gate).pipe(Effect.flatMap(Option.match({
|
|
443
|
+
onNone: () => Effect.void,
|
|
444
|
+
onSome: runWork
|
|
445
|
+
}))))).pipe(Effect.tap(() => Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", health.name), "latch", "ready"), 1))), Effect.asVoid);
|
|
446
|
+
};
|
|
447
|
+
const wrapSpan = (worker, effect) => {
|
|
448
|
+
const spanName = worker.tick.spanName ?? "daemon.worker";
|
|
449
|
+
return effect.pipe(Effect.withSpan(spanName, {
|
|
450
|
+
root: true,
|
|
451
|
+
attributes: { "daemon.name": worker.name }
|
|
452
|
+
}), Effect.withLogSpan(spanName));
|
|
453
|
+
};
|
|
454
|
+
const buildPollLoop = (worker, loop, health) => {
|
|
455
|
+
const tick = buildPollTick(worker, health, loop.gate);
|
|
456
|
+
return Effect.repeat(tick, Schedule.spaced(loop.interval)).pipe(Effect.asVoid);
|
|
457
|
+
};
|
|
458
|
+
const buildStreamLoop = (worker, loop, health) => {
|
|
459
|
+
const body = Effect.gen(function* () {
|
|
460
|
+
yield* health.paused.await;
|
|
461
|
+
const fiber = yield* Effect.forkScoped(loop.stream.pipe(Stream.tap(() => Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", health.name), "latch", "ready"), 1))), Stream.runDrain));
|
|
462
|
+
const ready = applyTimeout(worker.tick)(health.ready.await);
|
|
463
|
+
yield* Effect.raceFirst(ready, Fiber.join(fiber));
|
|
464
|
+
yield* Fiber.join(fiber);
|
|
465
|
+
});
|
|
466
|
+
return wrapSpan(worker, applyInnerRetry(worker.tickHooks)(body).pipe(Effect.asVoid));
|
|
467
|
+
};
|
|
468
|
+
const buildSubscriptionLoop = (worker, loop, health) => {
|
|
469
|
+
return wrapSpan(worker, Effect.gen(function* () {
|
|
470
|
+
yield* health.paused.await;
|
|
471
|
+
yield* applyTimeout(worker.tick)(loop.acquire);
|
|
472
|
+
yield* Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", health.name), "latch", "ready"), 1));
|
|
473
|
+
return yield* Effect.never;
|
|
474
|
+
}).pipe(Effect.asVoid));
|
|
475
|
+
};
|
|
476
|
+
const buildWorkerLoop = (worker, health) => Match.value(worker.loop).pipe(Match.tag("Poll", (loop) => buildPollLoop(worker, loop, health)), Match.tag("Stream", (loop) => buildStreamLoop(worker, loop, health)), Match.tag("Subscription", (loop) => buildSubscriptionLoop(worker, loop, health)), Match.exhaustive);
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/internal/boot.ts
|
|
479
|
+
const allocateWorkerHealth = (name) => Effect.gen(function* () {
|
|
480
|
+
const ready = yield* Effect.makeLatch(false);
|
|
481
|
+
const healthy = yield* Effect.makeLatch(true);
|
|
482
|
+
const paused = yield* Effect.makeLatch(true);
|
|
483
|
+
yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "ready"), 0);
|
|
484
|
+
yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "healthy"), 1);
|
|
485
|
+
yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "paused"), 1);
|
|
486
|
+
return {
|
|
487
|
+
name,
|
|
488
|
+
ready,
|
|
489
|
+
healthy,
|
|
490
|
+
paused
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
const allocateSupervisorHealth = (name, children) => Effect.gen(function* () {
|
|
494
|
+
const ready = yield* Effect.makeLatch(false);
|
|
495
|
+
const healthy = yield* Effect.makeLatch(true);
|
|
496
|
+
const paused = yield* Effect.makeLatch(true);
|
|
497
|
+
yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "ready"), 0);
|
|
498
|
+
yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "healthy"), 1);
|
|
499
|
+
yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "paused"), 1);
|
|
500
|
+
return {
|
|
501
|
+
name,
|
|
502
|
+
ready,
|
|
503
|
+
healthy,
|
|
504
|
+
paused,
|
|
505
|
+
children
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
const bootChild = (child) => {
|
|
509
|
+
if (isWorker(child)) return allocateWorkerHealth(child.name).pipe(Effect.map((health) => ({
|
|
510
|
+
name: child.name,
|
|
511
|
+
health,
|
|
512
|
+
run: buildWorkerLoop(child, health).pipe(Effect.orDie),
|
|
513
|
+
childPolicy: child.child
|
|
514
|
+
})));
|
|
515
|
+
return Effect.gen(function* () {
|
|
516
|
+
const bootedChildren = yield* Effect.forEach(child.children, bootChild);
|
|
517
|
+
const health = yield* allocateSupervisorHealth(child.name, bootedChildren.map((b) => b.health));
|
|
518
|
+
const run = buildSupervisorBody(child, health, bootedChildren).pipe(Effect.orDie);
|
|
519
|
+
return {
|
|
520
|
+
name: child.name,
|
|
521
|
+
health,
|
|
522
|
+
run,
|
|
523
|
+
childPolicy: {}
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/internal/dynamic.ts
|
|
529
|
+
const buildDynamic = (spec, health) => Effect.gen(function* () {
|
|
530
|
+
const state = yield* Ref.make({
|
|
531
|
+
nextId: 0,
|
|
532
|
+
children: HashMap.empty()
|
|
533
|
+
});
|
|
534
|
+
const startChildImpl = (args) => Effect.gen(function* () {
|
|
535
|
+
const worker = spec.child(args);
|
|
536
|
+
const loop = buildWorkerLoop(worker, yield* allocateWorkerHealth(worker.name)).pipe(Effect.orDie);
|
|
537
|
+
const removed = yield* Effect.makeLatch(false);
|
|
538
|
+
const reservedId = yield* Ref.modify(state, (current) => {
|
|
539
|
+
if (HashMap.size(current.children) >= spec.maxChildren) return [Option.none(), current];
|
|
540
|
+
const children = HashMap.set(current.children, current.nextId, {
|
|
541
|
+
fiber: Option.none(),
|
|
542
|
+
removed
|
|
543
|
+
});
|
|
544
|
+
return [Option.some(current.nextId), {
|
|
545
|
+
nextId: current.nextId + 1,
|
|
546
|
+
children
|
|
547
|
+
}];
|
|
548
|
+
});
|
|
549
|
+
if (Option.isNone(reservedId)) return yield* new DynamicLimitExceeded({ limit: spec.maxChildren });
|
|
550
|
+
const id = reservedId.value;
|
|
551
|
+
yield* Metric.set(supervisorChildrenGauge, HashMap.size(yield* Ref.get(state).pipe(Effect.map((s) => s.children))));
|
|
552
|
+
const cleanup = Effect.gen(function* () {
|
|
553
|
+
const count = yield* Ref.modify(state, (current) => {
|
|
554
|
+
const children = HashMap.remove(current.children, id);
|
|
555
|
+
return [HashMap.size(children), {
|
|
556
|
+
...current,
|
|
557
|
+
children
|
|
558
|
+
}];
|
|
559
|
+
});
|
|
560
|
+
yield* Metric.set(supervisorChildrenGauge, count);
|
|
561
|
+
yield* removed.open;
|
|
562
|
+
}).pipe(Effect.asVoid);
|
|
563
|
+
const fiber = yield* Effect.forkScoped(loop.pipe(Effect.ensuring(cleanup)));
|
|
564
|
+
const count = yield* Ref.modify(state, (current) => {
|
|
565
|
+
const childOpt = HashMap.get(current.children, id);
|
|
566
|
+
if (Option.isNone(childOpt)) return [HashMap.size(current.children), current];
|
|
567
|
+
const children = HashMap.set(current.children, id, {
|
|
568
|
+
...childOpt.value,
|
|
569
|
+
fiber: Option.some(fiber)
|
|
570
|
+
});
|
|
571
|
+
return [HashMap.size(children), {
|
|
572
|
+
nextId: current.nextId,
|
|
573
|
+
children
|
|
574
|
+
}];
|
|
575
|
+
});
|
|
576
|
+
yield* Metric.set(supervisorChildrenGauge, count);
|
|
577
|
+
return {
|
|
578
|
+
id,
|
|
579
|
+
removed: removed.await
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
const stopChildImpl = (ref) => Effect.gen(function* () {
|
|
583
|
+
const [stateOpt, count] = yield* Ref.modify(state, (current) => {
|
|
584
|
+
const found = HashMap.get(current.children, ref.id);
|
|
585
|
+
const children = HashMap.remove(current.children, ref.id);
|
|
586
|
+
return [[found, HashMap.size(children)], {
|
|
587
|
+
...current,
|
|
588
|
+
children
|
|
589
|
+
}];
|
|
590
|
+
});
|
|
591
|
+
if (Option.isSome(stateOpt)) {
|
|
592
|
+
const { fiber, removed } = stateOpt.value;
|
|
593
|
+
yield* Option.match(fiber, {
|
|
594
|
+
onNone: () => Effect.void,
|
|
595
|
+
onSome: (running) => Effect.gen(function* () {
|
|
596
|
+
yield* Fiber.interrupt(running);
|
|
597
|
+
yield* Fiber.await(running);
|
|
598
|
+
})
|
|
599
|
+
});
|
|
600
|
+
yield* Metric.set(supervisorChildrenGauge, count);
|
|
601
|
+
yield* removed.open;
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
const countImpl = Ref.get(state).pipe(Effect.map((current) => HashMap.size(current.children)));
|
|
605
|
+
yield* Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", spec.name), "latch", "ready"), 1));
|
|
606
|
+
return {
|
|
607
|
+
health,
|
|
608
|
+
startChild: startChildImpl,
|
|
609
|
+
stopChild: stopChildImpl,
|
|
610
|
+
count: countImpl
|
|
611
|
+
};
|
|
612
|
+
});
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/internal/lock-gate.ts
|
|
615
|
+
const decideLockGate = (lock) => {
|
|
616
|
+
if (lock.mode === "none") return Option.none();
|
|
617
|
+
return Option.some({
|
|
618
|
+
key: lock.key,
|
|
619
|
+
mode: lock.mode
|
|
620
|
+
});
|
|
621
|
+
};
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/run.ts
|
|
624
|
+
const applyLock = (lock, effect) => {
|
|
625
|
+
const gate = decideLockGate(lock);
|
|
626
|
+
if (Option.isNone(gate)) return effect;
|
|
627
|
+
const locked = withLeaderLock(effect, gate.value);
|
|
628
|
+
if (lock.mode !== "required") return locked;
|
|
629
|
+
const retryWithRestart = (eff) => Effect.retry(eff, {
|
|
630
|
+
schedule: lock.acquireRetryBackoff,
|
|
631
|
+
while: Predicate.isTagged("LeaderLockNotAcquired")
|
|
632
|
+
}).pipe(Effect.catchTag("LeaderLockNotAcquired", () => retryWithRestart(eff)));
|
|
633
|
+
return retryWithRestart(locked);
|
|
634
|
+
};
|
|
635
|
+
const isModeNone = (lock) => lock.mode === "none";
|
|
636
|
+
function worker(w) {
|
|
637
|
+
return Effect.gen(function* () {
|
|
638
|
+
const health = yield* allocateWorkerHealth(w.name);
|
|
639
|
+
const loop = buildWorkerLoop(w, health).pipe(Effect.orDie);
|
|
640
|
+
if (isModeNone(w.lock)) yield* Effect.forkScoped(loop);
|
|
641
|
+
else {
|
|
642
|
+
const locked = applyLock(w.lock, loop);
|
|
643
|
+
yield* Effect.forkScoped(locked.pipe(Effect.orDie));
|
|
644
|
+
}
|
|
645
|
+
return health;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function supervisor(s) {
|
|
649
|
+
return Effect.gen(function* () {
|
|
650
|
+
const booted = yield* Effect.forEach(s.children, bootChild);
|
|
651
|
+
const health = yield* allocateSupervisorHealth(s.name, booted.map((b) => b.health));
|
|
652
|
+
const body = buildSupervisorBody(s, health, booted).pipe(Effect.orDie);
|
|
653
|
+
if (isModeNone(s.lock)) yield* Effect.forkScoped(body);
|
|
654
|
+
else {
|
|
655
|
+
const locked = applyLock(s.lock, body);
|
|
656
|
+
yield* Effect.forkScoped(locked.pipe(Effect.orDie));
|
|
657
|
+
}
|
|
658
|
+
return health;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const dynamic$1 = (spec) => Effect.gen(function* () {
|
|
662
|
+
return yield* buildDynamic(spec, yield* allocateSupervisorHealth(spec.name, []));
|
|
663
|
+
});
|
|
664
|
+
const run = {
|
|
665
|
+
worker,
|
|
666
|
+
supervisor,
|
|
667
|
+
dynamic: dynamic$1
|
|
668
|
+
};
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/supervision-preset.ts
|
|
671
|
+
var LeaderConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/LeaderConfig", { defaultValue: () => ({
|
|
672
|
+
backoffBase: Duration.seconds(1),
|
|
673
|
+
intensity: new UnboundedIntensity(),
|
|
674
|
+
cooldown: Duration.zero
|
|
675
|
+
}) }) {};
|
|
676
|
+
var WorkerConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/WorkerConfig", { defaultValue: () => ({
|
|
677
|
+
backoffBase: Duration.seconds(10),
|
|
678
|
+
intensity: new BoundedIntensity({
|
|
679
|
+
restarts: 10,
|
|
680
|
+
window: Duration.seconds(60)
|
|
681
|
+
}),
|
|
682
|
+
cooldown: Duration.seconds(30)
|
|
683
|
+
}) }) {};
|
|
684
|
+
var TaskConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/TaskConfig", { defaultValue: () => ({
|
|
685
|
+
backoffBase: Duration.seconds(1),
|
|
686
|
+
intensity: new UnboundedIntensity(),
|
|
687
|
+
cooldown: Duration.zero
|
|
688
|
+
}) }) {};
|
|
689
|
+
const Supervision = {
|
|
690
|
+
leader: (cap) => Effect.gen(function* () {
|
|
691
|
+
const config = yield* LeaderConfig;
|
|
692
|
+
return {
|
|
693
|
+
intensity: config.intensity,
|
|
694
|
+
backoff: cappedBackoff(config.backoffBase, cap),
|
|
695
|
+
cooldown: config.cooldown
|
|
696
|
+
};
|
|
697
|
+
}),
|
|
698
|
+
worker: (cap) => Effect.gen(function* () {
|
|
699
|
+
const config = yield* WorkerConfig;
|
|
700
|
+
return {
|
|
701
|
+
intensity: config.intensity,
|
|
702
|
+
backoff: cappedBackoff(config.backoffBase, cap),
|
|
703
|
+
cooldown: config.cooldown
|
|
704
|
+
};
|
|
705
|
+
}),
|
|
706
|
+
task: (budget) => Effect.gen(function* () {
|
|
707
|
+
const config = yield* TaskConfig;
|
|
708
|
+
return {
|
|
709
|
+
intensity: config.intensity,
|
|
710
|
+
backoff: Schedule.exponential(config.backoffBase).pipe(Schedule.jittered, Schedule.upTo(budget)),
|
|
711
|
+
cooldown: config.cooldown
|
|
712
|
+
};
|
|
713
|
+
}),
|
|
714
|
+
custom: (policy) => Effect.succeed(policy)
|
|
715
|
+
};
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/supervisor.ts
|
|
718
|
+
const makeSupervisor = (opts, strategy) => ({
|
|
719
|
+
[SupervisorTypeId]: SupervisorTypeId,
|
|
720
|
+
name: opts.name,
|
|
721
|
+
strategy,
|
|
722
|
+
children: opts.children,
|
|
723
|
+
supervision: opts.supervision,
|
|
724
|
+
lock: opts.lock,
|
|
725
|
+
reporter: opts.reporter ?? {}
|
|
726
|
+
});
|
|
727
|
+
const oneForOne = (opts) => makeSupervisor(opts, "one_for_one");
|
|
728
|
+
const oneForAll = (opts) => makeSupervisor(opts, "one_for_all");
|
|
729
|
+
const restForOne = (opts) => makeSupervisor(opts, "rest_for_one");
|
|
730
|
+
const dynamic = (opts) => ({
|
|
731
|
+
[DynamicSpecTypeId]: DynamicSpecTypeId,
|
|
732
|
+
name: opts.name,
|
|
733
|
+
child: opts.child,
|
|
734
|
+
maxChildren: opts.maxChildren ?? 1e3
|
|
735
|
+
});
|
|
736
|
+
//#endregion
|
|
737
|
+
export { BoundedIntensity, ChildPolicyConfig, Daemon, DaemonReporter, DynamicLimitExceeded, DynamicSpecTypeId, Intensity, IntensityConfig, IntensityTypeId, LeaderConfig, LeaderLock, LeaderLockFromPrimitive, LeaderLockInfraError, LeaderLockNotAcquired, LockPolicyConfig, LockPrimitive, LockPrimitiveError, Supervision, SupervisorTypeId, TaskConfig, TickPolicyConfig, UnboundedIntensity, WorkerConfig, WorkerTypeId, cappedBackoff, dynamic, healthStateGauge, isSupervisor, isWorker, oneForAll, oneForOne, poll, restForOne, run, stream, subscription, supervisor, supervisorChildrenGauge, supervisorExhaustionsCounter, supervisorRestartsCounter, withLeaderLock, worker };
|