Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sim/behaviour-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ function regionHealthy(ctx: BehaviourCtx, state: NodeStateLike, i: number): bool
*/
const region: ComponentBehaviour = {
kind: 'region',
// A pure switch, like an lb: it forwards without holding slots of its own.
// A pure switch: it forwards without holding slots of its own.
servesRequests: true,
generatesLoad: false,
pullsFromQueues: false,
Expand Down
6 changes: 5 additions & 1 deletion src/sim/behaviour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ const client: ComponentBehaviour = {
/**
* A dispatcher. Picks exactly one downstream per request: weighted-random when
* the edge weights differ, least-loaded when they are all equal.
*
* Its pool is real. There is no onAdmit, so the engine's default `serve` puts
* `capacity`, `instances` and `queueLimit` through ordinary slot and queue
* discipline. `passthru` skipped that, which made all three knobs inert and
* left an autoscaler writing `instances` with nothing to turn (#52).
*/
const lb: ComponentBehaviour = {
kind: 'lb',
Expand All @@ -298,7 +303,6 @@ const lb: ComponentBehaviour = {
buffersForConsumers: false,
pump: 'own',
creditsJoinCompletion: true,
onAdmit: () => 'passthru',
route: () => 'one',
pickEdge: (ctx, _state, _req, out) => ctx.pickWeightedOrLeastLoaded(out),
};
Expand Down
74 changes: 74 additions & 0 deletions src/sim/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,80 @@ describe('load response', () => {
});
});

/**
* A load balancer's pool used to be theatre: onAdmit returned passthru, so
* capacity, instances and queueLimit never met an admission check (#52).
*
* The ceiling is AGENTS.md's sizing rule, capacity * instances * (1000 /
* serviceMs). The service behind the lb is deliberately far above that, so
* if throughput still ignores the pool, the lb is the thing that is lying.
*/
describe('load balancer pool', () => {
function topology(lb: {
capacity: number;
instances: number;
queueLimit: number;
serviceMs: number;
}): Topology {
const client = makeNode('client', 0, 0);
client.id = 'client';
client.config = { ...defaultConfig('client'), rps: 3000, timeoutMs: 10_000 };

const balancer = makeNode('lb', 260, 0);
balancer.id = 'lb';
balancer.config = { ...defaultConfig('lb'), ...lb, serviceCv: 0 };

const service = makeNode('service', 520, 0);
service.id = 'service';
service.config = {
...defaultConfig('service'),
capacity: 64,
instances: 8,
serviceMs: 8,
queueLimit: 10_000,
};

return {
nodes: [client, balancer, service],
edges: [
{ id: 'e1', from: 'client', to: 'lb', weight: 1 },
{ id: 'e2', from: 'lb', to: 'service', weight: 1 },
],
};
}

it('caps throughput at the pool ceiling instead of passing every request', () => {
const snapshot = run(
topology({ capacity: 2, instances: 1, queueLimit: 4, serviceMs: 5 }),
30,
);
const ceiling = 2 * 1 * (1000 / 5);
expect(snapshot.nodes.lb.throughput).toBeLessThanOrEqual(ceiling * 1.35);
expect(snapshot.system.offeredRps).toBeGreaterThan(ceiling * 2);
expect(snapshot.nodes.lb.shedRate).toBeGreaterThan(0);
});

it('lets instances raise that ceiling', () => {
const tight = run(
topology({ capacity: 2, instances: 1, queueLimit: 4, serviceMs: 5 }),
30,
);
const wide = run(
topology({ capacity: 2, instances: 12, queueLimit: 4, serviceMs: 5 }),
30,
);
expect(wide.nodes.lb.throughput).toBeGreaterThan(tight.nodes.lb.throughput * 2);
});

it('queues when the pool is full, instead of keeping waiting at zero', () => {
const snapshot = run(
topology({ capacity: 2, instances: 1, queueLimit: 4096, serviceMs: 5 }),
10,
);
expect(snapshot.nodes.lb.queued).toBeGreaterThan(0);
});
});

describe('hostile topologies', () => {
const cases: Array<[string, Topology]> = [
['empty', { nodes: [], edges: [] }],
Expand Down
2 changes: 1 addition & 1 deletion src/sim/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1715,7 +1715,7 @@ export class Engine implements BehaviourCtx {
}
}

/** lb / client style pass-through with a tiny (possibly zero) service time. */
/** Client-style pass-through with a tiny (possibly zero) service time. */
private beginZeroService(state: NodeState, req: Req): void {
const ms = this.serviceTimeFor(state);
req.ownMs = ms;
Expand Down
Loading