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:
+21
-2
@@ -2,6 +2,8 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
@@ -69,6 +71,15 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
getWorkflowRunOrFail: jest.fn(),
|
||||
};
|
||||
|
||||
const mockBillingService = {
|
||||
isBillingEnabled: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
const mockBillingUsageService = {
|
||||
hasAvailableCredits: jest.fn().mockResolvedValue(true),
|
||||
decrementAvailableCredits: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mockExceptionHandlerService = {
|
||||
captureExceptions: jest.fn(),
|
||||
};
|
||||
@@ -101,6 +112,14 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
provide: WorkflowRunWorkspaceService,
|
||||
useValue: mockWorkflowRunWorkspaceService,
|
||||
},
|
||||
{
|
||||
provide: BillingService,
|
||||
useValue: mockBillingService,
|
||||
},
|
||||
{
|
||||
provide: BillingUsageService,
|
||||
useValue: mockBillingUsageService,
|
||||
},
|
||||
{
|
||||
provide: ExceptionHandlerService,
|
||||
useValue: mockExceptionHandlerService,
|
||||
@@ -675,8 +694,8 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
||||
});
|
||||
|
||||
describe('sendWorkflowNodeRunEvent', () => {
|
||||
it('should emit a billing event', () => {
|
||||
service['sendWorkflowNodeRunEvent']('workspace-id', 'workflow-id');
|
||||
it('should emit a billing event', async () => {
|
||||
await service['sendWorkflowNodeRunEvent']('workspace-id', 'workflow-id');
|
||||
|
||||
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||
USAGE_RECORDED,
|
||||
|
||||
+16
-2
@@ -9,6 +9,8 @@ import {
|
||||
WorkflowRunStepInfos,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
@@ -56,6 +58,8 @@ export class WorkflowExecutorWorkspaceService {
|
||||
private readonly workflowActionFactory: WorkflowActionFactory,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly billingUsageService: BillingUsageService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
private readonly metricsService: MetricsService,
|
||||
@InjectMessageQueue(MessageQueue.workflowQueue)
|
||||
@@ -178,7 +182,7 @@ export class WorkflowExecutorWorkspaceService {
|
||||
!actionOutput.shouldFailSafely &&
|
||||
!actionOutput.shouldSkipStepExecution
|
||||
) {
|
||||
this.sendWorkflowNodeRunEvent(workspaceId, workflowRun.workflowId);
|
||||
await this.sendWorkflowNodeRunEvent(workspaceId, workflowRun.workflowId);
|
||||
}
|
||||
|
||||
const { shouldProcessNextSteps } = await this.processStepExecutionResult({
|
||||
@@ -355,7 +359,10 @@ export class WorkflowExecutorWorkspaceService {
|
||||
});
|
||||
}
|
||||
|
||||
private sendWorkflowNodeRunEvent(workspaceId: string, workflowId: string) {
|
||||
private async sendWorkflowNodeRunEvent(
|
||||
workspaceId: string,
|
||||
workflowId: string,
|
||||
) {
|
||||
this.workspaceEventEmitter.emitCustomBatchEvent<UsageEvent>(
|
||||
USAGE_RECORDED,
|
||||
[
|
||||
@@ -370,6 +377,13 @@ export class WorkflowExecutorWorkspaceService {
|
||||
],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (this.billingService.isBillingEnabled()) {
|
||||
await this.billingUsageService.decrementAvailableCredits({
|
||||
workspaceId,
|
||||
usedCredits: 100,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async processStepExecutionResult({
|
||||
|
||||
Reference in New Issue
Block a user