Billing - optimize usageEvent CH table (#20019)

- Update usageEvent clickhouse table, partitioning, indexing and
projection (auto materialized view) to optimize credit usage queries
- Add caching for available credits and billing subscription


To do in next PR: deprecate enforceCapUsage cron. Bonus : real-time on
billingSubscription
This commit is contained in:
Etienne
2026-04-28 16:06:49 +02:00
committed by GitHub
parent 2a3b8adcfb
commit fbaea0639a
33 changed files with 601 additions and 379 deletions
@@ -3,9 +3,12 @@
* ClickHouse expects: YYYY-MM-DD HH:mm:ss.SSS (no 'T' separator, no 'Z' suffix)
* JavaScript toISOString() returns: YYYY-MM-DDTHH:mm:ss.SSSZ
*/
export const formatDateForClickHouse = (date: Date | string): string => {
export const formatDateTimeForClickHouse = (date: Date | string): string => {
const iso = typeof date === 'string' ? date : date.toISOString();
// Extract date (YYYY-MM-DD) and time with milliseconds (HH:mm:ss.SSS)
return `${iso.slice(0, 10)} ${iso.slice(11, 23)}`;
};
export const formatDateForClickHouse = (date: Date): string =>
date.toISOString().slice(0, 10);
@@ -0,0 +1,53 @@
CREATE TABLE
IF NOT EXISTS usageEvent_v2 (
`timestamp` DateTime64 (3) NOT NULL,
`workspaceId` String NOT NULL,
`periodStart` DateTime64 (3),
`userWorkspaceId` String DEFAULT '',
`resourceType` LowCardinality (String) NOT NULL,
`operationType` LowCardinality (String) NOT NULL,
`quantity` Int64 NOT NULL DEFAULT 0,
`unit` LowCardinality (String) NOT NULL DEFAULT 'CREDIT',
`creditsUsedMicro` Int64 NOT NULL DEFAULT 0,
`resourceId` String DEFAULT '',
`resourceContext` String DEFAULT '',
`metadata` JSON
) ENGINE = MergeTree
PARTITION BY
toYYYYMM (timestamp)
ORDER BY
(workspaceId, timestamp, userWorkspaceId)
PRIMARY KEY (workspaceId, timestamp)
TTL toDateTime(timestamp) + INTERVAL 3 YEAR DELETE;
INSERT INTO
usageEvent_v2 (timestamp, workspaceId, userWorkspaceId, resourceType, operationType, quantity, unit, creditsUsedMicro, resourceId, resourceContext, metadata)
SELECT
timestamp,
workspaceId,
userWorkspaceId,
resourceType,
operationType,
quantity,
unit,
creditsUsedMicro,
resourceId,
resourceContext,
metadata
FROM
usageEvent;
RENAME TABLE usageEvent TO usageEvent_old,
usageEvent_v2 TO usageEvent;
DROP TABLE IF EXISTS usageEvent_old;
ALTER TABLE usageEvent ADD PROJECTION IF NOT EXISTS billing_by_workspace_period (
SELECT
workspaceId,
periodStart,
sum(creditsUsedMicro) AS totalCreditsUsedMicro
GROUP BY
workspaceId, periodStart
);
@@ -4,7 +4,7 @@ import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
import { type GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
import { formatDateForClickHouse } from 'src/database/clickHouse/clickHouse.util';
import { formatDateTimeForClickHouse } from 'src/database/clickHouse/clickHouse.util';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
@@ -17,6 +17,7 @@ export type ObjectEventFixture = GenericTrackEvent & {
export type UsageEventFixture = {
timestamp: string;
workspaceId: string;
periodStart: string;
userWorkspaceId: string;
resourceType: string;
operationType: string;
@@ -215,8 +216,17 @@ const buildUsageEventFixtures = (): UsageEventFixture[] => {
: '';
fixtures.push({
timestamp: formatDateForClickHouse(eventDate),
timestamp: formatDateTimeForClickHouse(eventDate),
workspaceId: SEED_APPLE_WORKSPACE_ID,
periodStart: formatDateTimeForClickHouse(
new Date(
Date.UTC(
eventDate.getUTCFullYear(),
eventDate.getUTCMonth(),
1,
),
),
),
userWorkspaceId: users[userIdx],
resourceType: op.resourceType,
operationType: op.operationType,