-
Notifications
You must be signed in to change notification settings - Fork 66
fix: add destroy methods to prevent memory leaks in protocol clients #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,4 +28,6 @@ export default interface Protocol { | |
| report(): this; | ||
|
|
||
| flush(): Promise<any> | null; | ||
|
|
||
| destroy?(): void; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,4 +23,6 @@ export default interface Client { | |
| start(): void; | ||
|
|
||
| flush(): Promise<any> | null; | ||
|
|
||
| destroy?(): void; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,4 +89,14 @@ export default class HeartbeatClient implements Client { | |
| logger.warn('HeartbeatClient does not need flush().'); | ||
| return null; | ||
| } | ||
|
|
||
| destroy(): void { | ||
| // Clear heartbeat timer to prevent memory leak | ||
| if (this.heartbeatTimer) { | ||
| clearInterval(this.heartbeatTimer); | ||
| this.heartbeatTimer = undefined; | ||
| } | ||
|
|
||
| logger.info('HeartbeatClient destroyed and resources cleaned up'); | ||
| } | ||
|
Comment on lines
+93
to
+101
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,16 +34,31 @@ export default class TraceReportClient implements Client { | |||||||||
| private readonly reporterClient: TraceSegmentReportServiceClient; | ||||||||||
| private readonly buffer: Segment[] = []; | ||||||||||
| private timeout?: NodeJS.Timeout; | ||||||||||
| private segmentFinishedListener: (segment: Segment) => void; | ||||||||||
|
|
||||||||||
| constructor() { | ||||||||||
| this.reporterClient = new TraceSegmentReportServiceClient( | ||||||||||
| config.collectorAddress, | ||||||||||
| config.secure ? grpc.credentials.createSsl() : grpc.credentials.createInsecure(), | ||||||||||
| ); | ||||||||||
| emitter.on('segment-finished', (segment) => { | ||||||||||
|
|
||||||||||
| // Store listener reference for cleanup | ||||||||||
| this.segmentFinishedListener = (segment: Segment) => { | ||||||||||
| // Limit buffer size to prevent memory leak during network issues | ||||||||||
| if (this.buffer.length >= config.maxBufferSize) { | ||||||||||
| logger.warn( | ||||||||||
| `Trace buffer reached maximum size (${config.maxBufferSize}). ` + | ||||||||||
| `Discarding oldest segment to prevent memory leak. ` + | ||||||||||
| `This may indicate network connectivity issues with the collector.` | ||||||||||
| ); | ||||||||||
| this.buffer.shift(); // Remove oldest segment | ||||||||||
| } | ||||||||||
|
|
||||||||||
| this.buffer.push(segment); | ||||||||||
| this.timeout?.ref(); | ||||||||||
| }); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| emitter.on('segment-finished', this.segmentFinishedListener); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| get isConnected(): boolean { | ||||||||||
|
|
@@ -107,4 +122,22 @@ export default class TraceReportClient implements Client { | |||||||||
| this.reportFunction(resolve); | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| destroy(): void { | ||||||||||
| // Clean up event listener to prevent memory leak | ||||||||||
| if (this.segmentFinishedListener) { | ||||||||||
| emitter.off('segment-finished', this.segmentFinishedListener); | ||||||||||
| } | ||||||||||
|
Comment on lines
+128
to
+130
|
||||||||||
| if (this.segmentFinishedListener) { | |
| emitter.off('segment-finished', this.segmentFinishedListener); | |
| } | |
| emitter.off('segment-finished', this.segmentFinishedListener); |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition if destroy() is called while segments are being reported. The buffer could be cleared at line 139 while reportFunction() is iterating over it (lines 91-99), potentially causing data loss or unexpected behavior. Consider either waiting for in-flight reports to complete before cleanup, or adding synchronization to prevent concurrent access to the buffer.
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The gRPC client channels should be closed when destroying the client to properly release network resources. The gRPC library provides a close() method on the client to gracefully shutdown the channel. Consider adding this.reporterClient.close() after clearing the timeout and before clearing the buffer to ensure all network connections are properly terminated.
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition if destroy() is called while reportFunction() is executing. The timeout could be cleared while the report is in progress, but then reportFunction() might schedule a new timeout at line 106 after destroy has completed. Consider adding a flag to track if the client is being destroyed and check it before scheduling new timeouts in reportFunction().
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The destroy() method should be idempotent to handle cases where it's called multiple times. Consider adding a flag to track if the client has already been destroyed and return early if it's called again. This prevents potential errors from clearing already-cleared resources and removes event listeners multiple times.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,6 +72,20 @@ class Agent { | |
| }); | ||
| }); | ||
| } | ||
|
|
||
| destroy(): void { | ||
| if (this.protocol === null) { | ||
| logger.warn('Trying to destroy() SkyWalking agent which is not started.'); | ||
| return; | ||
| } | ||
|
|
||
| logger.info('Destroying SkyWalking agent and cleaning up resources'); | ||
|
|
||
| // Clean up protocol resources | ||
| this.protocol.destroy?.(); | ||
| this.protocol = null; | ||
|
Comment on lines
+85
to
+86
|
||
| this.started = false; | ||
| } | ||
| } | ||
|
|
||
| export default new Agent(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The gRPC client channel should be closed when destroying the client to properly release network resources. The gRPC library provides a
close()method on the client to gracefully shutdown the channel. Consider addingthis.managementServiceClient.close()after clearing the heartbeat timer to ensure all network connections are properly terminated.