@zakkster/lite-pick 0.2.0 → 0.3.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/CHANGELOG.md +37 -0
- package/Pick.d.ts +20 -2
- package/Pick.js +88 -3
- package/README.md +30 -21
- package/llms.txt +16 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to `@zakkster/lite-pick` are documented here. The format fol
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
M3: P2C (power-of-two-choices), the headline strategy -- and the balance-quality anchor
|
|
10
|
+
(ROADMAP.md M3).
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `P2cBalancer extends BalancerBase` -- power-of-two-choices. Draws two DISTINCT eligible
|
|
15
|
+
endpoints uniformly at random (rejection sampling over the shared bitmap -- no peer, no
|
|
16
|
+
owned draw-set) and returns the one with the lower in-flight load; ties to the first draw.
|
|
17
|
+
In-flight counts are the caller's `Uint32Array` (read-only to `pick()`). O(1) per pick,
|
|
18
|
+
0 B/op. Fails closed (`PICK_NONE`) when the whole pool is down. Owns only a seeded,
|
|
19
|
+
deterministic PRNG (reproducible benches).
|
|
20
|
+
- The distinct second choice uses a BOUNDED redraw (up to 32 tries), not a single nudge, so
|
|
21
|
+
the two-choices property holds even at tiny pool sizes (~2^-32 collision chance), while
|
|
22
|
+
staying expected-O(1) and 0 B/op.
|
|
23
|
+
- `test/P2C.test.js` -- 10 tests: n=2 always-lower-load, determinism by seed, fail-closed,
|
|
24
|
+
single-node, skips-down, a very-sparse-pool fallback path, a never-returns-a-down-index
|
|
25
|
+
proof under 200k churned picks, and an in-suite balance smoke.
|
|
26
|
+
- **The balance anchor** (`test/balance.mjs`): the balls-into-bins experiment now proves the
|
|
27
|
+
`ln ln n / ln 2` ceiling -- at n=1024, k=32 balls/bin, P2C peak-to-mean gap ~2 vs a random
|
|
28
|
+
single-draw foil's ~21, and P2C's gap stays ~2-3 as n grows to 4096 while random's grows.
|
|
29
|
+
- Gates extended for P2C: torture (retention + 0 B/op `pick()`), PerfGate (`zgcSuite` scenario
|
|
30
|
+
+ a `mustFail` teeth-check), witness (`const` complexity -> flat throughput), benchmark
|
|
31
|
+
matrix (P2C subject + a random-draw foil).
|
|
32
|
+
- `decisions/0005-p2c-draw.md` -- rejection sampling (no peer, RandomSet deferred), the
|
|
33
|
+
bounded-distinct-redraw enrichment, and caller-owned in-flight counters.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Version 0.2.0 -> 0.3.0 across `package.json`, `Pick.js` `VERSION`, and `llms.txt`.
|
|
38
|
+
- Folded lite-o1 v1.11.0's new members into the substrate map (RESEARCH s6, ROADMAP):
|
|
39
|
+
`Reservoir` -> the Subsetting substrate (post-1.0 #4); `EliasFano` -> a viable ring-with-
|
|
40
|
+
vnodes option for M8 ConsistentHash; `RankSelect` noted as static-only (not for the
|
|
41
|
+
mutating eligibility bitmap).
|
|
42
|
+
|
|
7
43
|
## [0.2.0] - 2026-09-23
|
|
8
44
|
|
|
9
45
|
M2: SmoothWRR, the weighted default (ROADMAP.md M2).
|
|
@@ -105,6 +141,7 @@ ownership boundary in place before any `pick()` is written.
|
|
|
105
141
|
- Next: **M1 RoundRobin** (0.1.0) -- the first strategy, landing the throughput witness
|
|
106
142
|
and the balance gate.
|
|
107
143
|
|
|
144
|
+
[0.3.0]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.3.0
|
|
108
145
|
[0.2.0]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.2.0
|
|
109
146
|
[0.1.0]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.1.0
|
|
110
147
|
[0.0.1]: https://github.com/PeshoVurtoleta/lite-pick/releases/tag/v0.0.1
|
package/Pick.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zakkster/lite-pick -- TypeScript declarations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* strategy classes (
|
|
4
|
+
* M3 (0.3.0): substrate seams + RoundRobin + SmoothWRR + P2C (the headline). The
|
|
5
|
+
* remaining strategy classes (LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad,
|
|
6
6
|
* WeightedRandom) are added one per session.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -87,3 +87,21 @@ export class SmoothWRRBalancer extends BalancerBase {
|
|
|
87
87
|
/** Next endpoint by smooth weighting, or `PICK_NONE` when the eligible-weight sum is 0. */
|
|
88
88
|
pick(): number;
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* P2cBalancer -- power-of-two-choices (M3), the headline strategy. Draws two distinct
|
|
93
|
+
* eligible endpoints at random and returns the one with the lower in-flight load; the
|
|
94
|
+
* `ln ln n / ln 2` peak-load ceiling. In-flight counts are the caller's Uint32Array
|
|
95
|
+
* (read-only to `pick()`). O(1) per pick, 0 B/op. Fails closed (`PICK_NONE`) when down.
|
|
96
|
+
*/
|
|
97
|
+
export class P2cBalancer extends BalancerBase {
|
|
98
|
+
/**
|
|
99
|
+
* @param capacity endpoint count (fixed).
|
|
100
|
+
* @param eligible shared view: 1 = pickable, 0 = down (length >= capacity).
|
|
101
|
+
* @param inflight per-endpoint in-flight counts (length >= capacity), caller-owned, read-only.
|
|
102
|
+
* @param seed deterministic PRNG seed (default 0x9e3779b9); reproducible benches.
|
|
103
|
+
*/
|
|
104
|
+
constructor(capacity: number, eligible: Uint8Array, inflight: Uint32Array, seed?: number);
|
|
105
|
+
/** Pick by power-of-two-choices (lower in-flight of two random eligibles), or `PICK_NONE`. */
|
|
106
|
+
pick(): number;
|
|
107
|
+
}
|
package/Pick.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zakkster/lite-pick -- zero-GC load-balancing SELECTION KERNEL.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* M3 (0.3.0): substrate seams + RoundRobin + SmoothWRR + P2C (the headline). This file ships:
|
|
5
5
|
*
|
|
6
6
|
* - VERSION the single source-of-truth version stamp (3-place sync).
|
|
7
7
|
* - PICK_NONE the fail-closed sentinel (-1): "no endpoint", never a dead pick.
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* the eligibility view, skipping down nodes, O(1) amortized, 0 B/op.
|
|
16
16
|
* - SmoothWRRBalancer the weighted default: nginx smooth weighted round-robin over
|
|
17
17
|
* caller-configured integer weights, O(cap)/pick, 0 B/op.
|
|
18
|
+
* - P2cBalancer the headline: power-of-two-choices over caller-owned in-flight counts;
|
|
19
|
+
* the ln ln n balance ceiling, O(1)/pick, 0 B/op.
|
|
18
20
|
*
|
|
19
21
|
* The identity (decisions/0001): lite-pick OWNS NO mutable state it can avoid owning.
|
|
20
22
|
* It reads pre-allocated views (eligibility, inflight, weights, scores) that siblings or
|
|
@@ -22,13 +24,13 @@
|
|
|
22
24
|
* counters live OUTSIDE the kernel. The steady-state pick path allocates 0 B/op.
|
|
23
25
|
*
|
|
24
26
|
* Roster (one strategy per session -- see ROADMAP.md): RoundRobin [M1], SmoothWRR [M2],
|
|
25
|
-
* P2C, LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom [planned].
|
|
27
|
+
* P2C [M3], LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom [planned].
|
|
26
28
|
*
|
|
27
29
|
* Zero runtime dependencies. node:test only. ESM, single file, tree-shakeable.
|
|
28
30
|
*/
|
|
29
31
|
|
|
30
32
|
/** Version stamp. Synced across package.json and llms.txt (three-place rule). */
|
|
31
|
-
export const VERSION = '0.
|
|
33
|
+
export const VERSION = '0.3.0';
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Fail-closed sentinel returned by pick() when no endpoint is eligible.
|
|
@@ -298,3 +300,86 @@ export class SmoothWRRBalancer extends BalancerBase {
|
|
|
298
300
|
return best;
|
|
299
301
|
}
|
|
300
302
|
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* P2cBalancer -- power-of-two-choices (M3), the headline strategy.
|
|
306
|
+
*
|
|
307
|
+
* `pick()` draws TWO distinct eligible endpoints uniformly at random and returns the one
|
|
308
|
+
* with the lower in-flight load. One extra probe over pure random buys an exponential drop
|
|
309
|
+
* in peak load: the max load stays within `ln ln n / ln 2 + O(1)` of the mean (Azar-Broder-
|
|
310
|
+
* Karlin-Upfal 1994), versus random's `ln n / ln ln n` gap. That additive `ln ln n` ceiling
|
|
311
|
+
* -- proven in test/balance.mjs against a random foil -- is the library's analytical anchor.
|
|
312
|
+
*
|
|
313
|
+
* Ownership (ADR 0001, ADR 0005): in-flight counts live in the CALLER's Uint32Array, read-
|
|
314
|
+
* only to `pick()` (the caller / the M5 lite-query adapter increments on dispatch, decrements
|
|
315
|
+
* on settle -- lite-pick holds no request state). The eligible draw is REJECTION SAMPLING
|
|
316
|
+
* over the shared bitmap: no peer, no owned draw-set, expected O(1) draws when eligibility is
|
|
317
|
+
* dense (the common case), a bounded retry + a zero-alloc rotated linear-scan fallback for the
|
|
318
|
+
* degenerate sparse case. A true worst-case-O(1) draw via lite-o1 `RandomSet` is a deferred
|
|
319
|
+
* optional-peer optimization (ADR 0005), added only if sparse-eligibility measurement demands.
|
|
320
|
+
*
|
|
321
|
+
* Bound: O(d) = O(1) with d = 2 (two expected-O(1) draws + one compare). Steady-state pick():
|
|
322
|
+
* a few PRNG steps + array reads, no object/closure/array created -- proven 0 B/op by
|
|
323
|
+
* test/torture.mjs and test/perf/PerfGate.test.mjs. Fails closed (PICK_NONE) when the whole
|
|
324
|
+
* pool is down.
|
|
325
|
+
*/
|
|
326
|
+
export class P2cBalancer extends BalancerBase {
|
|
327
|
+
/**
|
|
328
|
+
* @param {number} capacity endpoint count (fixed).
|
|
329
|
+
* @param {Uint8Array} eligible shared view: 1 = pickable, 0 = down (length >= capacity).
|
|
330
|
+
* @param {Uint32Array} inflight per-endpoint in-flight counts (length >= capacity),
|
|
331
|
+
* caller-owned and only READ here.
|
|
332
|
+
* @param {number} [seed=0x9e3779b9] deterministic PRNG seed (reproducible benches).
|
|
333
|
+
*/
|
|
334
|
+
constructor(capacity, eligible, inflight, seed = 0x9e3779b9) {
|
|
335
|
+
super(capacity, eligible);
|
|
336
|
+
if (!(inflight instanceof Uint32Array) || inflight.length < capacity) {
|
|
337
|
+
throw new RangeError('[lite-pick] inflight must be a Uint32Array of length >= capacity');
|
|
338
|
+
}
|
|
339
|
+
this._inflight = inflight;
|
|
340
|
+
this._rng = new Prng(seed);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A uniformly random ELIGIBLE index, or PICK_NONE if none. Expected O(1) (rejection
|
|
345
|
+
* sampling); a rotated linear scan from a random start is the zero-alloc fallback under
|
|
346
|
+
* degenerate sparsity (unbiased first-eligible-after-a-random-offset). Internal.
|
|
347
|
+
* @returns {number}
|
|
348
|
+
*/
|
|
349
|
+
_draw() {
|
|
350
|
+
if (this._live === 0) return PICK_NONE;
|
|
351
|
+
const cap = this._cap, el = this._eligible;
|
|
352
|
+
for (let tries = 0; tries < 64; tries++) {
|
|
353
|
+
const i = this._rng.nextBelow(cap);
|
|
354
|
+
if (el[i]) return i;
|
|
355
|
+
}
|
|
356
|
+
// Degenerate (very sparse eligibility): scan from a random start, wrapping, and
|
|
357
|
+
// return the first eligible found. Zero-alloc; live > 0 guarantees a hit.
|
|
358
|
+
let i = this._rng.nextBelow(cap);
|
|
359
|
+
for (let k = 0; k < cap; k++) {
|
|
360
|
+
if (el[i]) return i;
|
|
361
|
+
i++;
|
|
362
|
+
if (i >= cap) i = 0;
|
|
363
|
+
}
|
|
364
|
+
return PICK_NONE;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Pick an endpoint by power-of-two-choices, or PICK_NONE (fail closed). O(1).
|
|
369
|
+
* @returns {number}
|
|
370
|
+
*/
|
|
371
|
+
pick() {
|
|
372
|
+
const a = this._draw();
|
|
373
|
+
if (a < 0) return PICK_NONE; // whole pool down: fail closed
|
|
374
|
+
if (this._live === 1) return a; // only one eligible: it is both choices
|
|
375
|
+
// Draw a DISTINCT second choice. A bounded redraw (not a single nudge) keeps the
|
|
376
|
+
// two-choices property intact even at tiny pool sizes, where a single retry collides
|
|
377
|
+
// often: at live>=2 each redraw misses with probability <= 1/2, so 32 tries leaves a
|
|
378
|
+
// ~2^-32 collision chance -- while staying expected-O(1) (about two draws) and 0 B/op.
|
|
379
|
+
let b = this._draw();
|
|
380
|
+
for (let t = 0; b === a && t < 32; t++) b = this._draw();
|
|
381
|
+
if (b < 0 || b === a) return a; // astronomically rare: fall back to the first draw
|
|
382
|
+
// Lower in-flight wins; ties go to the first draw (unbiased over many picks).
|
|
383
|
+
return this._inflight[b] < this._inflight[a] ? b : a;
|
|
384
|
+
}
|
|
385
|
+
}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-pick
|
|
2
2
|
|
|
3
|
-
> Zero-GC load-balancing **selection kernel**: one hot `pick()` that returns an endpoint **index** over a fixed pool and allocates **0 B/op** on the steady-state path. A pure selector, never a proxy -- it consumes health and circuit state, it never owns them. **v0.
|
|
3
|
+
> Zero-GC load-balancing **selection kernel**: one hot `pick()` that returns an endpoint **index** over a fixed pool and allocates **0 B/op** on the steady-state path. A pure selector, never a proxy -- it consumes health and circuit state, it never owns them. **v0.3.0 ships three strategies -- `RoundRobinBalancer`, `SmoothWRRBalancer`, and `P2cBalancer`** (power-of-two-choices, the headline) -- on the substrate seams (`VERSION`, `PICK_NONE`, a deterministic `Prng`, and `BalancerBase`'s shared read-only eligibility view). The rest of the roster -- LeastConn/SED/NQ, PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom -- lands one per session.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@zakkster/lite-pick)
|
|
6
6
|
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
@@ -21,7 +21,7 @@ The npm landscape has old algorithm libraries (`load-balancers`, `loadbalance`,
|
|
|
21
21
|
- **Two pieces of evidence, both shipped.** A **0 B/op** witness on the pick path (no object, closure, string, or array created per pick), and a measured **balance-quality anchor** -- peak-to-average load within the strategy's theoretical ceiling (for P2C, the Azar-Broder-Karlin-Upfal `ln ln n / ln 2` bound) and strictly better than a random foil.
|
|
22
22
|
- **A pure selector, not a proxy.** It **consumes** health and circuit state; it never owns them. Health is a shared read-only bitmap written by [`@zakkster/lite-di-health`](https://www.npmjs.com/package/@zakkster/lite-di-health); circuit state comes from [`@zakkster/lite-statechart`](https://www.npmjs.com/package/@zakkster/lite-statechart); load counters are caller-owned typed arrays. `pick()` only reads.
|
|
23
23
|
|
|
24
|
-
> **Status:
|
|
24
|
+
> **Status: M3 (v0.3.0).** Ships the substrate seams **plus `RoundRobinBalancer`, `SmoothWRRBalancer`, and `P2cBalancer`**. Every strategy is gated: `pick()` proven **0 B/op** (torture + PerfGate), RoundRobin **perfectly fair** with **zero dead picks** vs the naive `i++ % n` foil, SmoothWRR **exactly weighted** and **smooth**, and **P2C proves the `ln ln n` balance ceiling** -- peak-to-mean gap ~2 vs a random foil's ~21 at n=1024, holding flat as the pool grows. See [ROADMAP.md](./ROADMAP.md) for the M3 -> M10 path to 1.0.0, and [decisions/](./decisions) for the ownership boundary (ADR 0001), anti-flapping (ADR 0002), and the RoundRobin (0003), SmoothWRR (0004), and P2C (0005) design forks.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
npm install @zakkster/lite-pick
|
|
@@ -76,6 +76,32 @@ wrr.setWeight(1, 4); // B is now 4x
|
|
|
76
76
|
|
|
77
77
|
`pick()` is **0 B/op** and **O(cap)** (one scan of the pool -- negligible at real endpoint counts). It owns its smoothing accumulators; weights live in your `Uint32Array` but you mutate them only through `setWeight`, which keeps the internal eligible-weight total exact. Marking a node down/up resets its accumulator, so a recovered node rejoins neutral -- no stale burst or starvation ([ADR 0004](./decisions/0004-smoothwrr-weight-ownership.md)).
|
|
78
78
|
|
|
79
|
+
## P2C -- power-of-two-choices (v0.3.0, the headline)
|
|
80
|
+
|
|
81
|
+
Two random eligible draws, take the one with lower in-flight load. That single extra probe buys an **exponential** drop in peak load -- the max stays within an *additive* `ln ln n / ln 2` of the mean, versus random's `ln n / ln ln n` gap.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { P2cBalancer } from '@zakkster/lite-pick';
|
|
85
|
+
|
|
86
|
+
const eligible = Uint8Array.from([1, 1, 1, 1]);
|
|
87
|
+
const inflight = new Uint32Array(4); // YOU own this; pick() only reads it
|
|
88
|
+
|
|
89
|
+
const p2c = new P2cBalancer(4, eligible, inflight);
|
|
90
|
+
|
|
91
|
+
const i = p2c.pick(); // the lower-loaded of two random eligibles
|
|
92
|
+
inflight[i]++; // you increment on dispatch...
|
|
93
|
+
// ...and inflight[i]-- when the request settles (the M5 lite-query adapter will do this)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The proof (from `test/balance.mjs`, the library's analytical anchor):
|
|
97
|
+
|
|
98
|
+
| pool `n` | P2C peak/avg | random foil peak/avg |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| 1024 | **1.06** (gap 2) | 1.66 (gap 21) |
|
|
101
|
+
| 4096 | **1.06** (gap 2) | 1.78 (gap 25) |
|
|
102
|
+
|
|
103
|
+
P2C's gap stays a small `ln ln n` constant while the random foil's grows with the pool. `pick()` is **0 B/op** and **O(1)** (two expected-O(1) rejection draws + a compare); in-flight counts are your caller-owned `Uint32Array` ([ADR 0005](./decisions/0005-p2c-draw.md)).
|
|
104
|
+
|
|
79
105
|
## The substrate (under every strategy)
|
|
80
106
|
|
|
81
107
|
```js
|
|
@@ -101,27 +127,10 @@ rng.nextBelow(4); // -> a uint32 in [0, 4)
|
|
|
101
127
|
rng.reset(); // replays the exact stream
|
|
102
128
|
|
|
103
129
|
PICK_NONE; // -> -1 (fail-closed sentinel: no endpoint, never a dead pick)
|
|
104
|
-
VERSION; // -> '0.
|
|
130
|
+
VERSION; // -> '0.3.0'
|
|
105
131
|
```
|
|
106
132
|
|
|
107
|
-
`BalancerBase.pick()` is **abstract** -- it throws, so an unfinished strategy fails loudly rather than returning a dead index
|
|
108
|
-
|
|
109
|
-
```js
|
|
110
|
-
// SHAPE ONLY -- not shipped until M3. Two random eligible draws, return the lower
|
|
111
|
-
// in-flight load. One extra probe over random buys the ln ln n balance ceiling.
|
|
112
|
-
class P2cBalancer extends BalancerBase {
|
|
113
|
-
constructor(capacity, eligible, inflight, seed) {
|
|
114
|
-
super(capacity, eligible);
|
|
115
|
-
this._inflight = inflight; // caller-owned Uint32Array; pick() only reads it
|
|
116
|
-
this._rng = new Prng(seed);
|
|
117
|
-
}
|
|
118
|
-
pick() {
|
|
119
|
-
if (this.live === 0) return PICK_NONE; // fail closed
|
|
120
|
-
const a = this._draw(), b = this._draw(); // two distinct eligible draws
|
|
121
|
-
return this._inflight[b] < this._inflight[a] ? b : a;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
133
|
+
`BalancerBase.pick()` is **abstract** -- it throws, so an unfinished strategy fails loudly rather than returning a dead index. Every shipped strategy (`RoundRobinBalancer`, `SmoothWRRBalancer`, `P2cBalancer`) extends it and reads the same shared eligibility view; you subclass it the same way to add your own.
|
|
125
134
|
|
|
126
135
|
## Design ownership (ratified before any strategy)
|
|
127
136
|
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-pick
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
5
5
|
Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
|
|
6
6
|
Node: >= 18.
|
|
@@ -14,11 +14,12 @@ reading pre-allocated views that siblings or the caller write, and returning an
|
|
|
14
14
|
The complementary evidence lite-pick ships is a measured balance-quality anchor (peak-to-
|
|
15
15
|
average load vs the strategy's theoretical ceiling) alongside the 0 B/op pick witness.
|
|
16
16
|
|
|
17
|
-
0.
|
|
18
|
-
default). It exports `VERSION`, the fail-closed
|
|
19
|
-
`Prng` (xorshift32), `BalancerBase` (the shared
|
|
20
|
-
|
|
21
|
-
(see ROADMAP.md):
|
|
17
|
+
0.3.0 ships the substrate seams + three strategies: RoundRobin, SmoothWRR (the weighted
|
|
18
|
+
default), and P2C (power-of-two-choices, the headline). It exports `VERSION`, the fail-closed
|
|
19
|
+
sentinel `PICK_NONE` (-1), a deterministic `Prng` (xorshift32), `BalancerBase` (the shared
|
|
20
|
+
read-only eligibility seam + O(1) live count), `RoundRobinBalancer`, `SmoothWRRBalancer`, and
|
|
21
|
+
`P2cBalancer`. The remaining strategies land one per session (see ROADMAP.md): LeastConn/SED/NQ,
|
|
22
|
+
PeakEWMA, ConsistentHash, BoundedLoad, WeightedRandom.
|
|
22
23
|
|
|
23
24
|
## Design ownership (decisions/0001, 0002)
|
|
24
25
|
|
|
@@ -69,6 +70,15 @@ default). It exports `VERSION`, the fail-closed sentinel `PICK_NONE` (-1), a det
|
|
|
69
70
|
weight; pick max; current -= total`), interleaved smoothly (weights [5,1,1] ->
|
|
70
71
|
A,A,B,A,C,A,A), or `PICK_NONE` when the eligible-weight sum is 0. O(cap) per pick, 0 B/op.
|
|
71
72
|
Owns its Float64Array smoothing accumulators (ADR 0004).
|
|
73
|
+
- `P2cBalancer extends BalancerBase` -- class. The headline strategy (M3): power-of-two-choices.
|
|
74
|
+
- `new P2cBalancer(capacity, eligible, inflight, seed?=0x9e3779b9)` -- `inflight` is a
|
|
75
|
+
caller-owned Uint32Array (length >= capacity), read-only to pick(): the caller / the M5
|
|
76
|
+
lite-query adapter increments on dispatch, decrements on settle. `seed` seeds the internal
|
|
77
|
+
deterministic PRNG (reproducible benches).
|
|
78
|
+
- `pick()` -> number. Draws two DISTINCT eligible endpoints uniformly at random (rejection
|
|
79
|
+
sampling over the bitmap, no peer) and returns the one with the lower in-flight load; ties
|
|
80
|
+
to the first draw. The `ln ln n / ln 2` peak-load ceiling (ADR 0005). O(1) per pick, 0 B/op.
|
|
81
|
+
`PICK_NONE` when the whole pool is down.
|
|
72
82
|
|
|
73
83
|
## Gates (every session)
|
|
74
84
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-pick",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Zero-dependency, zero-GC load-balancing selection kernel: one hot pick() -> endpoint index over a fixed pool, 0 B/op steady-state. A pure selector (consumes health/circuit state, never a proxy) for the in-process hop, complementary to AWS NLB/ALB. Tree-shakeable ESM roster: RoundRobin, SmoothWRR, P2C, least-conn, PeakEWMA, consistent hashing.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./Pick.js",
|