feat(core): Check license config for insights max retention (#15256)

This commit is contained in:
Guillaume Jacquart
2025-05-12 09:43:58 +02:00
committed by GitHub
parent 15e62e6dfa
commit 3be05556f9
4 changed files with 84 additions and 13 deletions

View File

@@ -1,3 +1,4 @@
import type { LicenseState } from '@n8n/backend-common';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { DateTime } from 'luxon';
@@ -38,12 +39,21 @@ describe('InsightsPruningService', () => {
let insightsConfig: InsightsConfig;
let insightsByPeriodRepository: InsightsByPeriodRepository;
let insightsPruningService: InsightsPruningService;
let licenseState: LicenseState;
beforeAll(async () => {
insightsConfig = Container.get(InsightsConfig);
insightsConfig.maxAgeDays = 10;
insightsConfig.pruneCheckIntervalHours = 1;
insightsPruningService = Container.get(InsightsPruningService);
insightsByPeriodRepository = Container.get(InsightsByPeriodRepository);
licenseState = mock<LicenseState>({
getInsightsRetentionMaxAge: () => insightsConfig.maxAgeDays,
});
insightsPruningService = new InsightsPruningService(
insightsByPeriodRepository,
insightsConfig,
licenseState,
mockLogger(),
);
});
test('old insights get pruned successfully', async () => {
@@ -90,6 +100,58 @@ describe('InsightsPruningService', () => {
expect(await insightsByPeriodRepository.count()).toBe(1);
});
test.each<{ config: number; license: number; result: number }>([
{
config: -1,
license: -1,
result: Number.MAX_SAFE_INTEGER,
},
{
config: -1,
license: 5,
result: 5,
},
{
config: 5,
license: -1,
result: 5,
},
{
config: 5,
license: 10,
result: 5,
},
{
config: 10,
license: 5,
result: 5,
},
])(
'pruningMaxAgeInDays is minimal age between license and config max age',
async ({ config, license, result }) => {
// ARRANGE
const licenseState = mock<LicenseState>({
getInsightsRetentionMaxAge() {
return license;
},
});
const insightsPruningService = new InsightsPruningService(
insightsByPeriodRepository,
mock<InsightsConfig>({
maxAgeDays: config,
}),
licenseState,
mockLogger(),
);
// ACT
const maxAge = insightsPruningService.pruningMaxAgeInDays;
// ASSERT
expect(maxAge).toBe(result);
},
);
describe('pruning scheduling', () => {
beforeEach(() => {
jest.useFakeTimers();
@@ -111,6 +173,7 @@ describe('InsightsPruningService', () => {
const insightsPruningService = new InsightsPruningService(
insightsByPeriodRepository,
insightsConfig,
licenseState,
mockLogger(),
);
const pruneSpy = jest.spyOn(insightsPruningService, 'pruneInsights');
@@ -134,6 +197,7 @@ describe('InsightsPruningService', () => {
const insightsPruningService = new InsightsPruningService(
insightsByPeriodRepository,
insightsConfig,
licenseState,
mockLogger(),
);

View File

@@ -512,7 +512,6 @@ describe('getAvailableDateRanges', () => {
mock<InsightsCollectionService>(),
mock<InsightsPruningService>(),
licenseMock,
mock<InsightsConfig>(),
mockLogger(),
);
});
@@ -615,7 +614,6 @@ describe('getMaxAgeInDaysAndGranularity', () => {
mock<InsightsCollectionService>(),
mock<InsightsPruningService>(),
licenseMock,
mock<InsightsConfig>(),
mockLogger(),
);
});
@@ -704,7 +702,6 @@ describe('shutdown', () => {
mockCollectionService,
mockPruningService,
mock<LicenseState>(),
mock<InsightsConfig>(),
mockLogger(),
);
});
@@ -736,6 +733,7 @@ describe('timers', () => {
const mockPruningService = mock<InsightsPruningService>({
startPruningTimer: jest.fn(),
stopPruningTimer: jest.fn(),
isPruningEnabled: false,
});
const mockedLogger = mockLogger();
@@ -750,7 +748,6 @@ describe('timers', () => {
mockCollectionService,
mockPruningService,
mock<LicenseState>(),
mockedConfig,
mockedLogger,
);
});
@@ -768,6 +765,7 @@ describe('timers', () => {
test('startTimers starts pruning timer', () => {
// ARRANGE
mockedConfig.maxAgeDays = 30;
Object.defineProperty(mockPruningService, 'isPruningEnabled', { value: true });
// ACT
insightsService.startTimers();

View File

@@ -1,3 +1,4 @@
import { LicenseState } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import { strict } from 'assert';
import { Logger } from 'n8n-core';
@@ -18,11 +19,25 @@ export class InsightsPruningService {
constructor(
private readonly insightsByPeriodRepository: InsightsByPeriodRepository,
private readonly config: InsightsConfig,
private readonly licenseState: LicenseState,
private readonly logger: Logger,
) {
this.logger = this.logger.scoped('insights');
}
get isPruningEnabled() {
return this.licenseState.getInsightsRetentionMaxAge() > -1 || this.config.maxAgeDays > -1;
}
get pruningMaxAgeInDays() {
const toMaxSafeIfUnlimited = (days: number) => (days === -1 ? Number.MAX_SAFE_INTEGER : days);
const licenseMaxAge = toMaxSafeIfUnlimited(this.licenseState.getInsightsRetentionMaxAge());
const configMaxAge = toMaxSafeIfUnlimited(this.config.maxAgeDays);
return Math.min(licenseMaxAge, configMaxAge);
}
startPruningTimer() {
strict(this.isStopped);
this.clearPruningTimer();
@@ -57,7 +72,7 @@ export class InsightsPruningService {
async pruneInsights() {
this.logger.info('Pruning old insights data');
try {
const result = await this.insightsByPeriodRepository.pruneOldData(this.config.maxAgeDays);
const result = await this.insightsByPeriodRepository.pruneOldData(this.pruningMaxAgeInDays);
this.logger.debug(
'Deleted insights by period',
result.affected ? { count: result.affected } : {},

View File

@@ -15,7 +15,6 @@ import { InsightsByPeriodRepository } from './database/repositories/insights-by-
import { InsightsCollectionService } from './insights-collection.service';
import { InsightsCompactionService } from './insights-compaction.service';
import { InsightsPruningService } from './insights-pruning.service';
import { InsightsConfig } from './insights.config';
const keyRangeToDays: Record<InsightsDateRange['key'], number> = {
day: 1,
@@ -35,20 +34,15 @@ export class InsightsService {
private readonly collectionService: InsightsCollectionService,
private readonly pruningService: InsightsPruningService,
private readonly licenseState: LicenseState,
private readonly config: InsightsConfig,
private readonly logger: Logger,
) {
this.logger = this.logger.scoped('insights');
}
get isPruningEnabled() {
return this.config.maxAgeDays > -1;
}
startTimers() {
this.compactionService.startCompactionTimer();
this.collectionService.startFlushingTimer();
if (this.isPruningEnabled) {
if (this.pruningService.isPruningEnabled) {
this.pruningService.startPruningTimer();
}
this.logger.debug('Started compaction, flushing and pruning schedulers');