Skip to content
Closed
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
29 changes: 29 additions & 0 deletions packages/loopover-engine/src/tenant-quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,32 @@ export function evaluateTenantQuota(usage: TenantUsage, quota: TenantQuota): Ten
remaining,
};
}

/**
* #7662: the soft-warning counterpart to {@link evaluateTenantQuota}. Returns the FIRST dimension — same
* compute→time→concurrency precedence as the hard check — whose remaining headroom has fallen to or below
* `warnThreshold` of its cap (default 0.2 ⇒ ≤20% left / ≥80% consumed), the pre-exhaustion band a soft
* "running low" notification fires in — or null when every bounded dimension still has comfortable headroom.
* A dimension already exhausted (remaining 0) is owned by the hard check and is NOT warned on here; a
* zero/undefined cap yields no meaningful fraction and is skipped. Pure: the caller decides whether/when to
* actually fire a notification from the result (the delivery wiring rides #7647's admission-check point).
*/
export function evaluateTenantQuotaWarning(
usage: TenantUsage,
quota: TenantQuota,
warnThreshold = 0.2,
): { warned: boolean; dimension: QuotaDimension | null; remaining: TenantQuotaDecision["remaining"] } {
const { remaining } = evaluateTenantQuota(usage, quota);
const byDimension: ReadonlyArray<[QuotaDimension, number, number]> = [
["compute", remaining.computeUnits, finiteNonNegativeInt(quota.computeUnits)],
["time", remaining.wallClockMs, finiteNonNegativeInt(quota.wallClockMs)],
["concurrency", remaining.concurrentLoops, finiteNonNegativeInt(quota.maxConcurrentLoops)],
];
for (const [dimension, left, cap] of byDimension) {
if (cap <= 0) continue;
if (left > 0 && left / cap <= warnThreshold) {
return { warned: true, dimension, remaining };
}
}
return { warned: false, dimension: null, remaining };
}
39 changes: 38 additions & 1 deletion test/unit/tenant-quota.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";

import { evaluateTenantQuota } from "../../packages/loopover-engine/src/tenant-quota";
import { evaluateTenantQuota, evaluateTenantQuotaWarning } from "../../packages/loopover-engine/src/tenant-quota";

const QUOTA = { computeUnits: 100, wallClockMs: 60_000, maxConcurrentLoops: 3 };

Expand Down Expand Up @@ -72,3 +72,40 @@ describe("evaluateTenantQuota (#4796)", () => {
expect(evaluateTenantQuota({ computeUnitsUsed: 5, wallClockMsUsed: 5_000, activeLoops: 1 }, QUOTA)).toEqual(under);
});
});

describe("evaluateTenantQuotaWarning (#7662)", () => {
it("does not warn when every dimension has comfortable headroom", () => {
const w = evaluateTenantQuotaWarning({ computeUnitsUsed: 40, wallClockMsUsed: 10_000, activeLoops: 1 }, QUOTA);
expect(w).toEqual({ warned: false, dimension: null, remaining: { computeUnits: 60, wallClockMs: 50_000, concurrentLoops: 2 } });
});

it("warns on the first low dimension at/below the soft threshold (85/100 compute ⇒ 15% left ≤ 20%)", () => {
const w = evaluateTenantQuotaWarning({ computeUnitsUsed: 85, wallClockMsUsed: 0, activeLoops: 0 }, QUOTA);
expect(w.warned).toBe(true);
expect(w.dimension).toBe("compute");
});

it("reports the time dimension when compute is healthy but wall-clock is low (precedence respected)", () => {
const w = evaluateTenantQuotaWarning({ computeUnitsUsed: 10, wallClockMsUsed: 50_000, activeLoops: 0 }, QUOTA);
expect(w.dimension).toBe("time");
});

it("does not warn a dimension already exhausted — the hard check owns remaining 0", () => {
const w = evaluateTenantQuotaWarning({ computeUnitsUsed: 100, wallClockMsUsed: 0, activeLoops: 0 }, QUOTA);
expect(w.warned).toBe(false);
expect(w.dimension).toBeNull();
});

it("skips a dimension whose cap is zero/undefined (no meaningful fraction)", () => {
const w = evaluateTenantQuotaWarning(
{ computeUnitsUsed: 0, wallClockMsUsed: 1_000, activeLoops: 0 },
{ computeUnits: 0, wallClockMs: 60_000, maxConcurrentLoops: 3 },
);
expect(w.warned).toBe(false);
});

it("honors a custom warn threshold (30% left: no warn at the 20% default, warns at 40%)", () => {
expect(evaluateTenantQuotaWarning({ computeUnitsUsed: 70, wallClockMsUsed: 0, activeLoops: 0 }, QUOTA).warned).toBe(false);
expect(evaluateTenantQuotaWarning({ computeUnitsUsed: 70, wallClockMsUsed: 0, activeLoops: 0 }, QUOTA, 0.4).dimension).toBe("compute");
});
});