clear entity cache on usage limit update

This commit is contained in:
johnyeo
2026-06-11 18:26:52 +01:00
parent eefd5e1026
commit e2848db0e8
4 changed files with 100 additions and 2 deletions

View File

@@ -50,6 +50,34 @@ for field_name, field_value in pairs(updates) do
table.insert(updated_fields, field_name)
end
if updates.usage_limits ~= nil then
local seen_feature_ids = {}
local usage_window_feature_ids = {}
local function append_usage_limit_feature_ids(usage_limits)
if type(usage_limits) ~= 'table' then
return
end
for _, usage_limit in ipairs(usage_limits) do
if type(usage_limit) == 'table' and usage_limit.feature_id ~= nil then
local feature_id = usage_limit.feature_id
if seen_feature_ids[feature_id] == nil then
seen_feature_ids[feature_id] = true
table.insert(usage_window_feature_ids, feature_id)
end
end
end
end
if cached.customer ~= nil then
append_usage_limit_feature_ids(cached.customer.usage_limits)
end
append_usage_limit_feature_ids(cached.entity.usage_limits)
cached.usageWindowFeatureIds = usage_window_feature_ids
end
redis.call("SET", subject_key, cjson.encode(cached), "EX", cache_ttl)
return cjson.encode({ success = true, updated_fields = updated_fields })

View File

@@ -278,6 +278,8 @@ export const executeRedisDeductionV2 = async ({
)
? resultJson.usage_window_mutations
: [];
const usageWindowsByFeatureId =
resultJson.usage_windows_by_feature_id ?? {};
const modifiedCustomerEntitlementIds = Array.isArray(
resultJson.modified_customer_entitlement_ids,
)
@@ -301,7 +303,7 @@ export const executeRedisDeductionV2 = async ({
// Typed handoff for the PG mirror; empty arrays kept (prune-to-empty
// must still full-replace).
for (const [featureId, usageWindows] of Object.entries(
resultJson.usage_windows_by_feature_id ?? {},
usageWindowsByFeatureId,
)) {
allUsageWindowUpdates[featureId] = {
internal_customer_id: fullSubject.internalCustomerId,

View File

@@ -20,7 +20,10 @@ export const updateCachedEntityData = async ({
customerId: string;
entityId: string;
updates: Partial<
Pick<Entity, "spend_limits" | "usage_alerts" | "overage_allowed">
Pick<
Entity,
"spend_limits" | "usage_limits" | "usage_alerts" | "overage_allowed"
>
>;
}): Promise<void> => {
if (Object.keys(updates).length === 0) return;

View File

@@ -131,6 +131,71 @@ test.concurrent(
},
);
test.concurrent(
`${chalk.yellowBright("ent-uw-enforce1b: entities.update usage_limits patches a pre-existing cached subject")}`,
async () => {
const perEntityProduct = products.base({
id: "ent-uw-enforce-cached-update",
items: [
items.monthlyMessages({
includedUsage: 100,
entityFeatureId: TestFeature.Users,
}),
],
});
const customerId = "ent-uw-enforce-cached-1";
const { entities } = await initScenario({
customerId,
setup: [
s.customer({ testClock: false }),
s.products({ list: [perEntityProduct] }),
s.entities({ count: 1, featureId: TestFeature.Users }),
],
actions: [s.billing.attach({ productId: perEntityProduct.id })],
});
await autumnV2_3.check({
customer_id: customerId,
entity_id: entities[0].id,
feature_id: TestFeature.Messages,
});
await setEntityUsageLimit({
autumn: autumnV2_3,
customerId,
entityId: entities[0].id,
featureId: TestFeature.Messages,
limit: 5,
});
await autumnV2_3.track({
customer_id: customerId,
entity_id: entities[0].id,
feature_id: TestFeature.Messages,
value: 7,
});
await expectEntityFeatureBalance({
autumn: autumnV2_3,
customerId,
entityId: entities[0].id,
featureId: TestFeature.Messages,
granted: 100,
remaining: 95,
usage: 5,
});
await expectEntityUsageLimit({
autumn: autumnV2_3,
customerId,
entityId: entities[0].id,
featureId: TestFeature.Messages,
usage: 5,
limit: 5,
});
},
);
test.concurrent(
`${chalk.yellowBright("ent-uw-enforce2: two entities with different caps stay isolated while customer balance aggregates")}`,
async () => {