fix: metering (#3591)

This commit is contained in:
Daniel Salazar
2026-08-16 14:19:05 -07:00
committed by GitHub
parent d22b656df8
commit 87180b1081
8 changed files with 166 additions and 58 deletions
@@ -1432,6 +1432,33 @@ describe('MeteringService', () => {
expect(allowed.remaining).toBe(0);
});
it('folds the legacy baseline in exactly once under concurrent increments', async () => {
const sub = await target.getActorSubscription(actor);
const month = `${new Date().getUTCFullYear()}-${String(new Date().getUTCMonth() + 1).padStart(2, '0')}`;
// Legacy spend with no split recorded — one page load then fires
// many metered requests at once, all seeing the field absent.
await server.stores.meteringBuffer.incr({
key: `${METRICS_PREFIX}:actor:${actor.user.uuid}:${month}`,
pathAndAmountMap: { total: 10_000 },
});
await Promise.all(
Array.from({ length: 8 }, () =>
target.incrementUsage(actor, 'kv:read', 1, 1_000),
),
);
const { usage } =
await target.getActorCurrentMonthUsageDetails(actor);
expect(usage.total).toBe(18_000);
// Without the claim, every concurrent settle re-adds the ~10_000
// baseline and the month reads as nearly exhausted.
expect(usage.allowanceUsed).toBe(18_000);
const allowed = await target.getAllowedUsage(actor);
expect(allowed.remaining).toBe(sub.monthUsageAllowance - 18_000);
});
it('counts consumed credits from prior months against the credit pool only', async () => {
// Simulate a prior-month overage: consumed credits exist but the
// current month has no usage (monthly usage keys roll over).
@@ -1746,7 +1746,10 @@ export class MeteringService extends PuterService {
* A month record without `allowanceUsed` predates the split; its first
* settled increment folds the fallback baseline into the write, so the
* record answers directly from then on and no balance moves on the deploy
* that introduced the field.
* that introduced the field. Writing the baseline is guarded by a claim
* counter (the `monthlyChargesApplied` pattern): concurrent increments
* one page load meters many responses at once all see the field absent,
* and without the claim each would add the baseline again.
*
* Returns the month's allowance-charged spend as of after this settle
* `usageRecord` itself predates the write, so callers deciding on the
@@ -1776,8 +1779,19 @@ export class MeteringService extends PuterService {
const usedBefore =
usageRecord.allowanceUsed ??
Math.min(totalBefore, Math.max(0, monthUsageAllowance || 0));
const baseline =
usageRecord.allowanceUsed === undefined ? usedBefore : 0;
let baseline = 0;
if (usageRecord.allowanceUsed === undefined && usedBefore > 0) {
const { res } = await this.stores.meteringBuffer.incr({
key: actorUsageKey,
pathAndAmountMap: { allowanceUsedBaselined: 1 },
});
const claim = (res as unknown as UsageByType)
.allowanceUsedBaselined;
if (claim === 1) {
baseline = usedBefore;
}
}
const headroom = Math.max(0, monthUsageAllowance - usedBefore);
const allowanceCharge = Math.min(incrementCost, headroom);
+7
View File
@@ -51,6 +51,13 @@ export type UsageByType = {
* against the allowance, capped at the allowance.
*/
allowanceUsed?: number;
/**
* Claim counter for folding the pre-split baseline into `allowanceUsed` on
* a record that predates it. 1 for the settle that claimed the fold; higher
* for concurrent settles that raced and lost and must not add the baseline
* again.
*/
allowanceUsedBaselined?: number;
/**
* Claim counter for the month's recurring charges see
* `MONTHLY_CHARGE_CLAIM`. Absent until the first read or write of the
+1 -4
View File
@@ -544,10 +544,7 @@ const TabHome = {
// Load monthly usage data
try {
const res = await puter.auth.getMonthlyUsage();
const budget = usageBudget(
res.usage?.total ?? 0,
res.allowanceInfo?.remaining ?? 0,
);
const budget = usageBudget(res.usage, res.allowanceInfo);
// The server reports credits (already scaled) or raw amounts
// (no multiplier configured), and says which via the unit flag.
const inCredits = usageIsCredits(res.allowanceInfo);
+1 -1
View File
@@ -279,7 +279,7 @@ function renderUsageTable () {
async function update_usage_details ($el_window) {
const monthlyUsagePromise = puter.auth.getMonthlyUsage().then(res => {
const budget = usageBudget(res.usage?.total ?? 0, res.allowanceInfo?.remaining ?? 0);
const budget = usageBudget(res.usage, res.allowanceInfo);
// The server reports credits (already scaled) or raw amounts (no
// multiplier configured), and says which via the unit flag.
const inCredits = usageIsCredits(res.allowanceInfo);
+2
View File
@@ -33,12 +33,14 @@ export const usageIsCredits = (allowanceInfo) =>
* Credits as the raw number users see: whole credits once the amount has any
* size, decimals only while fractions are all there is to show. Never renders
* a nonzero amount as "0" a cost that exists shows as at least "<0.01".
* Negative amounts (net usage with unspent top-up) keep their sign.
*
* @param {number} credits
* @returns {string}
*/
export const formatCredits = (credits) => {
const value = Number.isFinite(credits) ? credits : 0;
if (value < 0) return `-${formatCredits(-value)}`;
if (value <= 0) return '0';
if (value >= 100) {
return Math.round(value).toLocaleString('en-US');
+37 -24
View File
@@ -19,40 +19,53 @@
/**
* @typedef {Object} UsageBudget
* @property {number} used - Month-to-date spend, in the server's units.
* @property {number} capacity - The whole budget: spend plus what is left.
* @property {number} percent - `used` as a whole-number share of `capacity`, 0-100.
* @property {number} barPercent - The same share unrounded, for a bar width.
* @property {number} used - Allowance-charged spend net of unspent top-up, in
* the server's units. Negative when top-up credit exceeds the spend.
* @property {number} capacity - The monthly plan allowance.
* @property {number} percent - `used` as a whole-number share of `capacity`.
* Negative when `used` is.
* @property {number} barPercent - The share clamped to 0-100, for a bar width.
*/
/**
* The three numbers the usage cards show, from the two the server reports.
* The numbers the usage cards show, anchored to the monthly plan.
*
* `remaining` is already netted: what is left of the monthly allowance plus
* what is left of any purchased credit, with overage past the allowance
* charged to the credit. So spend plus remaining IS the budget, and deriving
* capacity that way can't contradict either input "$3.00 of $12.00, 25%"
* always adds up, whatever the mix.
* Capacity is the plan's monthly allowance, always the bar answers "how much
* of my plan have I used", so its denominator must not move with purchases.
* `used` is the month's allowance-charged spend (`usage.allowanceUsed`; records
* from before the split was tracked fall back to the month total, capped at the
* allowance), minus whatever top-up credit is still unspent. Unspent credit
* therefore reads as headroom spend a little with a large credit balance and
* the share is negative rather than inflating the plan's capacity.
*
* The share is taken against that whole budget rather than the monthly
* allowance alone. Measured against the allowance, held credit subtracted from
* spend, so an account that had bought credit and barely spent it read as a
* negative percentage of its own plan.
*
* @param {number} totalUsage - Month-to-date spend.
* @param {number} remaining - Server-netted budget left.
* @param {Object | null | undefined} usage - `usage` from `getMonthlyUsage()`.
* @param {Object | null | undefined} allowanceInfo - Its `allowanceInfo`
* sibling.
* @returns {UsageBudget}
*/
export const usageBudget = (totalUsage, remaining) => {
const used = Number.isFinite(totalUsage) ? Math.max(0, totalUsage) : 0;
const left = Number.isFinite(remaining) ? Math.max(0, remaining) : 0;
const capacity = used + left;
export const usageBudget = (usage, allowanceInfo) => {
const capacity = Number.isFinite(allowanceInfo?.monthUsageAllowance)
? Math.max(0, allowanceInfo.monthUsageAllowance)
: 0;
const total = Number.isFinite(usage?.total) ? Math.max(0, usage.total) : 0;
const allowanceUsed = Number.isFinite(usage?.allowanceUsed)
? Math.max(0, usage.allowanceUsed)
: Math.min(total, capacity);
const addons = allowanceInfo?.addons ?? {};
const purchased = Number.isFinite(addons.purchasedCredits)
? addons.purchasedCredits
: 0;
const consumed = Number.isFinite(addons.consumedPurchaseCredits)
? addons.consumedPurchaseCredits
: 0;
const creditRemaining = Math.max(0, purchased - consumed);
const used = allowanceUsed - creditRemaining;
const share = capacity ? (used / capacity) * 100 : 0;
const barPercent = Math.max(0, Math.min(100, share));
return {
used,
capacity,
percent: Math.round(barPercent),
barPercent,
percent: Math.round(share),
barPercent: Math.max(0, Math.min(100, share)),
};
};
+74 -26
View File
@@ -1,48 +1,96 @@
import { describe, expect, it } from 'vitest';
import { usageBudget } from './usageBudget.js';
const info = (allowance, addons = {}) => ({
monthUsageAllowance: allowance,
addons,
});
describe('usageBudget', () => {
it('reports the plan alone when no credit has been bought', () => {
// $0.50 spent of a $9.00 allowance.
const budget = usageBudget(50_000_000, 850_000_000);
it('anchors the bar to the monthly allowance', () => {
// $0.50 of a $9.00 allowance spent, no top-up.
const budget = usageBudget(
{ total: 50_000_000, allowanceUsed: 50_000_000 },
info(900_000_000),
);
expect(budget.capacity).toBe(900_000_000);
expect(budget.used).toBe(50_000_000);
expect(budget.percent).toBe(6);
});
it('never reports a negative share for an account holding credit', () => {
// $0.50 spent, $9.00 allowance, $10.00 of purchased credit untouched.
// The cards used to subtract held credit from spend, which rendered
// this account at roughly -105% of its own plan.
const budget = usageBudget(50_000_000, 1_850_000_000);
expect(budget.percent).toBeGreaterThanOrEqual(0);
expect(budget.percent).toBe(3);
expect(budget.capacity).toBe(1_900_000_000);
it('keeps the capacity at the plan when top-up credit exists', () => {
// $12.00 of a $95.00 allowance spent, $5.00 top-up untouched: the
// credit reads as headroom against the plan, never as more plan.
const budget = usageBudget(
{ total: 1_200_000_000, allowanceUsed: 1_200_000_000 },
info(9_500_000_000, {
purchasedCredits: 500_000_000,
consumedPurchaseCredits: 0,
}),
);
expect(budget.capacity).toBe(9_500_000_000);
expect(budget.used).toBe(700_000_000);
expect(budget.percent).toBe(7);
});
it('keeps used, capacity and share consistent once credit is being spent', () => {
// $12.00 spent: the $9.00 allowance plus $3.00 of a $10.00 top-up,
// leaving $7.00. Capacity has to stay the full $19.00 — reading it as
// allowance-plus-what's-left implied $4.00 remaining while the server
// said $7.00.
const budget = usageBudget(1_200_000_000, 700_000_000);
expect(budget.capacity).toBe(1_900_000_000);
expect(budget.used + 700_000_000).toBe(budget.capacity);
expect(budget.percent).toBe(63);
it('goes negative when unspent top-up exceeds the spend', () => {
// $0.50 spent, $10.00 credit untouched, $9.00 allowance.
const budget = usageBudget(
{ total: 50_000_000, allowanceUsed: 50_000_000 },
info(900_000_000, {
purchasedCredits: 1_000_000_000,
consumedPurchaseCredits: 0,
}),
);
expect(budget.used).toBe(-950_000_000);
expect(budget.percent).toBe(-106);
expect(budget.barPercent).toBe(0);
});
it('reads a spent budget as full rather than overflowing the bar', () => {
const budget = usageBudget(900_000_000, 0);
it('ignores credit already consumed — only what is left offsets usage', () => {
// Allowance exhausted, $200 of top-up bought and fully spent: the
// month reads as full, not as 200 dollars into the negatives.
const budget = usageBudget(
{ total: 29_500_000_000, allowanceUsed: 9_500_000_000 },
info(9_500_000_000, {
purchasedCredits: 20_000_000_000,
consumedPurchaseCredits: 20_000_000_000,
}),
);
expect(budget.used).toBe(9_500_000_000);
expect(budget.percent).toBe(100);
expect(budget.barPercent).toBe(100);
});
it('counts only allowance-charged spend against the plan', () => {
// $9.11 of allowance used this month; the total also carries spend
// that purchased credit already paid for.
const budget = usageBudget(
{ total: 1_500_000_000, allowanceUsed: 911_000_000 },
info(9_500_000_000),
);
expect(budget.used).toBe(911_000_000);
expect(budget.percent).toBe(10);
});
it('falls back to the capped total for records without the split', () => {
// Legacy record: no allowanceUsed. Everything up to the allowance
// counts, and an overshot total still reads as a full plan.
const budget = usageBudget({ total: 1_000_000_000 }, info(900_000_000));
expect(budget.used).toBe(900_000_000);
expect(budget.percent).toBe(100);
expect(budget.barPercent).toBe(100);
});
it('answers zero for an account with no budget at all', () => {
const budget = usageBudget(0, 0);
expect(budget).toMatchObject({ capacity: 0, percent: 0 });
const budget = usageBudget({ total: 0 }, info(0));
expect(budget).toMatchObject({ capacity: 0, used: 0, percent: 0 });
});
it('treats missing numbers as zero rather than rendering NaN', () => {
it('treats missing objects and numbers as zero rather than rendering NaN', () => {
expect(usageBudget(undefined, undefined).percent).toBe(0);
expect(usageBudget(NaN, 100).capacity).toBe(100);
expect(usageBudget(null, info(NaN)).capacity).toBe(0);
expect(usageBudget({ total: NaN }, info(100)).capacity).toBe(100);
expect(usageBudget({ total: NaN }, info(100)).percent).toBe(0);
});
});