import { GlobalConfig } from '@n8n/config'; import { Service } from '@n8n/di'; import { toMb } from '@n8n/utils/number/bytes'; import { DataTableSizeStatus, DataTablesSizeData } from 'n8n-workflow'; import { Telemetry } from '@/telemetry'; import { DataTableValidationError } from './errors/data-table-validation.error'; @Service() export class DataTableSizeValidator { private lastCheck: Date | undefined; private cachedSizeData: DataTablesSizeData | undefined; private pendingCheck: Promise | null = null; constructor( private readonly globalConfig: GlobalConfig, private readonly telemetry: Telemetry, ) {} private shouldRefresh(now: Date): boolean { if ( !this.lastCheck || !this.cachedSizeData || now.getTime() - this.lastCheck.getTime() >= this.globalConfig.dataTable.sizeCheckCacheDuration ) { return true; } return false; } async getCachedSizeData( fetchSizeDataFn: () => Promise, now = new Date(), ): Promise { // If there's a pending check, wait for it to complete if (this.pendingCheck) { this.cachedSizeData = await this.pendingCheck; } else { // Check if we need to refresh the size data if (this.shouldRefresh(now)) { this.pendingCheck = fetchSizeDataFn(); try { this.cachedSizeData = await this.pendingCheck; this.lastCheck = now; } finally { this.pendingCheck = null; } } } return this.cachedSizeData!; } async validateSize( fetchSizeFn: () => Promise, now = new Date(), ): Promise { const size = await this.getCachedSizeData(fetchSizeFn, now); if (size.totalBytes >= this.globalConfig.dataTable.maxSize) { this.telemetry.track('User hit data table storage limit', { total_bytes: size.totalBytes, max_bytes: this.globalConfig.dataTable.maxSize, }); throw new DataTableValidationError( `Data table size limit exceeded: ${toMb(size.totalBytes)}MB used, limit is ${toMb(this.globalConfig.dataTable.maxSize)}MB`, ); } } sizeToState(sizeBytes: number): DataTableSizeStatus { const warningThreshold = this.globalConfig.dataTable.warningThreshold ?? Math.floor(0.8 * this.globalConfig.dataTable.maxSize); if (sizeBytes >= this.globalConfig.dataTable.maxSize) { return 'error'; } else if (sizeBytes >= warningThreshold) { return 'warn'; } return 'ok'; } async getSizeStatus(fetchSizeFn: () => Promise, now = new Date()) { const size = await this.getCachedSizeData(fetchSizeFn, now); return this.sizeToState(size.totalBytes); } reset() { this.lastCheck = undefined; this.cachedSizeData = undefined; this.pendingCheck = null; } }