This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { pickIdlerIndex, type ClaimCandidate } from './idle-routing.js';
|
||||
|
||||
const c = (freeSlots: number, opts: Partial<ClaimCandidate> = {}): ClaimCandidate => ({
|
||||
freeSlots,
|
||||
availableForClaim: opts.availableForClaim ?? true,
|
||||
servesRole: opts.servesRole ?? true,
|
||||
});
|
||||
|
||||
describe('pickIdlerIndex (most-free-wins)', () => {
|
||||
it('returns -1 when there are no siblings', () => {
|
||||
expect(pickIdlerIndex(0, [])).toBe(-1);
|
||||
});
|
||||
|
||||
it('yields to a strictly-idler sibling (0/n beats a loaded worker)', () => {
|
||||
// self has 0 free (fully loaded); sibling has 4 free.
|
||||
expect(pickIdlerIndex(0, [c(4)])).toBe(0);
|
||||
});
|
||||
|
||||
it('does not yield on a tie (claims itself)', () => {
|
||||
expect(pickIdlerIndex(2, [c(2), c(2)])).toBe(-1);
|
||||
});
|
||||
|
||||
it('does not yield when the caller is the most free', () => {
|
||||
expect(pickIdlerIndex(4, [c(1), c(3)])).toBe(-1);
|
||||
});
|
||||
|
||||
it('picks the idlest among several stricter competitors', () => {
|
||||
expect(pickIdlerIndex(1, [c(2), c(5), c(3)])).toBe(1); // 5 free wins
|
||||
});
|
||||
|
||||
it('ignores siblings that do not serve the role', () => {
|
||||
expect(pickIdlerIndex(0, [c(8, { servesRole: false })])).toBe(-1);
|
||||
});
|
||||
|
||||
it('ignores unavailable (unhealthy/stopped) siblings', () => {
|
||||
expect(pickIdlerIndex(0, [c(8, { availableForClaim: false })])).toBe(-1);
|
||||
});
|
||||
|
||||
it('skips a non-serving idler but still yields to a serving one', () => {
|
||||
// idx0: very idle but wrong role; idx1: idle and serves → pick idx1.
|
||||
expect(pickIdlerIndex(0, [c(9, { servesRole: false }), c(3)])).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Idle-preferring worker selection helper.
|
||||
*
|
||||
* Workers run as in-process instances that each poll the DB for jobs. Without
|
||||
* coordination, whichever worker's poll timer fires first claims the next job —
|
||||
* so a busy worker can grab work while an idle sibling sits at 0/n. This helper
|
||||
* implements the "most-free-wins" rule: before claiming, a worker checks its
|
||||
* siblings and yields to one that has STRICTLY more free slots (scoped to
|
||||
* siblings that are available and actually serve the job's role). A 0/n idle
|
||||
* worker therefore always beats a partially-loaded one.
|
||||
*
|
||||
* Returns the index of the idlest qualifying competitor, or -1 when the caller
|
||||
* is already (tied for) the most free and should claim the job itself.
|
||||
*/
|
||||
export interface ClaimCandidate {
|
||||
/** max_concurrency − inflight for this candidate. */
|
||||
freeSlots: number;
|
||||
/** Running, healthy, enabled — would actually claim if poked. */
|
||||
availableForClaim: boolean;
|
||||
/** Serves the role of the job about to be claimed. */
|
||||
servesRole: boolean;
|
||||
}
|
||||
|
||||
export function pickIdlerIndex(
|
||||
selfFreeSlots: number,
|
||||
candidates: readonly ClaimCandidate[],
|
||||
): number {
|
||||
let bestIdx = -1;
|
||||
let bestFree = selfFreeSlots; // a competitor must STRICTLY exceed this to win
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
const c = candidates[i];
|
||||
if (!c.availableForClaim || !c.servesRole) continue;
|
||||
if (c.freeSlots > bestFree) {
|
||||
bestIdx = i;
|
||||
bestFree = c.freeSlots;
|
||||
}
|
||||
}
|
||||
return bestIdx;
|
||||
}
|
||||
Reference in New Issue
Block a user