fix(runtime): prevent memory leaks and improve shutdown/stream handling across services
This commit is contained in:
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-02-18 - 6.13.1 - fix(dcrouter)
|
||||||
enable PROXY protocol v1 handling for SmartProxy when remoteIngress is enabled to preserve client IPs
|
enable PROXY protocol v1 handling for SmartProxy when remoteIngress is enabled to preserve client IPs
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.13.1',
|
version: '6.13.2',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* Get backoff info for UI display
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -534,6 +534,12 @@ export class DcRouter {
|
|||||||
|
|
||||||
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
||||||
if (challengeHandlers.length > 0) {
|
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({
|
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||||
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
||||||
certManager: new StorageBackedCertManager(this.storageManager),
|
certManager: new StorageBackedCertManager(this.storageManager),
|
||||||
@@ -944,6 +950,25 @@ export class DcRouter {
|
|||||||
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err));
|
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');
|
console.log('All DcRouter services stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during DcRouter shutdown:', error);
|
console.error('Error during DcRouter shutdown:', error);
|
||||||
|
|||||||
@@ -279,6 +279,14 @@ export class MetricsManager {
|
|||||||
if (recipient) {
|
if (recipient) {
|
||||||
const count = this.emailMetrics.recipients.get(recipient) || 0;
|
const count = this.emailMetrics.recipients.get(recipient) || 0;
|
||||||
this.emailMetrics.recipients.set(recipient, count + 1);
|
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) {
|
if (deliveryTimeMs) {
|
||||||
|
|||||||
@@ -148,17 +148,17 @@ export class LogsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For follow mode, simulate real-time log streaming
|
// 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 categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
||||||
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
||||||
|
|
||||||
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
|
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
|
||||||
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
|
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
|
||||||
|
|
||||||
// Filter by requested criteria
|
// Filter by requested criteria
|
||||||
if (levelFilter && !levelFilter.includes(mockLevel)) return;
|
if (levelFilter && !levelFilter.includes(mockLevel)) return;
|
||||||
if (categoryFilter && !categoryFilter.includes(mockCategory)) return;
|
if (categoryFilter && !categoryFilter.includes(mockCategory)) return;
|
||||||
|
|
||||||
const logEntry = {
|
const logEntry = {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
level: mockLevel,
|
level: mockLevel,
|
||||||
@@ -168,10 +168,16 @@ export class LogsHandler {
|
|||||||
requestId: plugins.uuid.v4(),
|
requestId: plugins.uuid.v4(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const logData = JSON.stringify(logEntry);
|
const logData = JSON.stringify(logEntry);
|
||||||
const encoder = new TextEncoder();
|
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
|
}, 2000); // Send a log every 2 seconds
|
||||||
|
|
||||||
// TODO: Hook into actual logger events
|
// TODO: Hook into actual logger events
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ export class VlanManager {
|
|||||||
// Cache the result
|
// Cache the result
|
||||||
this.normalizedMacCache.set(mac, normalized);
|
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;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.13.1',
|
version: '6.13.2',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user