diff --git a/changelog.md b/changelog.md index e6bfc37..b19f9fc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-19 - 6.13.2 - fix(runtime) +prevent memory leaks and improve shutdown/stream handling across services + +- Add CertProvisionScheduler.clear() to reset in-memory backoff cache and call it during DcRouter shutdown +- Stop any existing SmartAcme instance before creating a new one (await stop and log errors) to avoid duplicate running instances +- Null out many DcRouter service references and clear certificateStatusMap on shutdown to allow GC of stopped services +- Cap emailMetrics.recipients map size and trim to ~80% of MAX_TOP_DOMAINS to prevent unbounded growth +- Await virtualStream.sendData in logs follow handler and clear the interval if the stream errors/closes to avoid interval leaks +- Limit normalizedMacCache size and evict oldest entries when it exceeds 10000 to prevent unbounded cache growth + ## 2026-02-18 - 6.13.1 - fix(dcrouter) enable PROXY protocol v1 handling for SmartProxy when remoteIngress is enabled to preserve client IPs diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c5ae802..5c5c47f 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '6.13.1', + version: '6.13.2', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.cert-provision-scheduler.ts b/ts/classes.cert-provision-scheduler.ts index e70bdc6..59f4833 100644 --- a/ts/classes.cert-provision-scheduler.ts +++ b/ts/classes.cert-provision-scheduler.ts @@ -106,6 +106,13 @@ export class CertProvisionScheduler { } } + /** + * Clear all in-memory backoff cache entries + */ + public clear(): void { + this.backoffCache.clear(); + } + /** * Get backoff info for UI display */ diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 3acc64d..ec0c5cb 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -534,6 +534,12 @@ export class DcRouter { // If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction if (challengeHandlers.length > 0) { + // Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig) + if (this.smartAcme) { + await this.smartAcme.stop().catch(err => + console.error('[DcRouter] Error stopping old SmartAcme:', err) + ); + } this.smartAcme = new plugins.smartacme.SmartAcme({ accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com', certManager: new StorageBackedCertManager(this.storageManager), @@ -944,6 +950,25 @@ export class DcRouter { await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err)); } + // Clear backoff cache in cert scheduler + if (this.certProvisionScheduler) { + this.certProvisionScheduler.clear(); + } + + // Allow GC of stopped services by nulling references + this.smartProxy = undefined; + this.emailServer = undefined; + this.dnsServer = undefined; + this.metricsManager = undefined; + this.cacheCleaner = undefined; + this.cacheDb = undefined; + this.tunnelManager = undefined; + this.radiusServer = undefined; + this.smartAcme = undefined; + this.certProvisionScheduler = undefined; + this.remoteIngressManager = undefined; + this.certificateStatusMap.clear(); + console.log('All DcRouter services stopped'); } catch (error) { console.error('Error during DcRouter shutdown:', error); diff --git a/ts/monitoring/classes.metricsmanager.ts b/ts/monitoring/classes.metricsmanager.ts index 597797c..5060096 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -279,6 +279,14 @@ export class MetricsManager { if (recipient) { const count = this.emailMetrics.recipients.get(recipient) || 0; this.emailMetrics.recipients.set(recipient, count + 1); + + // Cap recipients map to prevent unbounded growth within a day + if (this.emailMetrics.recipients.size > this.MAX_TOP_DOMAINS) { + const sorted = Array.from(this.emailMetrics.recipients.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8)); + this.emailMetrics.recipients = new Map(sorted); + } } if (deliveryTimeMs) { diff --git a/ts/opsserver/handlers/logs.handler.ts b/ts/opsserver/handlers/logs.handler.ts index 9989dc4..f934567 100644 --- a/ts/opsserver/handlers/logs.handler.ts +++ b/ts/opsserver/handlers/logs.handler.ts @@ -148,17 +148,17 @@ export class LogsHandler { } // For follow mode, simulate real-time log streaming - intervalId = setInterval(() => { + intervalId = setInterval(async () => { const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email']; const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug']; - + const mockCategory = categories[Math.floor(Math.random() * categories.length)]; const mockLevel = levels[Math.floor(Math.random() * levels.length)]; - + // Filter by requested criteria if (levelFilter && !levelFilter.includes(mockLevel)) return; if (categoryFilter && !categoryFilter.includes(mockCategory)) return; - + const logEntry = { timestamp: Date.now(), level: mockLevel, @@ -168,10 +168,16 @@ export class LogsHandler { requestId: plugins.uuid.v4(), }, }; - + const logData = JSON.stringify(logEntry); const encoder = new TextEncoder(); - virtualStream.sendData(encoder.encode(logData)); + try { + await virtualStream.sendData(encoder.encode(logData)); + } catch { + // Stream closed or errored — clean up to prevent interval leak + clearInterval(intervalId!); + intervalId = null; + } }, 2000); // Send a log every 2 seconds // TODO: Hook into actual logger events diff --git a/ts/radius/classes.vlan.manager.ts b/ts/radius/classes.vlan.manager.ts index 8bfd9a9..9fdbc18 100644 --- a/ts/radius/classes.vlan.manager.ts +++ b/ts/radius/classes.vlan.manager.ts @@ -100,6 +100,14 @@ export class VlanManager { // Cache the result this.normalizedMacCache.set(mac, normalized); + // Prevent unbounded cache growth + if (this.normalizedMacCache.size > 10000) { + const iterator = this.normalizedMacCache.keys(); + for (let i = 0; i < 1000; i++) { + this.normalizedMacCache.delete(iterator.next().value); + } + } + return normalized; } diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index c5ae802..5c5c47f 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '6.13.1', + version: '6.13.2', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }