@loadmap/cli 0.1.1 → 0.2.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.
|
@@ -12,6 +12,10 @@ function measured(value, unit, o) {
|
|
|
12
12
|
function estimated(value, unit, o) {
|
|
13
13
|
return capacity(value, unit, "estimated", o);
|
|
14
14
|
}
|
|
15
|
+
/** A limit someone set, such as a rate limiter's: exact, so it takes no ±. */
|
|
16
|
+
function configured(value, unit, source) {
|
|
17
|
+
return capacity(value, unit, "configured", source);
|
|
18
|
+
}
|
|
15
19
|
function capacity(value, unit, provenance, o) {
|
|
16
20
|
const { source, within } = typeof o === "string" ? {
|
|
17
21
|
source: o,
|
|
@@ -210,6 +214,19 @@ function workers(component, o) {
|
|
|
210
214
|
fill: o.fill ?? "round-robin"
|
|
211
215
|
};
|
|
212
216
|
}
|
|
217
|
+
function cache(o) {
|
|
218
|
+
return {
|
|
219
|
+
kind: "cache",
|
|
220
|
+
...o
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function limit(component, o = {}) {
|
|
224
|
+
return {
|
|
225
|
+
kind: "limit",
|
|
226
|
+
component,
|
|
227
|
+
...o
|
|
228
|
+
};
|
|
229
|
+
}
|
|
213
230
|
function store(component, o = {}) {
|
|
214
231
|
return {
|
|
215
232
|
kind: "store",
|
|
@@ -229,11 +246,17 @@ function fanout(o) {
|
|
|
229
246
|
...o
|
|
230
247
|
};
|
|
231
248
|
}
|
|
249
|
+
/** The share a stage turns away when its component is past `full`: what is above it. */
|
|
250
|
+
function rejectShare(util, full) {
|
|
251
|
+
return util != null && util > full ? 1 - full / util : 0;
|
|
252
|
+
}
|
|
232
253
|
function dyn(v, ctx) {
|
|
233
254
|
return typeof v === "function" ? v(ctx) : v;
|
|
234
255
|
}
|
|
235
|
-
function resolveStage(id, s, ctx, flowIds) {
|
|
256
|
+
function resolveStage(id, s, ctx, flowIds, full) {
|
|
236
257
|
const label = s.label ?? id;
|
|
258
|
+
const util = (c) => c ? ctx.util[c] : null;
|
|
259
|
+
const rejects = rejectShare(util(s.kind === "sink" ? void 0 : s.component), full);
|
|
237
260
|
switch (s.kind) {
|
|
238
261
|
case "queue": return {
|
|
239
262
|
id,
|
|
@@ -242,15 +265,32 @@ function resolveStage(id, s, ctx, flowIds) {
|
|
|
242
265
|
component: s.component,
|
|
243
266
|
partitions: dyn(s.partitions, ctx)
|
|
244
267
|
};
|
|
245
|
-
case "workers":
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
268
|
+
case "workers": {
|
|
269
|
+
const perPod = dyn(s.perPod, ctx);
|
|
270
|
+
const base = {
|
|
271
|
+
id,
|
|
272
|
+
label,
|
|
273
|
+
kind: "workers",
|
|
274
|
+
component: s.component,
|
|
275
|
+
perPod,
|
|
276
|
+
fill: s.fill
|
|
277
|
+
};
|
|
278
|
+
if (!s.autoscale) {
|
|
279
|
+
const pods = dyn(s.pods ?? 0, ctx);
|
|
280
|
+
return {
|
|
281
|
+
...base,
|
|
282
|
+
pods,
|
|
283
|
+
running: pods
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const a = autoscaleOf(id, s.autoscale, ctx);
|
|
287
|
+
return {
|
|
288
|
+
...base,
|
|
289
|
+
pods: a.max,
|
|
290
|
+
running: running(a, util(s.component)),
|
|
291
|
+
autoscale: a
|
|
292
|
+
};
|
|
293
|
+
}
|
|
254
294
|
case "fanout": {
|
|
255
295
|
const to = dyn(s.to, ctx);
|
|
256
296
|
const perFlow = {};
|
|
@@ -260,26 +300,72 @@ function resolveStage(id, s, ctx, flowIds) {
|
|
|
260
300
|
label,
|
|
261
301
|
kind: "fanout",
|
|
262
302
|
component: s.component,
|
|
263
|
-
to: perFlow
|
|
303
|
+
to: perFlow,
|
|
304
|
+
rejects,
|
|
305
|
+
shed: rejects
|
|
264
306
|
};
|
|
265
307
|
}
|
|
308
|
+
case "cache": return {
|
|
309
|
+
id,
|
|
310
|
+
label,
|
|
311
|
+
kind: "cache",
|
|
312
|
+
component: s.component,
|
|
313
|
+
hit: hitOf(id, s, ctx),
|
|
314
|
+
rejects,
|
|
315
|
+
shed: rejects
|
|
316
|
+
};
|
|
266
317
|
case "sink": return {
|
|
267
318
|
id,
|
|
268
319
|
label,
|
|
269
320
|
kind: "sink"
|
|
270
321
|
};
|
|
322
|
+
case "store": return {
|
|
323
|
+
id,
|
|
324
|
+
label,
|
|
325
|
+
kind: "store",
|
|
326
|
+
component: s.component
|
|
327
|
+
};
|
|
271
328
|
default: return {
|
|
272
329
|
id,
|
|
273
330
|
label,
|
|
274
331
|
kind: s.kind,
|
|
275
|
-
component: s.component
|
|
332
|
+
component: s.component,
|
|
333
|
+
rejects,
|
|
334
|
+
shed: rejects
|
|
276
335
|
};
|
|
277
336
|
}
|
|
278
337
|
}
|
|
338
|
+
/** A cache's hit share, checked: a share outside 0 to 1 is a mistake, not a rate. */
|
|
339
|
+
function hitOf(id, s, ctx) {
|
|
340
|
+
const hit = dyn(s.hit, ctx);
|
|
341
|
+
if (!(hit >= 0 && hit <= 1)) throw new Error(`loadmap: cache "${id}" needs a hit share from 0 to 1, not ${hit}`);
|
|
342
|
+
return hit;
|
|
343
|
+
}
|
|
344
|
+
function autoscaleOf(id, a, ctx) {
|
|
345
|
+
const min = dyn(a.min, ctx);
|
|
346
|
+
const max = dyn(a.max, ctx);
|
|
347
|
+
const target = dyn(a.target, ctx);
|
|
348
|
+
if (!(Number.isInteger(min) && Number.isInteger(max) && min >= 1 && max >= min)) throw new Error(`loadmap: workers "${id}" autoscale needs whole numbers with 1 ≤ min ≤ max, not ${min} and ${max}`);
|
|
349
|
+
if (!(target > 0 && target <= 1)) throw new Error(`loadmap: workers "${id}" autoscale needs a target above 0 and at most 1, not ${target}`);
|
|
350
|
+
return {
|
|
351
|
+
min,
|
|
352
|
+
max,
|
|
353
|
+
target
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The pods an autoscaler runs: enough to hold each near its target. `util` is against the pool at max pods, so
|
|
358
|
+
* util × max is the work in pods' worth of capacity.
|
|
359
|
+
*/
|
|
360
|
+
function running(a, util) {
|
|
361
|
+
const need = Math.ceil((util ?? 0) * a.max / a.target - 1e-9);
|
|
362
|
+
return Math.min(a.max, Math.max(a.min, need));
|
|
363
|
+
}
|
|
279
364
|
// ../core/src/model.ts
|
|
280
365
|
const RESERVED$1 = [
|
|
281
366
|
"load",
|
|
282
367
|
"flows",
|
|
368
|
+
"at",
|
|
283
369
|
"demand",
|
|
284
370
|
"util",
|
|
285
371
|
"derived"
|
|
@@ -299,6 +385,11 @@ function defineModel(def) {
|
|
|
299
385
|
if (st.kind === "fanout" && i < route.length - 1) throw new Error(`loadmap: fan-out "${s}" must be the last stage of the route of "${f}"`);
|
|
300
386
|
});
|
|
301
387
|
}
|
|
388
|
+
for (const c of componentIds) if (!def.components[c].demand && !stageIds.some((s) => def.stages[s].component === c)) throw new Error(`loadmap: component "${c}" needs a demand: no stage uses it`);
|
|
389
|
+
for (const s of stageIds) {
|
|
390
|
+
const st = def.stages[s];
|
|
391
|
+
if (st.kind === "workers" && st.pods == null === (st.autoscale == null)) throw new Error(`loadmap: workers "${s}" needs pods or autoscale, one of them`);
|
|
392
|
+
}
|
|
302
393
|
const thresholds = {
|
|
303
394
|
target: def.thresholds?.target ?? .5,
|
|
304
395
|
full: def.thresholds?.full ?? 1
|
|
@@ -397,8 +488,17 @@ function defineModel(def) {
|
|
|
397
488
|
const stated = context(def, values);
|
|
398
489
|
const uncertain = componentIds.filter((c) => (stated.capacity(c).within ?? 0) > 0);
|
|
399
490
|
const components = {};
|
|
491
|
+
const seen = /* @__PURE__ */ new Map();
|
|
492
|
+
const utilOf = (bound, load) => {
|
|
493
|
+
const k = `${bound}|${load}`;
|
|
494
|
+
const hit = seen.get(k);
|
|
495
|
+
if (hit) return hit;
|
|
496
|
+
const u = utilAt(values, bound)(load);
|
|
497
|
+
seen.set(k, u);
|
|
498
|
+
return u;
|
|
499
|
+
};
|
|
400
500
|
for (const c of componentIds) {
|
|
401
|
-
const at = (bound) => (load) =>
|
|
501
|
+
const at = (bound) => (load) => utilOf(bound, load)[c];
|
|
402
502
|
const both = (th) => {
|
|
403
503
|
const x = crossing(at(), th, searchMax);
|
|
404
504
|
if (x == null) return {
|
|
@@ -485,7 +585,7 @@ function defineModel(def) {
|
|
|
485
585
|
...defaults(),
|
|
486
586
|
...partial
|
|
487
587
|
};
|
|
488
|
-
const { ctx, capacity, over, describe } = context(def, values);
|
|
588
|
+
const { ctx, capacity, over, describe, overload: overloadOf, retries, shed } = context(def, values);
|
|
489
589
|
const crossings = crossingsFor(values);
|
|
490
590
|
const components = {};
|
|
491
591
|
for (const c of componentIds) {
|
|
@@ -525,15 +625,24 @@ function defineModel(def) {
|
|
|
525
625
|
const flows = {};
|
|
526
626
|
for (const f of flowIds) flows[f] = ctx.flows[f];
|
|
527
627
|
const stages = {};
|
|
528
|
-
for (const s of stageIds)
|
|
628
|
+
for (const s of stageIds) {
|
|
629
|
+
const st = resolveStage(s, def.stages[s], ctx, flowIds, thresholds.full);
|
|
630
|
+
stages[s] = st.kind === "pass" || st.kind === "limit" || st.kind === "cache" ? {
|
|
631
|
+
...st,
|
|
632
|
+
shed: shed(s)
|
|
633
|
+
} : st;
|
|
634
|
+
}
|
|
529
635
|
for (const st of Object.values(stages)) if (st.kind === "fanout") for (const b of Object.values(st.to).flat()) {
|
|
530
636
|
const to = stages[b.stage];
|
|
531
637
|
if (!to || to.kind === "queue" || to.kind === "workers" || to.kind === "fanout") throw new Error(`loadmap: fan-out "${st.id}" branches to "${b.stage}", which is not a sink, store or pass stage`);
|
|
532
638
|
}
|
|
533
639
|
const overload = {};
|
|
534
|
-
for (const f of flowIds)
|
|
535
|
-
|
|
536
|
-
|
|
640
|
+
for (const f of flowIds) overload[f] = overloadOf(f);
|
|
641
|
+
const at = {};
|
|
642
|
+
for (const s of stageIds) {
|
|
643
|
+
const row = {};
|
|
644
|
+
for (const f of flowIds) row[f] = ctx.at[s][f];
|
|
645
|
+
at[s] = row;
|
|
537
646
|
}
|
|
538
647
|
const assumptions = [];
|
|
539
648
|
for (const c of componentIds) {
|
|
@@ -567,7 +676,9 @@ function defineModel(def) {
|
|
|
567
676
|
assumptions,
|
|
568
677
|
stages,
|
|
569
678
|
routes: def.routes,
|
|
679
|
+
at,
|
|
570
680
|
overload,
|
|
681
|
+
retry: retries(),
|
|
571
682
|
ctx
|
|
572
683
|
};
|
|
573
684
|
};
|
|
@@ -604,9 +715,10 @@ function defineModel(def) {
|
|
|
604
715
|
}
|
|
605
716
|
/**
|
|
606
717
|
* The model's numbers for one set of values. Every number is computed on first use and kept, so a model function
|
|
607
|
-
* may read any other; a value that needs itself throws, naming the loop.
|
|
718
|
+
* may read any other; a value that needs itself throws, naming the loop. Retries are the one loop that is solved:
|
|
719
|
+
* a retrying flow's attempts settle where the share its route turns away gives back the same attempts.
|
|
608
720
|
*/
|
|
609
|
-
function context(def, values, bound, pinned) {
|
|
721
|
+
function context(def, values, bound, pinned, fixed) {
|
|
610
722
|
const cache = /* @__PURE__ */ new Map();
|
|
611
723
|
const stack = [];
|
|
612
724
|
const memo = (key, f) => {
|
|
@@ -630,15 +742,205 @@ function context(def, values, bound, pinned) {
|
|
|
630
742
|
return Object.freeze(t);
|
|
631
743
|
};
|
|
632
744
|
const ctx = { ...values };
|
|
745
|
+
const full = def.thresholds?.full ?? 1;
|
|
633
746
|
const flowIds = Object.keys(def.flows);
|
|
634
747
|
const componentIds = Object.keys(def.components);
|
|
748
|
+
const stages = def.stages;
|
|
749
|
+
const stageIds = Object.keys(stages);
|
|
750
|
+
const routes = def.routes;
|
|
635
751
|
const capacity = (c) => memo(`capacity of ${c}`, () => {
|
|
636
752
|
const cap = def.components[c].capacity;
|
|
637
753
|
return typeof cap === "function" ? cap(ctx) : cap;
|
|
638
754
|
});
|
|
639
755
|
const enabled = (c) => memo(`${c} enabled`, () => def.components[c].when?.(ctx) ?? true);
|
|
640
|
-
|
|
641
|
-
|
|
756
|
+
const overload = (f) => memo(`overload of ${f}`, () => {
|
|
757
|
+
const o = def.flows[f].overload;
|
|
758
|
+
return (typeof o === "function" ? o(ctx) : o) ?? {};
|
|
759
|
+
});
|
|
760
|
+
const retryOf = (f) => {
|
|
761
|
+
const rule = def.flows[f].retry;
|
|
762
|
+
const r = typeof rule === "function" ? rule(values) : rule;
|
|
763
|
+
if (!r) return void 0;
|
|
764
|
+
if (!(Number.isInteger(r.times) && r.times >= 0)) throw new Error(`loadmap: flow "${f}" retries a whole number of times, not ${r.times}`);
|
|
765
|
+
if (r.budget != null && !(r.budget >= 0 && Number.isFinite(r.budget))) throw new Error(`loadmap: flow "${f}" needs a retry budget of 0 or more, not ${r.budget}`);
|
|
766
|
+
return r.times > 0 && r.budget !== 0 ? r : void 0;
|
|
767
|
+
};
|
|
768
|
+
const rate = (f) => memo(`rate of ${f}`, () => def.flows[f].rate(ctx));
|
|
769
|
+
ctx.flows = table(flowIds, (f) => memo(`flow ${f}`, () => {
|
|
770
|
+
if (fixed && Object.hasOwn(fixed, f)) return fixed[f];
|
|
771
|
+
return retryOf(f) ? retries()[f].attempts : def.flows[f].rate(ctx);
|
|
772
|
+
}));
|
|
773
|
+
/** The share a stage turns away: past its component's `full`, what is above it. */
|
|
774
|
+
const rejects = (s) => memo(`rejects at ${s}`, () => {
|
|
775
|
+
const st = stages[s];
|
|
776
|
+
if (!(st.kind === "pass" || st.kind === "limit" || st.kind === "cache" || st.kind === "fanout") || !st.component) return 0;
|
|
777
|
+
return rejectShare(ctx.util[st.component], full);
|
|
778
|
+
});
|
|
779
|
+
const hit = (s) => memo(`hit at ${s}`, () => hitOf(s, stages[s], ctx));
|
|
780
|
+
/** The share of a flow's units that go on past a stage: cache hits stay, and what a limiter turns away. */
|
|
781
|
+
const onward = (s) => {
|
|
782
|
+
const st = stages[s];
|
|
783
|
+
return st.kind === "cache" ? 1 - hit(s) : st.kind === "limit" ? 1 - rejects(s) : 1;
|
|
784
|
+
};
|
|
785
|
+
/** Copies per unit of a flow that a fan-out at t sends to s: 0 unless t is a fan-out with a branch there. */
|
|
786
|
+
const copies = (t, f, s) => {
|
|
787
|
+
const st = stages[t];
|
|
788
|
+
if (st.kind !== "fanout") return 0;
|
|
789
|
+
const to = memo(`branches of ${t}`, () => dyn(st.to, ctx));
|
|
790
|
+
let n = 0;
|
|
791
|
+
for (const b of Array.isArray(to) ? to : to[f] ?? []) if (b.stage === s) n += b.share;
|
|
792
|
+
return n;
|
|
793
|
+
};
|
|
794
|
+
const arriving = (s, f) => memo(`${f} at ${s}`, () => {
|
|
795
|
+
const route = routes[f] ?? [];
|
|
796
|
+
const last = route.findLastIndex((t) => t === s || copies(t, f, s) > 0);
|
|
797
|
+
let total = 0;
|
|
798
|
+
let r = ctx.flows[f];
|
|
799
|
+
for (const [i, t] of route.entries()) {
|
|
800
|
+
if (t === s) total += r;
|
|
801
|
+
total += r * copies(t, f, s);
|
|
802
|
+
if (i >= last) break;
|
|
803
|
+
r *= onward(t);
|
|
804
|
+
}
|
|
805
|
+
return total;
|
|
806
|
+
});
|
|
807
|
+
const inner = /* @__PURE__ */ new Map();
|
|
808
|
+
ctx.at = table(stageIds, (s) => {
|
|
809
|
+
const known = inner.get(s);
|
|
810
|
+
if (known) return known;
|
|
811
|
+
const t = table(flowIds, (f) => arriving(s, f));
|
|
812
|
+
inner.set(s, t);
|
|
813
|
+
return t;
|
|
814
|
+
});
|
|
815
|
+
/**
|
|
816
|
+
* The share of a flow's attempts turned away somewhere on its route; a cache hit is an answer, not a failure. A
|
|
817
|
+
* stage's rejections are counted against what is offered to it, so behind another stage that turns work away it
|
|
818
|
+
* passes at most what it can take (offered × (1 − rejects)), not a share of a share: the bottleneck decides.
|
|
819
|
+
*/
|
|
820
|
+
const failure = (f) => memo(`failures of ${f}`, () => {
|
|
821
|
+
let offered = 1;
|
|
822
|
+
let actual = 1;
|
|
823
|
+
let answered = 0;
|
|
824
|
+
for (const s of routes[f] ?? []) {
|
|
825
|
+
const r = stages[s].kind === "fanout" ? 0 : rejects(s);
|
|
826
|
+
if (r > 0) actual = Math.min(actual, offered * (1 - r));
|
|
827
|
+
if (stages[s].kind === "cache") {
|
|
828
|
+
answered += actual * hit(s);
|
|
829
|
+
actual *= 1 - hit(s);
|
|
830
|
+
}
|
|
831
|
+
offered *= onward(s);
|
|
832
|
+
}
|
|
833
|
+
return Math.max(0, 1 - answered - actual);
|
|
834
|
+
});
|
|
835
|
+
/**
|
|
836
|
+
* What actually reaches a stage, per second: like `at`, less what stages before it turned away. A stage takes up to
|
|
837
|
+
* what is offered to it × (1 − rejects), so behind another stage that turns work away the bottleneck decides.
|
|
838
|
+
*/
|
|
839
|
+
const actual = (s, f) => memo(`${f} reaching ${s}`, () => {
|
|
840
|
+
const route = routes[f] ?? [];
|
|
841
|
+
const last = route.findLastIndex((t) => t === s || copies(t, f, s) > 0);
|
|
842
|
+
let total = 0;
|
|
843
|
+
let r = ctx.flows[f];
|
|
844
|
+
for (const [i, t] of route.entries()) {
|
|
845
|
+
if (t === s) total += r;
|
|
846
|
+
total += r * copies(t, f, s) * (1 - rejects(t));
|
|
847
|
+
if (i >= last) break;
|
|
848
|
+
const cut = rejects(t);
|
|
849
|
+
if (cut > 0) r = Math.min(r, arriving(t, f) * (1 - cut));
|
|
850
|
+
if (stages[t].kind === "cache") r *= 1 - hit(t);
|
|
851
|
+
}
|
|
852
|
+
return total;
|
|
853
|
+
});
|
|
854
|
+
/** The share of what actually reaches a stage that it turns away: the one number the sim and the meters show. */
|
|
855
|
+
const shed = (s) => memo(`shed at ${s}`, () => {
|
|
856
|
+
const r = rejects(s);
|
|
857
|
+
if (!r) return 0;
|
|
858
|
+
let offered = 0;
|
|
859
|
+
let reaching = 0;
|
|
860
|
+
for (const f of flowIds) {
|
|
861
|
+
offered += arriving(s, f);
|
|
862
|
+
reaching += actual(s, f);
|
|
863
|
+
}
|
|
864
|
+
return reaching > 0 ? Math.max(0, 1 - offered * (1 - r) / reaching) : 0;
|
|
865
|
+
});
|
|
866
|
+
const retries = () => memo("retries", () => {
|
|
867
|
+
const ids = flowIds.filter((f) => retryOf(f));
|
|
868
|
+
const lam = {};
|
|
869
|
+
for (const f of ids) lam[f] = rate(f);
|
|
870
|
+
const most = (f) => {
|
|
871
|
+
const r = retryOf(f);
|
|
872
|
+
return Math.min(r.times, r.budget ?? Number.POSITIVE_INFINITY);
|
|
873
|
+
};
|
|
874
|
+
const attempts = (e) => {
|
|
875
|
+
const a = {};
|
|
876
|
+
for (const f of ids) a[f] = lam[f] * (1 + e[f]);
|
|
877
|
+
return a;
|
|
878
|
+
};
|
|
879
|
+
const wanted = (e) => {
|
|
880
|
+
const sub = context(def, values, bound, pinned, attempts(e));
|
|
881
|
+
const w = {};
|
|
882
|
+
const p = {};
|
|
883
|
+
for (const f of ids) {
|
|
884
|
+
p[f] = sub.failure(f);
|
|
885
|
+
w[f] = Math.min(excess(p[f], retryOf(f).times), most(f));
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
w,
|
|
889
|
+
p
|
|
890
|
+
};
|
|
891
|
+
};
|
|
892
|
+
let e = Object.fromEntries(ids.map((f) => [f, 0]));
|
|
893
|
+
for (let step = 0; step < 100; step++) {
|
|
894
|
+
const { w, p: failed } = wanted(e);
|
|
895
|
+
const d = {};
|
|
896
|
+
for (const f of ids) d[f] = Math.max(0, w[f] - e[f]);
|
|
897
|
+
const live = ids.filter((f) => lam[f] > 0 && d[f] > (step ? 1e-10 * (1 + e[f]) : 0));
|
|
898
|
+
if (!live.length) {
|
|
899
|
+
const a = attempts(e);
|
|
900
|
+
const out = {};
|
|
901
|
+
for (const f of ids) {
|
|
902
|
+
const l = lam[f];
|
|
903
|
+
const p = failed[f];
|
|
904
|
+
const times = retryOf(f).times;
|
|
905
|
+
const retried = p > 0 && excess(p, times) > e[f] * (1 + 1e-12) ? root((y) => excess(y, times), e[f], p) / p : 1;
|
|
906
|
+
out[f] = {
|
|
907
|
+
times,
|
|
908
|
+
rate: l,
|
|
909
|
+
attempts: a[f],
|
|
910
|
+
rejected: p,
|
|
911
|
+
retried,
|
|
912
|
+
failed: l > 0 ? 1 - (1 - p) * (1 + e[f]) : 0
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
return out;
|
|
916
|
+
}
|
|
917
|
+
const along = (t) => {
|
|
918
|
+
const y = { ...e };
|
|
919
|
+
for (const f of live) y[f] = e[f] + t * d[f];
|
|
920
|
+
return y;
|
|
921
|
+
};
|
|
922
|
+
const holds = (t) => {
|
|
923
|
+
const y = along(t);
|
|
924
|
+
const wy = wanted(y).w;
|
|
925
|
+
return Math.min(...live.map((f) => (wy[f] - y[f]) / d[f]));
|
|
926
|
+
};
|
|
927
|
+
const far = Math.min(...live.map((f) => (most(f) - e[f]) / d[f]));
|
|
928
|
+
if (far <= 1) e = along(far);
|
|
929
|
+
else {
|
|
930
|
+
const h1 = holds(1);
|
|
931
|
+
e = along(h1 > 0 ? settle(holds, 1, far, h1) : 1);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
throw new Error(`loadmap: the retries of ${joinAnd(ids.map((f) => `"${f}"`))} do not settle`);
|
|
935
|
+
});
|
|
936
|
+
const usedBy = (c) => stageIds.filter((s) => stages[s].component === c);
|
|
937
|
+
ctx.demand = table(componentIds, (c) => memo(`demand of ${c}`, () => {
|
|
938
|
+
const d = def.components[c].demand;
|
|
939
|
+
if (d) return d(ctx);
|
|
940
|
+
let total = 0;
|
|
941
|
+
for (const s of usedBy(c)) for (const f of flowIds) total += arriving(s, f);
|
|
942
|
+
return total;
|
|
943
|
+
}));
|
|
642
944
|
ctx.util = table(componentIds, (c) => memo(`utilization of ${c}`, () => {
|
|
643
945
|
if (!enabled(c)) return null;
|
|
644
946
|
const cap = boundOf(capacity(c), c === pinned ? void 0 : bound);
|
|
@@ -650,10 +952,55 @@ function context(def, values, bound, pinned) {
|
|
|
650
952
|
return {
|
|
651
953
|
ctx: Object.freeze(ctx),
|
|
652
954
|
capacity,
|
|
955
|
+
overload,
|
|
956
|
+
failure,
|
|
957
|
+
shed,
|
|
958
|
+
retries: () => flowIds.some((f) => retryOf(f)) ? retries() : {},
|
|
653
959
|
over: (c) => text(def.components[c].over) ?? "",
|
|
654
960
|
describe: (c) => text(def.components[c].describe)
|
|
655
961
|
};
|
|
656
962
|
}
|
|
963
|
+
/** y + y² + … + yⁿ: the retries per unit when each attempt fails with share y and n retries may follow. */
|
|
964
|
+
function excess(y, n) {
|
|
965
|
+
return y >= 1 ? n : y * (1 - y ** n) / (1 - y);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Where h, above 0 at lo and at most 0 at hi, reaches 0: regula falsi with the Illinois step, a few evaluations where
|
|
969
|
+
* bisection takes forty, and still bracketed when h has a kink (the knee where rejections start).
|
|
970
|
+
*/
|
|
971
|
+
function settle(h, lo, hi, hl = h(lo)) {
|
|
972
|
+
let hh = h(hi);
|
|
973
|
+
if (hh >= 0) return hi;
|
|
974
|
+
let side = 0;
|
|
975
|
+
let x = lo;
|
|
976
|
+
for (let k = 0; k < 100 && hi - lo > 1e-13 * hi; k++) {
|
|
977
|
+
x = (lo * hh - hi * hl) / (hh - hl);
|
|
978
|
+
const hx = h(x);
|
|
979
|
+
if (hx === 0) break;
|
|
980
|
+
if (hx > 0) {
|
|
981
|
+
lo = x;
|
|
982
|
+
hl = hx;
|
|
983
|
+
if (side === 1) hh /= 2;
|
|
984
|
+
side = 1;
|
|
985
|
+
} else {
|
|
986
|
+
hi = x;
|
|
987
|
+
hh = hx;
|
|
988
|
+
if (side === -1) hl /= 2;
|
|
989
|
+
side = -1;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return x;
|
|
993
|
+
}
|
|
994
|
+
/** The y in [0, hi] where a rising f(y) reaches `goal`. */
|
|
995
|
+
function root(f, goal, hi) {
|
|
996
|
+
let lo = 0;
|
|
997
|
+
for (let k = 0; k < 200 && hi - lo > 1e-15; k++) {
|
|
998
|
+
const mid = (lo + hi) / 2;
|
|
999
|
+
if (f(mid) < goal) lo = mid;
|
|
1000
|
+
else hi = mid;
|
|
1001
|
+
}
|
|
1002
|
+
return (lo + hi) / 2;
|
|
1003
|
+
}
|
|
657
1004
|
/**
|
|
658
1005
|
* The lowest load multiple at which `util(load)` reaches `th`. A coarse geometric scan finds the first bracket, so a
|
|
659
1006
|
* utilization that does not rise steadily with the load is not mis-bisected; bisection then narrows it.
|
|
@@ -688,9 +1035,9 @@ function reach(r, searchMax, when = () => null) {
|
|
|
688
1035
|
if (!r) return multiple(null);
|
|
689
1036
|
return `${multiples(r, searchMax)}${months(when(r.low), when(r.high))}`;
|
|
690
1037
|
}
|
|
691
|
-
/** " (capacity estimated ±30 %)"; nothing for a measured capacity without a
|
|
1038
|
+
/** " (capacity estimated ±30 %)"; nothing for a measured capacity without a ± or a configured one. */
|
|
692
1039
|
function sureness(c) {
|
|
693
|
-
if (!c || c.provenance
|
|
1040
|
+
if (!c || c.provenance !== "estimated" && !c.within) return "";
|
|
694
1041
|
return ` (capacity ${c.provenance}${c.within ? ` ±${pct(c.within)}` : ""})`;
|
|
695
1042
|
}
|
|
696
1043
|
function verdict(load, live, first, range, th, searchMax, when) {
|
|
@@ -1048,12 +1395,12 @@ const COMPONENT_PROPS = {
|
|
|
1048
1395
|
demand: {
|
|
1049
1396
|
value: "number",
|
|
1050
1397
|
scope: "ctx",
|
|
1051
|
-
doc: "What the component must handle, in its capacity's unit."
|
|
1398
|
+
doc: "What the component must handle, in its capacity's unit. Without one: the units per second reaching its stages."
|
|
1052
1399
|
},
|
|
1053
1400
|
capacity: {
|
|
1054
1401
|
value: "capacity",
|
|
1055
1402
|
scope: "ctx",
|
|
1056
|
-
doc: "What it can handle: measured(value, unit, source) or estimated(value, unit, source)
|
|
1403
|
+
doc: "What it can handle: measured(value, unit, source) or estimated(value, unit, source), `value ± 30%` saying how sure it is; configured(value, unit, source) for a limit someone set."
|
|
1057
1404
|
},
|
|
1058
1405
|
when: {
|
|
1059
1406
|
value: "boolean",
|
|
@@ -1492,7 +1839,11 @@ const SCHEMA = {
|
|
|
1492
1839
|
doc: "A theme colour (accent, alt, ok, warn, drop, muted) or a CSS colour in quotes."
|
|
1493
1840
|
}
|
|
1494
1841
|
},
|
|
1495
|
-
children: [
|
|
1842
|
+
children: [
|
|
1843
|
+
"drop",
|
|
1844
|
+
"merge",
|
|
1845
|
+
"retry"
|
|
1846
|
+
]
|
|
1496
1847
|
},
|
|
1497
1848
|
drop: {
|
|
1498
1849
|
doc: "Units older than olderThan simulated seconds are dropped before they are handled.",
|
|
@@ -1534,6 +1885,30 @@ const SCHEMA = {
|
|
|
1534
1885
|
},
|
|
1535
1886
|
children: []
|
|
1536
1887
|
},
|
|
1888
|
+
retry: {
|
|
1889
|
+
doc: "A unit turned away on the route (by a pass, limit or cache stage past capacity) is sent again, up to times more times. The attempts settle where the retries they cause are the ones assumed.",
|
|
1890
|
+
example: "retry { times: 3; budget: 0.1 }",
|
|
1891
|
+
args: [],
|
|
1892
|
+
props: {
|
|
1893
|
+
times: {
|
|
1894
|
+
value: "number",
|
|
1895
|
+
scope: "values",
|
|
1896
|
+
doc: "Retries after the first attempt: a whole number. Reads only the parameters.",
|
|
1897
|
+
required: true
|
|
1898
|
+
},
|
|
1899
|
+
budget: {
|
|
1900
|
+
value: "number",
|
|
1901
|
+
scope: "values",
|
|
1902
|
+
doc: "At most this share of the flow is added as retries: 0.1 adds at most 10 %. Without one, every retry is sent."
|
|
1903
|
+
},
|
|
1904
|
+
when: {
|
|
1905
|
+
value: "boolean",
|
|
1906
|
+
scope: "values",
|
|
1907
|
+
doc: "The rule applies only while this is true; it reads only parameters."
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
children: []
|
|
1911
|
+
},
|
|
1537
1912
|
component: {
|
|
1538
1913
|
doc: "Something that can run out: its demand against its capacity. On a box, write these lines on the box.",
|
|
1539
1914
|
example: "component cache \"Cache\" { demand: flows.reads; capacity: estimated(9000, \"req/s\") }",
|
|
@@ -1568,6 +1943,8 @@ const SCHEMA = {
|
|
|
1568
1943
|
"store",
|
|
1569
1944
|
"sink",
|
|
1570
1945
|
"fanout",
|
|
1946
|
+
"cache",
|
|
1947
|
+
"limit",
|
|
1571
1948
|
"component"
|
|
1572
1949
|
]
|
|
1573
1950
|
},
|
|
@@ -1614,6 +1991,28 @@ const SCHEMA = {
|
|
|
1614
1991
|
props: { component: STAGE_COMPONENT },
|
|
1615
1992
|
children: []
|
|
1616
1993
|
},
|
|
1994
|
+
cache: {
|
|
1995
|
+
doc: "A share of the units (hit) is answered here and goes no further; the rest carry on to the next stage. Past capacity the share above 100 % is rejected.",
|
|
1996
|
+
example: "cache { hit: 0.8 }",
|
|
1997
|
+
args: STAGE_LABEL,
|
|
1998
|
+
props: {
|
|
1999
|
+
hit: {
|
|
2000
|
+
value: "number",
|
|
2001
|
+
scope: "ctx",
|
|
2002
|
+
doc: "The share answered here, 0 to 1.",
|
|
2003
|
+
required: true
|
|
2004
|
+
},
|
|
2005
|
+
component: STAGE_COMPONENT
|
|
2006
|
+
},
|
|
2007
|
+
children: []
|
|
2008
|
+
},
|
|
2009
|
+
limit: {
|
|
2010
|
+
doc: "A rate limiter: its component's capacity is the limit. Above it the share over is turned away, and the stages after it see only what got through.",
|
|
2011
|
+
example: "limit",
|
|
2012
|
+
args: STAGE_LABEL,
|
|
2013
|
+
props: { component: STAGE_COMPONENT },
|
|
2014
|
+
children: []
|
|
2015
|
+
},
|
|
1617
2016
|
store: {
|
|
1618
2017
|
doc: "Where a flow ends: written to its component.",
|
|
1619
2018
|
example: "store",
|
|
@@ -1658,8 +2057,7 @@ const SCHEMA = {
|
|
|
1658
2057
|
pods: {
|
|
1659
2058
|
value: "number",
|
|
1660
2059
|
scope: "ctx",
|
|
1661
|
-
doc: "How many pods."
|
|
1662
|
-
required: true
|
|
2060
|
+
doc: "How many pods; an autoscaled pool has autoscale instead."
|
|
1663
2061
|
},
|
|
1664
2062
|
perPod: {
|
|
1665
2063
|
value: "number",
|
|
@@ -1675,6 +2073,32 @@ const SCHEMA = {
|
|
|
1675
2073
|
},
|
|
1676
2074
|
component: STAGE_COMPONENT
|
|
1677
2075
|
},
|
|
2076
|
+
children: ["autoscale"]
|
|
2077
|
+
},
|
|
2078
|
+
autoscale: {
|
|
2079
|
+
doc: "Pods are added and removed to hold each running pod near target, between min and max. The component's capacity is the pool at max pods: it runs out when the pool cannot grow further.",
|
|
2080
|
+
example: "autoscale { min: 2; max: 10; target: 0.7 }",
|
|
2081
|
+
args: [],
|
|
2082
|
+
props: {
|
|
2083
|
+
min: {
|
|
2084
|
+
value: "number",
|
|
2085
|
+
scope: "ctx",
|
|
2086
|
+
doc: "The fewest pods, at least 1.",
|
|
2087
|
+
required: true
|
|
2088
|
+
},
|
|
2089
|
+
max: {
|
|
2090
|
+
value: "number",
|
|
2091
|
+
scope: "ctx",
|
|
2092
|
+
doc: "The most pods.",
|
|
2093
|
+
required: true
|
|
2094
|
+
},
|
|
2095
|
+
target: {
|
|
2096
|
+
value: "number",
|
|
2097
|
+
scope: "ctx",
|
|
2098
|
+
doc: "The utilization each running pod is held at, above 0 and at most 1: 0.7 scales out above 70 %.",
|
|
2099
|
+
required: true
|
|
2100
|
+
}
|
|
2101
|
+
},
|
|
1678
2102
|
children: []
|
|
1679
2103
|
},
|
|
1680
2104
|
fanout: {
|
|
@@ -1728,7 +2152,9 @@ const STAGES = [
|
|
|
1728
2152
|
"sink",
|
|
1729
2153
|
"queue",
|
|
1730
2154
|
"workers",
|
|
1731
|
-
"fanout"
|
|
2155
|
+
"fanout",
|
|
2156
|
+
"cache",
|
|
2157
|
+
"limit"
|
|
1732
2158
|
];
|
|
1733
2159
|
// ../dsl/src/expr.ts
|
|
1734
2160
|
/** A formula failed while the model ran: where, and why. */
|
|
@@ -1836,11 +2262,22 @@ const BUILTINS = {
|
|
|
1836
2262
|
min: 2,
|
|
1837
2263
|
result: "capacity",
|
|
1838
2264
|
fn: estimated
|
|
2265
|
+
},
|
|
2266
|
+
configured: {
|
|
2267
|
+
args: [
|
|
2268
|
+
"number",
|
|
2269
|
+
"string",
|
|
2270
|
+
"string"
|
|
2271
|
+
],
|
|
2272
|
+
min: 2,
|
|
2273
|
+
result: "capacity",
|
|
2274
|
+
fn: configured
|
|
1839
2275
|
}
|
|
1840
2276
|
};
|
|
1841
|
-
/** The tables a formula may read with a ".": flows.orders, demand.db, util.db, derived.x. */
|
|
2277
|
+
/** The tables a formula may read with a ".": flows.orders, at.db.orders, demand.db, util.db, derived.x. */
|
|
1842
2278
|
const TABLES = [
|
|
1843
2279
|
"flows",
|
|
2280
|
+
"at",
|
|
1844
2281
|
"demand",
|
|
1845
2282
|
"util",
|
|
1846
2283
|
"derived"
|
|
@@ -2005,14 +2442,15 @@ var Compiler$1 = class {
|
|
|
2005
2442
|
return bad;
|
|
2006
2443
|
}
|
|
2007
2444
|
example(table) {
|
|
2008
|
-
return [...table === "flows" ? this.names.flows : table === "derived" ? this.names.lets.keys() : this.names.components][0] ?? "x";
|
|
2445
|
+
return [...table === "flows" ? this.names.flows : table === "at" ? this.names.stages : table === "derived" ? this.names.lets.keys() : this.names.components][0] ?? "x";
|
|
2009
2446
|
}
|
|
2010
2447
|
member(e) {
|
|
2011
2448
|
const table = e.object.name;
|
|
2012
2449
|
const key = e.prop.name;
|
|
2450
|
+
if (table === "at") return this.at(e);
|
|
2013
2451
|
const pool = table === "flows" ? this.names.flows : table === "demand" || table === "util" ? this.names.components : table === "derived" ? this.names.lets.keys() : void 0;
|
|
2014
2452
|
if (!pool) {
|
|
2015
|
-
this.report.error(e.object, `"${table}" has no entries: only flows., demand., util. and derived. do`);
|
|
2453
|
+
this.report.error(e.object, `"${table}" has no entries: only flows., at., demand., util. and derived. do`);
|
|
2016
2454
|
return bad;
|
|
2017
2455
|
}
|
|
2018
2456
|
if (!(table === "derived" ? this.names.lets.has(key) : pool.has(key))) {
|
|
@@ -2027,6 +2465,26 @@ var Compiler$1 = class {
|
|
|
2027
2465
|
run: (c) => c[table][key]
|
|
2028
2466
|
};
|
|
2029
2467
|
}
|
|
2468
|
+
/** at.db: every flow reaching the stage, per second; at.db.orders: one flow. */
|
|
2469
|
+
at(e) {
|
|
2470
|
+
const stage = e.prop.name;
|
|
2471
|
+
if (!this.names.stages.has(stage)) {
|
|
2472
|
+
this.report.error(e.prop, `unknown stage "${stage}"`, didYouMean(stage, this.names.stages));
|
|
2473
|
+
return bad;
|
|
2474
|
+
}
|
|
2475
|
+
const flow = e.flow?.name;
|
|
2476
|
+
if (flow != null && !this.names.flows.has(flow)) {
|
|
2477
|
+
this.report.error(e.flow, `unknown flow "${flow}"`, didYouMean(flow, this.names.flows));
|
|
2478
|
+
return bad;
|
|
2479
|
+
}
|
|
2480
|
+
if (!this.canRead(e, flow == null ? `at.${stage}` : `at.${stage}.${flow}`)) return bad;
|
|
2481
|
+
const flows = [...this.names.flows];
|
|
2482
|
+
return {
|
|
2483
|
+
type: "number",
|
|
2484
|
+
constant: false,
|
|
2485
|
+
run: flow == null ? (c) => flows.reduce((a, f) => a + c.at[stage][f], 0) : (c) => c.at[stage][flow]
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2030
2488
|
call(e) {
|
|
2031
2489
|
const name = e.callee.name;
|
|
2032
2490
|
const pm = e.args[0]?.t === "pm" ? e.args[0] : null;
|
|
@@ -2771,6 +3229,19 @@ var Parser = class {
|
|
|
2771
3229
|
this.next();
|
|
2772
3230
|
const prop = this.ident("a name after \".\"");
|
|
2773
3231
|
if (!this.is("(")) {
|
|
3232
|
+
if (callee.name === "at" && this.is(".")) {
|
|
3233
|
+
this.next();
|
|
3234
|
+
const flow = this.ident("a flow after \"at.<stage>.\"");
|
|
3235
|
+
if (this.is(".")) this.fail(this.peek(), "at takes two names: at.db.orders");
|
|
3236
|
+
return {
|
|
3237
|
+
t: "member",
|
|
3238
|
+
object: callee,
|
|
3239
|
+
prop,
|
|
3240
|
+
flow,
|
|
3241
|
+
from: t.from,
|
|
3242
|
+
to: flow.to
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
2774
3245
|
if (this.is(".")) this.fail(this.peek(), "only one \".\" is allowed: flows.orders, util.db");
|
|
2775
3246
|
return {
|
|
2776
3247
|
t: "member",
|
|
@@ -2989,6 +3460,7 @@ var Compiler = class {
|
|
|
2989
3460
|
params: paramTypes,
|
|
2990
3461
|
lets: this.lets,
|
|
2991
3462
|
flows: new Set(flowNames.keys()),
|
|
3463
|
+
stages: new Set(stageNames.keys()),
|
|
2992
3464
|
components: new Set(componentNames.keys()),
|
|
2993
3465
|
fits: this.fits,
|
|
2994
3466
|
functions: this.functions
|
|
@@ -3074,6 +3546,8 @@ var Compiler = class {
|
|
|
3074
3546
|
if (priority != null) def.priority = priority;
|
|
3075
3547
|
const overload = this.overload(b);
|
|
3076
3548
|
if (overload) def.overload = overload;
|
|
3549
|
+
const retry = this.retry(b);
|
|
3550
|
+
if (retry) def.retry = retry;
|
|
3077
3551
|
flowDefs[id.name] = def;
|
|
3078
3552
|
const route = this.prop(b, "route");
|
|
3079
3553
|
const stages = [];
|
|
@@ -3109,6 +3583,8 @@ var Compiler = class {
|
|
|
3109
3583
|
if (def) stageDefs[s.id] = def;
|
|
3110
3584
|
}
|
|
3111
3585
|
for (const [id, s] of stageOf) if (!reached.has(id) && s.block.keyword !== "fanout") this.report.warning(s.block.args.id ?? s.block.at, `no flow passes the stage "${id}"`);
|
|
3586
|
+
const used = new Set(Object.values(stageDefs).map((d) => d.component));
|
|
3587
|
+
for (const [id, c] of Object.entries(componentDefs)) if (!c.demand && !used.has(id)) this.report.error(componentNames.get(id), `the component "${id}" needs "demand: …": no stage uses it`, COMPONENT_PROPS.demand.doc);
|
|
3112
3588
|
const calibration = {};
|
|
3113
3589
|
for (const f of fits) {
|
|
3114
3590
|
const fit = f.args.id && this.fits.get(f.args.id.name);
|
|
@@ -3706,13 +4182,16 @@ var Compiler = class {
|
|
|
3706
4182
|
if (!rules.length) return void 0;
|
|
3707
4183
|
const seen = /* @__PURE__ */ new Set();
|
|
3708
4184
|
const parts = [];
|
|
4185
|
+
const props = {
|
|
4186
|
+
drop: ["olderThan", "reason"],
|
|
4187
|
+
merge: ["share", "reason"]
|
|
4188
|
+
};
|
|
3709
4189
|
for (const r of rules) {
|
|
3710
4190
|
const key = r.keyword;
|
|
3711
4191
|
if (seen.has(key)) this.report.error(r.at, `"${key}" is set twice in this flow`);
|
|
3712
4192
|
seen.add(key);
|
|
3713
4193
|
const fields = [];
|
|
3714
|
-
const
|
|
3715
|
-
for (const k of [first, "reason"]) {
|
|
4194
|
+
for (const k of props[key]) {
|
|
3716
4195
|
const v = this.dynProp(r, k, k === "reason" ? "string" : "number");
|
|
3717
4196
|
if (v !== void 0) fields.push([k, v]);
|
|
3718
4197
|
}
|
|
@@ -3736,6 +4215,24 @@ var Compiler = class {
|
|
|
3736
4215
|
};
|
|
3737
4216
|
return fixed ? at(void 0) : at;
|
|
3738
4217
|
}
|
|
4218
|
+
/** A flow's retry rule: it reads only the load and the parameters, since retries change every other number. */
|
|
4219
|
+
retry(b) {
|
|
4220
|
+
const r = b.children.filter((c) => c.keyword === "retry");
|
|
4221
|
+
if (!r.length) return void 0;
|
|
4222
|
+
if (r.length > 1) this.report.error(r[1].at, "\"retry\" is set twice in this flow");
|
|
4223
|
+
const rule = r[0];
|
|
4224
|
+
const times = this.dynProp(rule, "times", "number");
|
|
4225
|
+
const budget = this.dynProp(rule, "budget", "number");
|
|
4226
|
+
const w = this.prop(rule, "when");
|
|
4227
|
+
const when = w && placed(w.value, this.formula(w.value, "values").run);
|
|
4228
|
+
const at = (v) => {
|
|
4229
|
+
if (when && !when(v)) return void 0;
|
|
4230
|
+
const out = { times: (typeof times === "function" ? times(v) : times) ?? 0 };
|
|
4231
|
+
if (budget !== void 0) out.budget = typeof budget === "function" ? budget(v) : budget;
|
|
4232
|
+
return out;
|
|
4233
|
+
};
|
|
4234
|
+
return !when && typeof times !== "function" && typeof budget !== "function" ? at(void 0) : at;
|
|
4235
|
+
}
|
|
3739
4236
|
component(b, box) {
|
|
3740
4237
|
const o = {};
|
|
3741
4238
|
const name = this.constProp(b, "name") ?? this.text(b.args.label) ?? this.text(box?.args.label);
|
|
@@ -3758,16 +4255,12 @@ var Compiler = class {
|
|
|
3758
4255
|
const prop = this.prop(b, k);
|
|
3759
4256
|
if (!prop) continue;
|
|
3760
4257
|
const check = k === "capacity" ? (v) => {
|
|
3761
|
-
if (!isCapacity(v)) throw new Error("a capacity comes from measured(…) or
|
|
4258
|
+
if (!isCapacity(v)) throw new Error("a capacity comes from measured(…), estimated(…) or configured(…)");
|
|
3762
4259
|
} : void 0;
|
|
3763
4260
|
o[k] = this.dyn(prop.value, want, "ctx", check);
|
|
3764
4261
|
}
|
|
3765
4262
|
const at = box ? box.args.id ?? box.at : b.at;
|
|
3766
|
-
for (const k of [
|
|
3767
|
-
"demand",
|
|
3768
|
-
"capacity",
|
|
3769
|
-
"over"
|
|
3770
|
-
]) if (!(k in o)) this.report.error(at, `the component needs "${k}: …"`, COMPONENT_PROPS[k].doc);
|
|
4263
|
+
for (const k of ["capacity", "over"]) if (!(k in o)) this.report.error(at, `the component needs "${k}: …"`, COMPONENT_PROPS[k].doc);
|
|
3771
4264
|
return o;
|
|
3772
4265
|
}
|
|
3773
4266
|
stage(b, box, components, resolve, flows, reached) {
|
|
@@ -3785,18 +4278,46 @@ var Compiler = class {
|
|
|
3785
4278
|
};
|
|
3786
4279
|
switch (b.keyword) {
|
|
3787
4280
|
case "pass": return component ? pass(component, base) : needs();
|
|
4281
|
+
case "limit": return component ? limit(component, base) : needs();
|
|
4282
|
+
case "cache": {
|
|
4283
|
+
const hit = this.dynProp(b, "hit", "number");
|
|
4284
|
+
return cache({
|
|
4285
|
+
...base,
|
|
4286
|
+
hit: hit ?? 0,
|
|
4287
|
+
...component ? { component } : {}
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
3788
4290
|
case "store": return component ? store(component, base) : needs();
|
|
3789
4291
|
case "sink": return sink(base);
|
|
3790
4292
|
case "workers": {
|
|
3791
4293
|
if (!component) return needs();
|
|
3792
|
-
const pods = this.dynProp(b, "pods", "number", this.count("pods"));
|
|
3793
4294
|
const perPod = this.dynProp(b, "perPod", "number", this.count("perPod"));
|
|
3794
4295
|
const fill = this.constProp(b, "fill");
|
|
3795
|
-
|
|
4296
|
+
const rest = {
|
|
3796
4297
|
...base,
|
|
3797
|
-
pods: pods ?? 0,
|
|
3798
4298
|
perPod: perPod ?? 0,
|
|
3799
4299
|
...fill ? { fill } : {}
|
|
4300
|
+
};
|
|
4301
|
+
const scale = b.children.find((c) => c.keyword === "autoscale");
|
|
4302
|
+
if (scale) {
|
|
4303
|
+
if (this.prop(b, "pods")) this.report.error(this.prop(b, "pods").key, "an autoscaled pool has no fixed pods", "leave out pods");
|
|
4304
|
+
const min = this.dynProp(scale, "min", "number") ?? 1;
|
|
4305
|
+
const max = this.dynProp(scale, "max", "number", this.count("pods")) ?? 1;
|
|
4306
|
+
const target = this.dynProp(scale, "target", "number") ?? 1;
|
|
4307
|
+
return workers(component, {
|
|
4308
|
+
...rest,
|
|
4309
|
+
autoscale: {
|
|
4310
|
+
min,
|
|
4311
|
+
max,
|
|
4312
|
+
target
|
|
4313
|
+
}
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
const pods = this.dynProp(b, "pods", "number", this.count("pods"));
|
|
4317
|
+
if (!this.prop(b, "pods")) this.report.error(b.at, "a workers stage needs pods, or autoscale", "pods: 3, or autoscale { min: 2; max: 10; target: 0.7 }");
|
|
4318
|
+
return workers(component, {
|
|
4319
|
+
...rest,
|
|
4320
|
+
pods: pods ?? 0
|
|
3800
4321
|
});
|
|
3801
4322
|
}
|
|
3802
4323
|
case "queue": {
|