Skip to content

2256_chore/fix-gatewayservice-uninitialized-services#2514

Merged
crivetimihai merged 1 commit intomainfrom
2256_chore/fix-gatewayservice-uninitialized-services
Jan 29, 2026
Merged

2256_chore/fix-gatewayservice-uninitialized-services#2514
crivetimihai merged 1 commit intomainfrom
2256_chore/fix-gatewayservice-uninitialized-services

Conversation

@Nayana-R-Gowda
Copy link
Copy Markdown
Collaborator

@Nayana-R-Gowda Nayana-R-Gowda commented Jan 27, 2026

Signed-off-by: NAYANAR nayana.r7813@gmail.com

Closes #2256

This change refactors GatewayService to reuse the globally-initialized service singletons (ToolService, PromptService, ResourceService) instead of creating private, uninitialized instances.
By importing the module-level services created and initialized in mcpgateway.main, all gateway operations now share the same EventService/Redis client.
This ensures events such as activate/deactivate propagate correctly across workers and reach Redis subscribers.
The fix resolves silent event drops caused by missing initialize() calls on locally constructed services.
Lazy imports are used to avoid circular dependencies while preserving correct initialization order.
Cross-worker UI updates and subscriber notifications now behave as intended.

Teating

python - <<'PY'import asynciofrom mcpgateway.services.tool_service import tool_serviceasync def test():    async def sub():        async for ev in tool_service.subscribe_events():            print('RECEIVED_EVENT', ev)            return    async def pub():        await asyncio.sleep(0.2)        await tool_service._publish_event({'type': 'test_event', 'data': {'ok': True}})    await asyncio.gather(sub(), pub())asyncio.run(test())PY

python - <<'PY'import mcpgateway.main as mainfrom mcpgateway.services.tool_service import tool_service as svcprint('tool_service is main.tool_service ->', svc is main.tool_service)PY

@Nayana-R-Gowda Nayana-R-Gowda marked this pull request as draft January 27, 2026 07:19
@Nayana-R-Gowda Nayana-R-Gowda marked this pull request as ready for review January 27, 2026 09:48
@crivetimihai crivetimihai force-pushed the 2256_chore/fix-gatewayservice-uninitialized-services branch from 73ad5a0 to 5dc5bbb Compare January 29, 2026 02:31
@crivetimihai
Copy link
Copy Markdown
Member

Review and Fixes Applied

Rebased onto main (no conflicts) and squashed all commits into one clean commit.

Issues Found and Fixed

  1. Bug in main.py: Was still creating root_service = RootService() instead of importing the module-level singleton from root_service.py. Fixed by importing root_service from the module.

  2. Dead code in export_service.py: The try/except blocks around self.server_service = server_service and self.root_service = root_service were unreachable - the imports already happened at the top of __init__, so the variables are always defined. Removed the unnecessary fallback code.

  3. Unused imports: Removed RootService class import from main.py and ServerService class import from export_service.py that were no longer needed.

  4. Redundant comments: Cleaned up duplicate/confusing comments.

Verification

  • ✅ All 5558 tests pass
  • ✅ Pylint: 10.00/10
  • ✅ Flake8: No issues
  • ✅ Bandit: No security issues
  • ✅ Singleton identity verified: tool_service is main.tool_service -> True

Design Review

The approach is sound:

  • Module-level singletons ensure all services share the same initialized EventService/Redis client
  • Lazy imports in __init__ avoid circular import issues
  • Pattern is consistent with standard Python singleton semantics

Refactors GatewayService, ExportService, ImportService, and A2AService to use
globally-initialized service singletons (ToolService, PromptService,
ResourceService, ServerService, RootService, GatewayService) instead of
creating private, uninitialized instances.

Uses lazy singleton pattern with __getattr__ to avoid import-time instantiation
when only exception classes are imported. This ensures services are created
after logging/plugin setup is complete.

By importing the module-level services, all gateway operations now share the
same EventService/Redis client. This ensures events such as activate/deactivate
propagate correctly across workers and reach Redis subscribers.

Changes:
- Add lazy singleton pattern using __getattr__ to service modules
- Update main.py to import singletons instead of instantiating services
- Update GatewayService.__init__ to use lazy imports of singletons
- Update ExportService.__init__ to use lazy imports of singletons
- Update ImportService.__init__ to use lazy imports of singletons
- Update A2AService methods to use tool_service singleton
- Update tests to patch singleton methods instead of class instantiation
- Add pylint disables for no-name-in-module (due to __getattr__)

The fix resolves silent event drops caused by missing initialize() calls on
locally constructed services. Cross-worker UI updates and subscriber
notifications now behave as intended.

Closes #2256

Signed-off-by: NAYANAR <nayana.r7813@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai crivetimihai force-pushed the 2256_chore/fix-gatewayservice-uninitialized-services branch from 5dc5bbb to 8e964d2 Compare January 29, 2026 08:49
@crivetimihai
Copy link
Copy Markdown
Member

Additional Review: Import-Time Instantiation Fix

After reviewing feedback about import-time side effects, I've updated the implementation to use lazy singleton pattern with __getattr__:

Issues Identified

  1. Import-time instantiation: The original module-level singletons (tool_service = ToolService()) were instantiated when exception classes were imported, before logging/plugin setup
  2. Test mocking: Tests patching ToolService class no longer intercepted the singleton

Solution Applied

Changed from eager instantiation:

# OLD - instantiated at module import time
tool_service = ToolService()

To lazy singleton pattern:

# NEW - instantiated on first access
_tool_service_instance = None

def __getattr__(name: str):
    global _tool_service_instance
    if name == "tool_service":
        if _tool_service_instance is None:
            _tool_service_instance = ToolService()
        return _tool_service_instance
    raise AttributeError(...)

Benefits

  • Singletons created only when first accessed, not at import time
  • Importing exception classes no longer triggers service instantiation
  • Services initialized after logging/plugin manager setup
  • Same singleton identity preserved (tool_service is main.tool_service)

Test Updates

Updated tests in test_a2a_query_param_auth.py and test_row_level_locking.py to use proper singleton mocking:

# OLD - patched class that was no longer used
with patch("mcpgateway.services.a2a_service.ToolService") as mock:

# NEW - patch method on actual singleton
from mcpgateway.services.tool_service import tool_service
with patch.object(tool_service, "create_tool_from_a2a_agent", ...):

Verification

  • ✅ All 5558 tests pass
  • ✅ Pylint: 10.00/10
  • ✅ Flake8: No issues
  • ✅ Bandit: No security issues

@crivetimihai crivetimihai merged commit e852508 into main Jan 29, 2026
53 checks passed
@crivetimihai crivetimihai deleted the 2256_chore/fix-gatewayservice-uninitialized-services branch January 29, 2026 10:24
hughhennelly pushed a commit to hughhennelly/mcp-context-forge that referenced this pull request Feb 8, 2026
IBM#2514)

Refactors GatewayService, ExportService, ImportService, and A2AService to use
globally-initialized service singletons (ToolService, PromptService,
ResourceService, ServerService, RootService, GatewayService) instead of
creating private, uninitialized instances.

Uses lazy singleton pattern with __getattr__ to avoid import-time instantiation
when only exception classes are imported. This ensures services are created
after logging/plugin setup is complete.

By importing the module-level services, all gateway operations now share the
same EventService/Redis client. This ensures events such as activate/deactivate
propagate correctly across workers and reach Redis subscribers.

Changes:
- Add lazy singleton pattern using __getattr__ to service modules
- Update main.py to import singletons instead of instantiating services
- Update GatewayService.__init__ to use lazy imports of singletons
- Update ExportService.__init__ to use lazy imports of singletons
- Update ImportService.__init__ to use lazy imports of singletons
- Update A2AService methods to use tool_service singleton
- Update tests to patch singleton methods instead of class instantiation
- Add pylint disables for no-name-in-module (due to __getattr__)

The fix resolves silent event drops caused by missing initialize() calls on
locally constructed services. Cross-worker UI updates and subscriber
notifications now behave as intended.

Closes IBM#2256

Signed-off-by: NAYANAR <nayana.r7813@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: hughhennnelly <hughhennelly06@gmail.com>
kcostell06 pushed a commit to kcostell06/mcp-context-forge that referenced this pull request Feb 24, 2026
IBM#2514)

Refactors GatewayService, ExportService, ImportService, and A2AService to use
globally-initialized service singletons (ToolService, PromptService,
ResourceService, ServerService, RootService, GatewayService) instead of
creating private, uninitialized instances.

Uses lazy singleton pattern with __getattr__ to avoid import-time instantiation
when only exception classes are imported. This ensures services are created
after logging/plugin setup is complete.

By importing the module-level services, all gateway operations now share the
same EventService/Redis client. This ensures events such as activate/deactivate
propagate correctly across workers and reach Redis subscribers.

Changes:
- Add lazy singleton pattern using __getattr__ to service modules
- Update main.py to import singletons instead of instantiating services
- Update GatewayService.__init__ to use lazy imports of singletons
- Update ExportService.__init__ to use lazy imports of singletons
- Update ImportService.__init__ to use lazy imports of singletons
- Update A2AService methods to use tool_service singleton
- Update tests to patch singleton methods instead of class instantiation
- Add pylint disables for no-name-in-module (due to __getattr__)

The fix resolves silent event drops caused by missing initialize() calls on
locally constructed services. Cross-worker UI updates and subscriber
notifications now behave as intended.

Closes IBM#2256

Signed-off-by: NAYANAR <nayana.r7813@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
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.

[CHORE]: GatewayService creates uninitialized service instances (ToolService, PromptService, ResourceService)

2 participants