fix(runtime): prevent memory leaks and improve shutdown/stream handling across services

This commit is contained in:
2026-02-19 08:33:41 +00:00
parent ddd0662fb8
commit 9ac297c197
8 changed files with 72 additions and 8 deletions

View File

@@ -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

View File

@@ -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.'
}

View File

@@ -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
*/

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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.'
}