mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(core): Decouple insights pruning max age from license (#29527)
This commit is contained in:
+10
-46
@@ -1,4 +1,3 @@
|
||||
import type { LicenseState } from '@n8n/backend-common';
|
||||
import {
|
||||
mockLogger,
|
||||
createTeamProject,
|
||||
@@ -41,20 +40,15 @@ 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;
|
||||
insightsByPeriodRepository = Container.get(InsightsByPeriodRepository);
|
||||
licenseState = mock<LicenseState>({
|
||||
getInsightsRetentionMaxAge: () => insightsConfig.maxAgeDays,
|
||||
});
|
||||
insightsPruningService = new InsightsPruningService(
|
||||
insightsByPeriodRepository,
|
||||
insightsConfig,
|
||||
licenseState,
|
||||
mockLogger(),
|
||||
);
|
||||
});
|
||||
@@ -103,55 +97,25 @@ 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,
|
||||
},
|
||||
test.each<{ config: number; result: number }>([
|
||||
{ config: -1, result: 730 },
|
||||
{ config: 0, result: 365 },
|
||||
{ config: 5, result: 5 },
|
||||
{ config: 365, result: 365 },
|
||||
{ config: 730, result: 730 },
|
||||
{ config: 2000, result: 730 },
|
||||
])(
|
||||
'pruningMaxAgeInDays is minimal age between license and config max age',
|
||||
async ({ config, license, result }) => {
|
||||
// ARRANGE
|
||||
const licenseState = mock<LicenseState>({
|
||||
getInsightsRetentionMaxAge() {
|
||||
return license;
|
||||
},
|
||||
});
|
||||
'pruningMaxAgeInDays uses N8N_INSIGHTS_MAX_AGE_DAYS: -1 maps to cap, other values below 1 use default, finite values capped at 730',
|
||||
async ({ config, result }) => {
|
||||
const insightsPruningService = new InsightsPruningService(
|
||||
insightsByPeriodRepository,
|
||||
mock<InsightsConfig>({
|
||||
maxAgeDays: config,
|
||||
}),
|
||||
licenseState,
|
||||
mockLogger(),
|
||||
);
|
||||
|
||||
// ACT
|
||||
const maxAge = insightsPruningService.pruningMaxAgeInDays;
|
||||
|
||||
// ASSERT
|
||||
expect(maxAge).toBe(result);
|
||||
expect(insightsPruningService.pruningMaxAgeInDays).toBe(result);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { LicenseState } from '@n8n/backend-common';
|
||||
import { mockLogger } from '@n8n/backend-test-utils';
|
||||
import { Time } from '@n8n/constants';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
@@ -11,20 +10,15 @@ describe('InsightsPruningService', () => {
|
||||
let insightsConfig: InsightsConfig;
|
||||
let insightsByPeriodRepository: InsightsByPeriodRepository;
|
||||
let insightsPruningService: InsightsPruningService;
|
||||
let licenseState: LicenseState;
|
||||
|
||||
beforeAll(() => {
|
||||
insightsConfig = new InsightsConfig();
|
||||
insightsConfig.maxAgeDays = 10;
|
||||
insightsConfig.pruneCheckIntervalHours = 1;
|
||||
insightsByPeriodRepository = mock<InsightsByPeriodRepository>();
|
||||
licenseState = mock<LicenseState>({
|
||||
getInsightsRetentionMaxAge: () => insightsConfig.maxAgeDays,
|
||||
});
|
||||
insightsPruningService = new InsightsPruningService(
|
||||
insightsByPeriodRepository,
|
||||
insightsConfig,
|
||||
licenseState,
|
||||
mockLogger(),
|
||||
);
|
||||
});
|
||||
@@ -51,7 +45,6 @@ describe('InsightsPruningService', () => {
|
||||
const insightsPruningService = new InsightsPruningService(
|
||||
insightsByPeriodRepository,
|
||||
insightsConfig,
|
||||
licenseState,
|
||||
mockLogger(),
|
||||
);
|
||||
const pruneSpy = jest.spyOn(insightsPruningService, 'pruneInsights');
|
||||
@@ -75,7 +68,6 @@ describe('InsightsPruningService', () => {
|
||||
const insightsPruningService = new InsightsPruningService(
|
||||
insightsByPeriodRepository,
|
||||
insightsConfig,
|
||||
licenseState,
|
||||
mockLogger(),
|
||||
);
|
||||
|
||||
|
||||
@@ -85,22 +85,15 @@ describe('InsightsService (Integration)', () => {
|
||||
shutdownSpy.mockRestore();
|
||||
});
|
||||
|
||||
const setupMocks = (
|
||||
instanceType: InstanceType,
|
||||
isLeader: boolean = false,
|
||||
isPruningEnabled: boolean = false,
|
||||
) => {
|
||||
const setupMocks = (instanceType: InstanceType, isLeader: boolean = false) => {
|
||||
(instanceSettings as any).instanceType = instanceType;
|
||||
Object.defineProperty(instanceSettings, 'isLeader', {
|
||||
get: jest.fn(() => isLeader),
|
||||
});
|
||||
Object.defineProperty(pruningService, 'isPruningEnabled', {
|
||||
get: jest.fn(() => isPruningEnabled),
|
||||
});
|
||||
};
|
||||
|
||||
test('starts flushing timer for main instance', async () => {
|
||||
setupMocks('main', false, false);
|
||||
setupMocks('main', false);
|
||||
|
||||
await insightsService.init();
|
||||
|
||||
@@ -109,18 +102,8 @@ describe('InsightsService (Integration)', () => {
|
||||
expect(pruningService.startPruningTimer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('starts compaction and flushing timers for main leader instances', async () => {
|
||||
setupMocks('main', true, false);
|
||||
|
||||
await insightsService.init();
|
||||
|
||||
expect(initSpy).toHaveBeenCalled();
|
||||
expect(compactionService.startCompactionTimer).toHaveBeenCalled();
|
||||
expect(pruningService.startPruningTimer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('starts compaction, flushing and pruning timers for main leader instance with pruning enabled', async () => {
|
||||
setupMocks('main', true, true);
|
||||
test('starts compaction, flushing and pruning timers for main leader instances', async () => {
|
||||
setupMocks('main', true);
|
||||
|
||||
await insightsService.init();
|
||||
|
||||
@@ -130,7 +113,7 @@ describe('InsightsService (Integration)', () => {
|
||||
});
|
||||
|
||||
test('starts only collection flushing timer for webhook instance', async () => {
|
||||
setupMocks('webhook', false, false);
|
||||
setupMocks('webhook', false);
|
||||
|
||||
await insightsService.init();
|
||||
|
||||
@@ -140,7 +123,7 @@ describe('InsightsService (Integration)', () => {
|
||||
});
|
||||
|
||||
test('do no start any timers for non-main instances', async () => {
|
||||
setupMocks('worker', false, false);
|
||||
setupMocks('worker', false);
|
||||
|
||||
await insightsService.init();
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { LicenseState, Logger } from '@n8n/backend-common';
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { Time } from '@n8n/constants';
|
||||
import { Service } from '@n8n/di';
|
||||
import { strict } from 'assert';
|
||||
|
||||
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
|
||||
import { InsightsConfig } from './insights.config';
|
||||
import { INSIGHTS_MAX_AGE_DAYS_CAP, INSIGHTS_MAX_AGE_DAYS_DEFAULT } from './insights.constants';
|
||||
|
||||
@Service()
|
||||
export class InsightsPruningService {
|
||||
@@ -17,23 +18,26 @@ 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 configuredMaxAgeDays = this.config.maxAgeDays;
|
||||
if (typeof configuredMaxAgeDays !== 'number' || !Number.isFinite(configuredMaxAgeDays)) {
|
||||
return INSIGHTS_MAX_AGE_DAYS_DEFAULT;
|
||||
}
|
||||
|
||||
const licenseMaxAge = toMaxSafeIfUnlimited(this.licenseState.getInsightsRetentionMaxAge());
|
||||
const configMaxAge = toMaxSafeIfUnlimited(this.config.maxAgeDays);
|
||||
if (configuredMaxAgeDays === -1) {
|
||||
return INSIGHTS_MAX_AGE_DAYS_CAP;
|
||||
}
|
||||
|
||||
return Math.min(licenseMaxAge, configMaxAge);
|
||||
if (configuredMaxAgeDays < 1) {
|
||||
return INSIGHTS_MAX_AGE_DAYS_DEFAULT;
|
||||
}
|
||||
|
||||
return Math.min(configuredMaxAgeDays, INSIGHTS_MAX_AGE_DAYS_CAP);
|
||||
}
|
||||
|
||||
startPruningTimer() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Config, Env } from '@n8n/config';
|
||||
|
||||
import { INSIGHTS_MAX_AGE_DAYS_DEFAULT } from './insights.constants';
|
||||
|
||||
@Config
|
||||
export class InsightsConfig {
|
||||
/**
|
||||
@@ -45,11 +47,11 @@ export class InsightsConfig {
|
||||
flushIntervalSeconds: number = 30;
|
||||
|
||||
/**
|
||||
* How old (days) insights data must be to qualify for regular deletion
|
||||
* Default: -1 (no pruning)
|
||||
* How old (days) insights data must be to qualify for regular deletion.
|
||||
* Default: 365. Values are capped at 730 (two years).
|
||||
*/
|
||||
@Env('N8N_INSIGHTS_MAX_AGE_DAYS')
|
||||
maxAgeDays: number = -1;
|
||||
maxAgeDays: number = INSIGHTS_MAX_AGE_DAYS_DEFAULT;
|
||||
|
||||
/**
|
||||
* How often (hours) insights data will be checked for regular deletion.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/** Default for `N8N_INSIGHTS_MAX_AGE_DAYS`; also used when the configured value is invalid or not a finite number ≥ 1. */
|
||||
export const INSIGHTS_MAX_AGE_DAYS_DEFAULT = 365;
|
||||
|
||||
/** Maximum allowed value for `N8N_INSIGHTS_MAX_AGE_DAYS` when pruning (2 years). */
|
||||
export const INSIGHTS_MAX_AGE_DAYS_CAP = 730;
|
||||
|
||||
export const INSIGHTS_DATE_RANGE_KEYS = [
|
||||
'day',
|
||||
'week',
|
||||
|
||||
@@ -52,9 +52,7 @@ export class InsightsService {
|
||||
@OnLeaderTakeover()
|
||||
startCompactionAndPruningTimers() {
|
||||
this.compactionService.startCompactionTimer();
|
||||
if (this.pruningService.isPruningEnabled) {
|
||||
this.pruningService.startPruningTimer();
|
||||
}
|
||||
this.pruningService.startPruningTimer();
|
||||
}
|
||||
|
||||
@OnLeaderStepdown()
|
||||
|
||||
Reference in New Issue
Block a user