Skip to content

fix: Add graceful subprocess cleanup during shutdown#10909

Merged
Cristhianzl merged 10 commits into
mainfrom
cz/add-gracefully-mcp-clean-main
Dec 12, 2025
Merged

fix: Add graceful subprocess cleanup during shutdown#10909
Cristhianzl merged 10 commits into
mainfrom
cz/add-gracefully-mcp-clean-main

Conversation

@Cristhianzl
Copy link
Copy Markdown
Member

@Cristhianzl Cristhianzl commented Dec 5, 2025

This pull request introduces a robust mechanism for cleaning up MCP server subprocesses during the shutdown phase of Langflow. The main goal is to ensure all MCP-related child and orphaned processes are properly terminated, preventing resource leaks and zombie processes. The cleanup logic is executed at the very beginning of the shutdown sequence for reliability and works on macOS and Linux platforms.

Graceful MCP subprocess cleanup:

  • Added a new utility module langflow/utils/mcp_cleanup.py that provides the cleanup_mcp_sessions async function to terminate MCP server subprocesses during shutdown, including fallback logic to forcibly kill lingering processes using psutil on Unix systems.
  • Integrated cleanup_mcp_sessions into the shutdown sequence in langflow/main.py, ensuring it runs before any other shutdown logic for maximum reliability. [1] [2]

mcp-graceful-shutdown-testing.md

Summary by CodeRabbit

  • Chores

    • Enhanced application shutdown process to ensure background services are properly cleaned up.
    • Implemented graceful termination with fallback mechanisms for edge cases.
  • Tests

    • Added comprehensive test coverage for shutdown cleanup functionality.

✏️ Tip: You can customize this high-level summary in your review settings.

…ppress to improve code readability

test(mcp_cleanup.py): update tests to use context manager for patching to enhance clarity and maintainability
@Cristhianzl Cristhianzl self-assigned this Dec 5, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 5, 2025

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

This PR introduces cleanup utilities for MCP (Model Context Protocol) sessions in Langflow. The main application shutdown flow is enhanced to eagerly terminate MCP sessions before other cleanup steps, with graceful fallback to process termination on Unix systems. Comprehensive unit tests validate all cleanup scenarios.

Changes

Cohort / File(s) Summary
Application Shutdown Enhancement
src/backend/base/langflow/main.py
Added import for cleanup_mcp_sessions and integrated MCP session cleanup into the application lifespan finally block to ensure MCP subprocesses terminate before other shutdown logic.
MCP Cleanup Utilities
src/backend/base/langflow/utils/mcp_cleanup.py
New module providing asynchronous cleanup_mcp_sessions() entrypoint that attempts graceful termination via MCPSessionManager cache, with fallback to Unix-specific process termination. Includes helper functions to terminate child and orphaned MCP processes using psutil, with platform guards and comprehensive error handling.
Unit Tests
src/backend/tests/unit/utils/test_mcp_cleanup.py
Added comprehensive unit tests covering cleanup_mcp_sessions behavior, session manager interactions, platform-specific process termination, exception handling, and integration scenarios with extensive mocking and error path validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas requiring attention:
    • mcp_cleanup.py platform-specific code paths (Unix vs. Windows handling)
    • Process management logic using psutil (child and orphaned process termination with timeout handling)
    • Error suppression and resilience patterns across cleanup helpers

Possibly related PRs

Suggested labels

bug, size:XL, lgtm

Suggested reviewers

  • edwinjosechittilappilly
  • italojohnny
  • mfortman11
  • ogabrielluiz

Pre-merge checks and finishing touches

✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding graceful subprocess cleanup during shutdown, which is the core purpose of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 93.94% which is sufficient. The required threshold is 80.00%.
Test Coverage For New Implementations ✅ Passed PR includes comprehensive test coverage for new MCP cleanup implementation with dedicated test file testing all functions including public entrypoint and internal helpers, covering both happy paths and extensive error scenarios including platform-specific behavior.
Test Quality And Coverage ✅ Passed Test suite demonstrates exceptional quality with 26 async tests, comprehensive coverage of 5 implementation functions, and extensive error scenario validation.
Test File Naming And Structure ✅ Passed Pull request introduces comprehensive test file with correct backend naming, proper pytest structure, 33+ descriptive async test methods organized into 6 logical test classes covering 13+ edge cases and error scenarios.
Excessive Mock Usage Warning ✅ Passed Test suite demonstrates appropriate mock usage for subprocess management code with 35+ @patch decorators targeting external system dependencies (psutil, platform, logging) that cannot be tested without mocking in CI environments.

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the bug Something isn't working label Dec 5, 2025
@github-actions github-actions Bot added lgtm This PR has been approved by a maintainer bug Something isn't working and removed bug Something isn't working labels Dec 5, 2025
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 5, 2025

Codecov Report

❌ Patch coverage is 97.01493% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 33.03%. Comparing base (7ebccb8) to head (c30e0c7).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/backend/base/langflow/utils/mcp_cleanup.py 96.92% 2 Missing ⚠️

❌ Your project status has failed because the head coverage (39.26%) is below the target coverage (60.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #10909      +/-   ##
==========================================
+ Coverage   32.96%   33.03%   +0.06%     
==========================================
  Files        1387     1388       +1     
  Lines       65472    65539      +67     
  Branches     9680     9680              
==========================================
+ Hits        21586    21648      +62     
- Misses      42788    42793       +5     
  Partials     1098     1098              
Flag Coverage Δ
backend 52.31% <97.01%> (+0.15%) ⬆️
frontend 15.16% <ø> (ø)
lfx 39.26% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/backend/base/langflow/main.py 66.57% <100.00%> (+0.18%) ⬆️
src/backend/base/langflow/utils/mcp_cleanup.py 96.92% <96.92%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (2)
src/backend/tests/unit/utils/test_mcp_cleanup.py (2)

63-71: Import error simulation may not work as intended.

Setting a module to None in sys.modules doesn't raise ImportError on import—it returns None. This test might pass accidentally because accessing attributes on None raises AttributeError, which is caught by contextlib.suppress(Exception), but it doesn't actually test the ImportError path.

Consider using a side effect that actually raises ImportError:

     async def test_cleanup_handles_import_error(self):
         """Test cleanup handles import errors gracefully."""
+        import builtins
+        original_import = builtins.__import__
+
+        def mock_import(name, *args, **kwargs):
+            if name == "lfx.base.mcp.util":
+                raise ImportError("Test import error")
+            return original_import(name, *args, **kwargs)
+
         with (
-            patch.dict("sys.modules", {"lfx.base.mcp.util": None}),
+            patch.object(builtins, "__import__", side_effect=mock_import),
             patch("langflow.utils.mcp_cleanup._kill_mcp_processes", new_callable=AsyncMock) as mock_kill,
         ):
             # Should not raise, should silently continue
             await cleanup_mcp_sessions()
             mock_kill.assert_called_once()

309-326: Consider simplifying the exception-raising mock.

The generator-based pattern property(lambda _: (_ for _ in ()).throw(...)) is creative but obscure. A simpler approach would use PropertyMock:

+        from unittest.mock import PropertyMock
+
         mock_proc = MagicMock()
-        mock_proc.info = property(lambda _: (_ for _ in ()).throw(mock_psutil.AccessDenied))
-
-        # Make info raise AccessDenied
-        type(mock_proc).info = property(lambda _: (_ for _ in ()).throw(mock_psutil.AccessDenied))
+        type(mock_proc).info = PropertyMock(side_effect=mock_psutil.AccessDenied)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a053bd and e385263.

📒 Files selected for processing (3)
  • src/backend/base/langflow/main.py (2 hunks)
  • src/backend/base/langflow/utils/mcp_cleanup.py (1 hunks)
  • src/backend/tests/unit/utils/test_mcp_cleanup.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend_development.mdc)

src/backend/**/*.py: Use FastAPI async patterns with await for async operations in component execution methods
Use asyncio.create_task() for background tasks and implement proper cleanup with try/except for asyncio.CancelledError
Use queue.put_nowait() for non-blocking queue operations and asyncio.wait_for() with timeouts for controlled get operations

Files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
  • src/backend/base/langflow/main.py
  • src/backend/base/langflow/utils/mcp_cleanup.py
src/backend/tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

src/backend/tests/**/*.py: Place backend unit tests in src/backend/tests/ directory, component tests in src/backend/tests/unit/components/ organized by component subdirectory, and integration tests accessible via make integration_tests
Use same filename as component with appropriate test prefix/suffix (e.g., my_component.pytest_my_component.py)
Use the client fixture (FastAPI Test Client) defined in src/backend/tests/conftest.py for API tests; it provides an async httpx.AsyncClient with automatic in-memory SQLite database and mocked environment variables. Skip client creation by marking test with @pytest.mark.noclient
Inherit from the correct ComponentTestBase family class located in src/backend/tests/base.py based on API access needs: ComponentTestBase (no API), ComponentTestBaseWithClient (needs API), or ComponentTestBaseWithoutClient (pure logic). Provide three required fixtures: component_class, default_kwargs, and file_names_mapping
Create comprehensive unit tests for all new backend components. If unit tests are incomplete, create a corresponding Markdown file documenting manual testing steps and expected outcomes
Test both sync and async code paths, mock external dependencies appropriately, test error handling and edge cases, validate input/output behavior, and test component initialization and configuration
Use @pytest.mark.asyncio decorator for async component tests and ensure async methods are properly awaited
Test background tasks using asyncio.create_task() and verify completion with asyncio.wait_for() with appropriate timeout constraints
Test queue operations using non-blocking queue.put_nowait() and asyncio.wait_for(queue.get(), timeout=...) to verify queue processing without blocking
Use @pytest.mark.no_blockbuster marker to skip the blockbuster plugin in specific tests
For database tests that may fail in batch runs, run them sequentially using uv run pytest src/backend/tests/unit/test_database.py r...

Files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
**/test_*.py

📄 CodeRabbit inference engine (Custom checks)

**/test_*.py: Review test files for excessive use of mocks that may indicate poor test design - check if tests have too many mock objects that obscure what's actually being tested
Warn when mocks are used instead of testing real behavior and interactions, and suggest using real objects or test doubles when mocks become excessive
Ensure mocks are used appropriately for external dependencies only, not for core logic
Backend test files should follow the naming convention test_*.py with proper pytest structure
Test files should have descriptive test function names that explain what is being tested
Tests should be organized logically with proper setup and teardown
Consider including edge cases and error conditions for comprehensive test coverage
Verify tests cover both positive and negative scenarios where appropriate
For async functions in backend tests, ensure proper async testing patterns are used with pytest
For API endpoints, verify both success and error response testing

Files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
🧠 Learnings (6)
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Create comprehensive unit tests for all new backend components. If unit tests are incomplete, create a corresponding Markdown file documenting manual testing steps and expected outcomes

Applied to files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Test both sync and async code paths, mock external dependencies appropriately, test error handling and edge cases, validate input/output behavior, and test component initialization and configuration

Applied to files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Use async fixtures with proper cleanup using try/finally blocks to ensure resources are properly released after tests complete

Applied to files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Use `monkeypatch` fixture to mock internal functions for testing error handling scenarios; validate error status codes and error message content in responses

Applied to files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Each test should have a clear docstring explaining its purpose; complex test setups should be commented; mock usage should be documented; expected behaviors should be explicitly stated

Applied to files:

  • src/backend/tests/unit/utils/test_mcp_cleanup.py
📚 Learning: 2025-11-24T19:46:09.104Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/backend_development.mdc:0-0
Timestamp: 2025-11-24T19:46:09.104Z
Learning: Applies to src/backend/base/langflow/components/**/__init__.py : Update `__init__.py` with alphabetically sorted imports when adding new components

Applied to files:

  • src/backend/base/langflow/main.py
🧬 Code graph analysis (3)
src/backend/tests/unit/utils/test_mcp_cleanup.py (1)
src/backend/base/langflow/utils/mcp_cleanup.py (5)
  • _kill_mcp_processes (44-65)
  • _terminate_child_mcp_processes (68-82)
  • _terminate_orphaned_mcp_processes (85-101)
  • _try_terminate_mcp_process (104-125)
  • cleanup_mcp_sessions (21-41)
src/backend/base/langflow/main.py (1)
src/backend/base/langflow/utils/mcp_cleanup.py (1)
  • cleanup_mcp_sessions (21-41)
src/backend/base/langflow/utils/mcp_cleanup.py (2)
src/lfx/src/lfx/base/mcp/util.py (2)
  • MCPSessionManager (459-1018)
  • cleanup_all (956-993)
src/backend/tests/unit/base/mcp/test_mcp_util.py (1)
  • session_manager (28-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: Lint Backend / Run Mypy (3.10)
  • GitHub Check: Lint Backend / Run Mypy (3.11)
  • GitHub Check: Lint Backend / Run Mypy (3.12)
  • GitHub Check: Run Frontend Tests / Determine Test Suites and Shard Distribution
  • GitHub Check: Test Docker Images / Test docker images
  • GitHub Check: Run Backend Tests / LFX Tests - Python 3.10
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 2
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 5
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 3
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 1
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 4
  • GitHub Check: Run Backend Tests / Integration Tests - Python 3.10
  • GitHub Check: Test Starter Templates
  • GitHub Check: Optimize new Python code in this PR
  • GitHub Check: Update Component Index
  • GitHub Check: Run Ruff Check and Format
  • GitHub Check: Update Starter Projects
🔇 Additional comments (13)
src/backend/base/langflow/main.py (2)

49-49: LGTM!

The import is correctly placed with other langflow utility imports.


318-320: Well-placed cleanup for maximum reliability.

Positioning the MCP cleanup at the very beginning of the finally block ensures subprocesses are terminated even if subsequent shutdown steps are interrupted. The comment clearly explains the rationale, and the function's internal contextlib.suppress(Exception) wrappers ensure this won't block other shutdown logic.

src/backend/base/langflow/utils/mcp_cleanup.py (6)

1-18: LGTM!

Clean module structure with proper docstring documenting platform constraints and appropriate use of TYPE_CHECKING for the optional psutil dependency.


21-41: Well-structured two-phase cleanup.

The separation of graceful session manager cleanup (first phase) and fallback process termination (second phase) in independent contextlib.suppress blocks ensures both are attempted regardless of individual failures. Lazy imports inside the function body correctly handle the case where dependencies may not be available.


44-65: LGTM!

Proper platform guard for Unix-only functionality, graceful handling of missing psutil, and appropriate logging strategy (only when processes are actually killed).


68-82: LGTM!

Recursive child enumeration correctly captures nested MCP subprocesses, and NoSuchProcess handling covers race conditions during shutdown.


85-101: LGTM!

The ppid == 1 check correctly identifies orphaned processes on Unix systems (those reparented to init/systemd). Exception handling covers the common race conditions during process enumeration.


104-125: LGTM!

The graceful termination pattern (SIGTERM → wait with timeout → SIGKILL) follows best practices. The cmdline matching is intentionally broad to catch MCP processes regardless of exact invocation pattern, and edge cases (empty/None cmdline) are handled correctly.

src/backend/tests/unit/utils/test_mcp_cleanup.py (5)

95-186: Good coverage of the _kill_mcp_processes function.

The tests properly verify platform guards, both termination paths, and conditional logging. Note that the sys.modules patching at line 108 has the same limitation mentioned earlier regarding import error simulation.


189-234: LGTM!

Tests correctly verify child process termination and the NoSuchProcess edge case. The patching of _try_terminate_mcp_process isolates the function under test appropriately.


329-472: Excellent test coverage.

Comprehensive testing of all code paths including the graceful termination sequence (terminate → wait → kill on timeout), exception handling for various psutil errors, and edge cases with empty/None cmdlines. Each test has a clear docstring explaining its purpose. Based on learnings, this follows best practices for test documentation.


475-536: Good integration-style tests.

These tests properly validate the complete cleanup flow including error resilience. The silent failure test at lines 516-536 is particularly valuable for ensuring shutdown reliability—it confirms that errors during cleanup won't produce unexpected log noise.


1-15: LGTM!

Good test structure with module-level pytestmark for async tests and appropriate imports. Testing the private helper functions is justified here as they contain critical cleanup logic that warrants direct verification.

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 12, 2025

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 16%
16.42% (4606/28041) 9.73% (2106/21644) 10.76% (664/6166)

Unit Test Results

Tests Skipped Failures Errors Time
1803 0 💤 0 ❌ 0 🔥 24.519s ⏱️

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@Cristhianzl Cristhianzl added this pull request to the merge queue Dec 12, 2025
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Dec 12, 2025
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Dec 12, 2025
@Cristhianzl Cristhianzl added this pull request to the merge queue Dec 12, 2025
Merged via the queue into main with commit f5e68c2 Dec 12, 2025
56 of 58 checks passed
@Cristhianzl Cristhianzl deleted the cz/add-gracefully-mcp-clean-main branch December 12, 2025 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants