Skip to content

fix(mcp): prevent memory leak from uncleared interval timer in MCPCir… - #77

Merged
murdore merged 1 commit into
juspay:releasefrom
sishir2001:fix/memory-leak-from-uncleared-interval-timer-in-MCPCircuitBreaker
Aug 18, 2025
Merged

murdore merged 1 commit into
juspay:releasefrom
sishir2001:fix/memory-leak-from-uncleared-interval-timer-in-MCPCircuitBreaker

Conversation

@sishir2001

@sishir2001 sishir2001 commented Aug 18, 2025 •

Copy link
Copy Markdown

Added timer cleanup to MCPCircuitBreaker to prevent memory leaks:

  • Store interval timer reference in cleanupTimer property
  • Implement destroy() method to clear timer and resources
  • Update removeBreaker() to call destroy() on removal
  • Add destroyAll() method for bulk cleanup

Issue:
The interval timer created in constructor was never cleared, causing memory leaks and preventing Node.js processes from exiting cleanly.

Pull Request

Description

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🧹 Code refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧪 Test coverage improvement
  • 🔧 Build/CI configuration change

Related Issues

  • Fixes #
  • Related to #

Changes Made

AI Provider Impact

  • OpenAI
  • Anthropic
  • Google AI/Vertex
  • AWS Bedrock
  • Azure OpenAI
  • Hugging Face
  • Ollama
  • Mistral
  • All providers
  • No provider-specific changes

Component Impact

  • CLI
  • SDK
  • MCP Integration
  • Streaming
  • Tool Calling
  • Configuration
  • Documentation
  • Tests

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • E2E tests added/updated
  • Manual testing performed
  • All existing tests pass

Test Environment

  • OS:
  • Node.js version:
  • Package manager:

Performance Impact

  • No performance impact
  • Performance improvement
  • Minor performance impact (acceptable)
  • Significant performance impact (needs discussion)

Breaking Changes

Screenshots/Demo

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

Summary by CodeRabbit

  • New Features
    • Added the ability to cleanly destroy all circuit breakers at once during shutdown.
  • Bug Fixes
    • Ensures timers and listeners are properly cleared when removing individual breakers, reducing potential memory/resource leaks.
  • Refactor
    • Introduced an explicit lifecycle for circuit breakers to support reliable cleanup without changing runtime behavior.
  • Chores
    • Improved logging around removal and destruction to clarify cleanup actions and shutdown progress.

@coderabbitai

coderabbitai Bot commented Aug 18, 2025 •

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds explicit cleanup lifecycle to circuit breakers: stores cleanup timer, introduces MCPCircuitBreaker.destroy() for timers/listeners/history cleanup, updates CircuitBreakerManager.removeBreaker() to destroy before removal, and adds CircuitBreakerManager.destroyAll() to destroy all breakers and clear the map. Logging updated to reflect cleanup/destruction.

Changes

Cohort / File(s) Summary
Circuit breaker lifecycle & cleanup
src/lib/mcp/mcpCircuitBreaker.ts
Store interval in this.cleanupTimer; add MCPCircuitBreaker.destroy() to clear timer, listeners, and history; update CircuitBreakerManager.removeBreaker(name) to call destroy() and report existence via boolean; add CircuitBreakerManager.destroyAll() to destroy all and clear map; adjust log messages.

Sequence Diagram(s)

sequenceDiagram
  actor App
  participant Manager as CircuitBreakerManager
  participant Breaker as MCPCircuitBreaker
  participant Timer as Cleanup Timer

  App->>Manager: removeBreaker(name)
  alt breaker exists
    Manager->>Breaker: destroy()
    Breaker-->>Timer: clearInterval(cleanupTimer)
    Breaker->>Breaker: removeAllListeners(), clearHistory()
    Manager->>Manager: delete from map
    Manager-->>App: true
  else
    Manager-->>App: false
  end
Loading
sequenceDiagram
  actor App
  participant Manager as CircuitBreakerManager
  participant Breaker* as All MCPCircuitBreaker instances
  participant Timer as Cleanup Timers

  App->>Manager: destroyAll()
  loop for each breaker
    Manager->>Breaker*: destroy()
    Breaker*-->>Timer: clearInterval()
    Breaker*->>Breaker*: removeAllListeners(), clearHistory()
  end
  Manager->>Manager: clear map
  Manager-->>App: done
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I flip the switches, hop with cheer,
Now timers stop, the burrows clear.
Breakers snooze, their watchers gone,
One thump—cleanup—and we’re done.
When dusk calls “all,” I tidy tight—
Map is empty, stars alight.
🐇✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/lib/mcp/mcpCircuitBreaker.ts (5)

128-130: Use a cross-runtime-safe timer type for better compatibility

Typing Node timers as NodeJS.Timeout ties you to Node’s ambient types. Prefer ReturnType to work across Node/browser TS configs and avoid type friction.

Apply this diff:

-  // Store the cleanup timer reference for proper cleanup
-  private cleanupTimer?: NodeJS.Timeout;
+  // Store the cleanup timer reference for proper cleanup
+  private cleanupTimer?: ReturnType<typeof setInterval>;
+
+  // Optional: mark lifecycle state to prevent use-after-destroy
+  private destroyed = false;

147-149: Unref the maintenance interval so it doesn’t keep the process alive

Storing the reference is great. In Node, calling unref() prevents this interval from keeping the event loop open if the rest of the app is done. This complements destroy() and further reduces shutdown friction.

Apply this diff:

-    // Clean up old call records periodically - now storing the timer reference
-    this.cleanupTimer = setInterval(() => this.cleanupCallHistory(), 60000);
+    // Clean up old call records periodically - now storing the timer reference
+    this.cleanupTimer = setInterval(() => this.cleanupCallHistory(), 60000);
+    // Do not keep the event loop alive only because of this maintenance timer (Node.js)
+    if (typeof (this.cleanupTimer as any).unref === "function") {
+      (this.cleanupTimer as any).unref();
+    }

Separately: your timeoutPromise used in Promise.race cannot be canceled and may still reject after the race settles, risking unhandled rejection warnings. Consider a cancelable timeout or AbortController. I can provide a small helper if desired.


434-449: Solid destroy() implementation; make it idempotent and guard subsequent usage

This is the right fix for the leak. Two small hardening tweaks:

  • Mark the breaker as destroyed to avoid use-after-destroy calls.
  • Optionally, throw if execute() is called after destroy().

Apply this diff to mark destruction:

   destroy(): void {
     // Clear the interval timer to prevent memory leaks
     if (this.cleanupTimer) {
       clearInterval(this.cleanupTimer);
       this.cleanupTimer = undefined;
       mcpLogger.debug(`[CircuitBreaker:${this.name}] Cleanup timer cleared`);
     }

     // Clear any remaining event listeners
     this.removeAllListeners();

     // Clear call history to free memory
     this.callHistory = [];

+    // Mark as destroyed to prevent use-after-destroy
+    this.destroyed = true;
+
     mcpLogger.debug(`[CircuitBreaker:${this.name}] Destroyed and cleaned up`);
   }

If you want to prevent calls post-destroy, add a guard in execute (outside changed lines):

if (this.destroyed) {
  throw new Error(`Circuit breaker '${this.name}' has been destroyed`);
}

Optional test idea (Jest/Vi with fake timers): assert that destroy() clears the interval and no further cleanup ticks occur.


571-578: destroyAll: continue on errors to ensure full shutdown

If a single breaker’s destroy throws, the loop aborts and the map may not be cleared. Catch per-breaker, continue, then clear the map.

Apply this diff:

   destroyAll(): void {
-    for (const breaker of this.breakers.values()) {
-      breaker.destroy();
-    }
-    this.breakers.clear();
+    for (const [name, breaker] of this.breakers) {
+      try {
+        breaker.destroy();
+      } catch (err) {
+        mcpLogger.error(
+          `[CircuitBreakerManager] Failed to destroy circuit breaker ${name}: ${
+            err instanceof Error ? err.message : String(err)
+          }`,
+        );
+      }
+    }
+    this.breakers.clear();
 
     mcpLogger.info("[CircuitBreakerManager] Destroyed all circuit breakers");
   }

478-493: Harden removeBreaker: isolate destroy errors and confirm no external callers

Wrap breaker.destroy() in a try/catch so that a failing destroy can’t block removal, and always delete the entry in a finally block. We searched the repo for removeBreaker(…) and found no call sites outside of mcpCircuitBreaker.ts—please verify there are no downstream integrations that depend on a thrown error or a void return.

Proposed diff in src/lib/mcp/mcpCircuitBreaker.ts (lines 478–493):

   removeBreaker(name: string): boolean {
-    const breaker = this.breakers.get(name);
-    if (breaker) {
-      // Destroy the breaker to clean up its timer and resources
-      breaker.destroy();
-      this.breakers.delete(name);
+    const breaker = this.breakers.get(name);
+    if (breaker) {
+      // Destroy the breaker to clean up its timer and resources
+      try {
+        breaker.destroy();
+      } catch (err) {
+        mcpLogger.error(
+          `[CircuitBreakerManager] Error destroying circuit breaker ${name}: ${
+            err instanceof Error ? err.message : String(err)
+          }`,
+        );
+      } finally {
+        this.breakers.delete(name);
+      }
 
       mcpLogger.debug(
         `[CircuitBreakerManager] Removed and cleaned up circuit breaker: ${name}`,
       );
       return true;
     }
     return false;
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9314be8 and dfd6a0a.

📒 Files selected for processing (1)
  • src/lib/mcp/mcpCircuitBreaker.ts (5 hunks)

@murdore
murdore requested a review from Copilot August 18, 2025 16:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a memory leak in the MCPCircuitBreaker class by implementing proper cleanup of interval timers. The interval timer created in the constructor was never cleared, preventing Node.js processes from exiting cleanly and causing memory leaks.

  • Added timer reference storage and cleanup mechanisms to MCPCircuitBreaker
  • Implemented proper resource destruction methods for individual and bulk cleanup
  • Updated the CircuitBreakerManager to call cleanup methods when removing breakers

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

private lastFailureTime = 0;
private halfOpenCalls = 0;
private lastStateChange = new Date();
// Store the cleanup timer reference for proper cleanup

Copilot AI Aug 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a JSDoc comment to document the purpose of the cleanupTimer property, explaining that it holds the reference to the interval timer for proper cleanup.

Suggested change
// Store the cleanup timer reference for proper cleanup
/**
* Holds the reference to the interval timer used for periodic cleanup of old call records.
* This allows proper cleanup of the timer when the circuit breaker is disposed.
*/

Copilot uses AI. Check for mistakes.
/**
* Destroy the circuit breaker and clean up resources
* This method should be called when the circuit breaker is no longer needed
* to prevent memory leaks from the cleanup timer

Copilot AI Aug 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destroy() method should have a JSDoc comment explaining when and why it should be called, its side effects (clearing timers, listeners, and memory), and that the circuit breaker becomes unusable after calling this method.

Suggested change
* to prevent memory leaks from the cleanup timer
* Destroys the circuit breaker and cleans up all associated resources.
*
* When and why to call:
* - Call this method when the circuit breaker is no longer needed, such as during application shutdown or when the breaker will not be used again.
*
* Side effects:
* - Clears the internal cleanup timer to prevent memory leaks.
* - Removes all event listeners attached to this circuit breaker instance.
* - Clears the call history to free up memory.
*
* After calling this method:
* - The circuit breaker instance becomes unusable and should not be used for further operations.

Copilot uses AI. Check for mistakes.

/**
* Destroy all circuit breakers and clean up their resources
* This should be called during application shutdown to prevent memory leaks

Copilot AI Aug 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destroyAll() method should have a JSDoc comment documenting its intended use during application shutdown and that it renders all managed circuit breakers unusable.

Suggested change
* This should be called during application shutdown to prevent memory leaks
* Destroys all managed circuit breakers and cleans up their resources.
* Intended to be called during application shutdown to prevent memory leaks.
* After calling this method, all managed circuit breakers become unusable.

Copilot uses AI. Check for mistakes.
…cuitBreaker

Added timer cleanup to MCPCircuitBreaker to prevent memory leaks:
- Store interval timer reference in cleanupTimer property
- Implement destroy() method to clear timer and resources
- Update removeBreaker() to call destroy() on removal
- Add destroyAll() method for bulk cleanup

Issue:
The interval timer created in constructor was never cleared, causing
memory leaks and preventing Node.js processes from exiting cleanly.
@murdore
murdore force-pushed the fix/memory-leak-from-uncleared-interval-timer-in-MCPCircuitBreaker branch from dfd6a0a to ff6bbf4 Compare August 18, 2025 17:27
@murdore
murdore merged commit 1f2ae47 into juspay:release Aug 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants