diff --git a/README.md b/README.md index 3467aa8..7ab7bf4 100644 --- a/README.md +++ b/README.md @@ -64,18 +64,18 @@ curl -X POST http://localhost:8004/services/ai_svc/connection-method \ ## 📚 Documentation -Complete documentation is available in our [Wiki](../../wiki): +Complete documentation is available in our [Wiki](wiki/): ### English Documentation -- [Getting Started](../../wiki/GETTING_STARTED) -- [Architecture Guide](../../wiki/ARCHITECTURE) -- [API Reference](../../wiki/API) -- [Development Guide](../../wiki/DEVELOPMENT) -- [Production Deployment](../../wiki/PRODUCTION) +- [Getting Started](wiki/en/Quick-Start.md) +- [Architecture Guide](wiki/en/Architecture.md) +- [API Reference](wiki/en/API.md) +- [Development Guide](wiki/en/Development.md) +- [Production Deployment](wiki/en/Production.md) ### Persian Documentation -- [راهنمای شروع](../../wiki/راهنمای-شروع) -- [معماری سیستم](../../wiki/معماری-سیستم) +- [راهنمای شروع](wiki/fa/راهنمای-شروع.md) +- [معماری سیستم](wiki/fa/معماری-سیستم.md) ## 🚀 Deployment diff --git a/services/ai_svc/tests/test_main.py b/services/ai_svc/tests/test_main.py new file mode 100644 index 0000000..c705ab2 --- /dev/null +++ b/services/ai_svc/tests/test_main.py @@ -0,0 +1,111 @@ + +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch, AsyncMock + +# Make sure the app can be imported +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from main import app, usage_storage, AIResponse + +client = TestClient(app) + +# A mock service token for testing +SERVICE_TOKEN = "test_service_token" +os.environ["SERVICE_TOKEN"] = SERVICE_TOKEN +HEADERS = {"X-Service-Token": SERVICE_TOKEN} + +@pytest.fixture(autouse=True) +def clear_storage(): + """Clear in-memory usage storage before each test.""" + usage_storage.clear() + +@pytest.fixture +def mock_openai(): + """Fixture to mock the call_openai_api function.""" + with patch("main.call_openai_api", new_callable=AsyncMock) as mock_api: + yield mock_api + +def test_health_check(): + response = client.get("/health") + assert response.status_code == 200 + assert response.json()["status"] == "healthy" + assert response.json()["service"] == "ai_svc" + +def test_call_ai_success(mock_openai): + # Arrange + mock_openai.return_value = { + "result": "Mocked AI response", + "model": "gpt-3.5-turbo", + "tokens_used": 50, + "mock": True + } + request_data = { + "prompt": "Test prompt", + "content": "Test content", + "user_id": 1 + } + + # Act + response = client.post("/call", json=request_data, headers=HEADERS) + + # Assert + assert response.status_code == 200 + json_response = response.json() + assert json_response["result"] == "Mocked AI response" + mock_openai.assert_called_once() + +def test_call_ai_quota_exceeded(mock_openai): + # Arrange + from main import UsageStats + usage_storage[1] = UsageStats( + user_id=1, + total_requests=10, + total_tokens=1000, + requests_today=10, + tokens_today=1000, + quota_remaining=0 + ) + request_data = { + "prompt": "Test prompt", + "content": "Test content", + "user_id": 1 + } + + # Act + response = client.post("/call", json=request_data, headers=HEADERS) + + # Assert + assert response.status_code == 429 + mock_openai.assert_not_called() + +@patch("main.call_ai", new_callable=AsyncMock) +def test_summarize_content(mock_call_ai): + # Arrange + mock_response = AIResponse( + result="Mocked summary", + model_used="gpt-3.5-turbo-mock", + tokens_used=25, + processing_time=0.1, + metadata={"test": True} + ) + mock_call_ai.return_value = mock_response + + request_data = { + "content": "This is a long text to be summarized.", + "max_length": 50 + } + + # Act + response = client.post("/summarize", json=request_data, headers=HEADERS) + + # Assert + assert response.status_code == 200 + assert response.json()['result'] == "Mocked summary" + mock_call_ai.assert_called_once() + +def test_invalid_service_token(): + response = client.post("/call", json={}, headers={"X-Service-Token": "invalid_token"}) + assert response.status_code == 401 diff --git a/services/channel_mgr_svc/tests/test_main.py b/services/channel_mgr_svc/tests/test_main.py new file mode 100644 index 0000000..9cc01c7 --- /dev/null +++ b/services/channel_mgr_svc/tests/test_main.py @@ -0,0 +1,91 @@ + +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch, AsyncMock + +# Make sure the app can be imported +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from main import app + +client = TestClient(app) + +# A mock service token for testing +SERVICE_TOKEN = "test_service_token" +os.environ["SERVICE_TOKEN"] = SERVICE_TOKEN +HEADERS = {"X-Service-Token": SERVICE_TOKEN} + +@pytest.fixture(autouse=True) +def manage_background_task(): + """Fixture to start and stop the background task for tests.""" + with patch("main.feed_monitoring_loop", new_callable=AsyncMock): + yield + +@pytest.fixture(autouse=True) +def clear_storage(): + """Clear in-memory storage before each test and after.""" + from main import channels_storage, feeds_storage + + channels_storage.clear() + feeds_storage.clear() + + globals_to_reset = {'channel_id_counter': 1, 'feed_id_counter': 1} + for var, value in globals_to_reset.items(): + if hasattr(sys.modules['main'], var): + setattr(sys.modules['main'], var, value) + +def test_health_check(): + response = client.get("/health") + assert response.status_code == 200 + assert response.json()["status"] == "healthy" + +@pytest.mark.asyncio +@patch("main.test_rss_feed", new_callable=AsyncMock) +async def test_create_feed_invalid_rss(mock_test_rss_feed): + # Arrange + channel_data = {"telegram_id": 12345, "title": "Test Channel", "owner_id": 1} + response = client.post("/channels", json=channel_data, headers=HEADERS) + assert response.status_code == 200 + channel_id = response.json()["id"] + + mock_test_rss_feed.return_value = {"valid": False} + + # Act + feed_data = {"url": "http://invalid-rss.com", "channel_id": channel_id} + response = client.post("/feeds", json=feed_data, headers=HEADERS) + + # Assert + assert response.status_code == 400 + assert "Invalid RSS feed" in response.json()["detail"] + +def test_create_feed_for_nonexistent_channel(): + feed_data = {"url": "http://example.com/rss", "channel_id": 999} + response = client.post("/feeds", json=feed_data, headers=HEADERS) + assert response.status_code == 404 + +@patch("main.check_feed_for_updates", new_callable=AsyncMock) +def test_check_feed_now_endpoint(mock_check_feed): + # Arrange + channel_data = {"telegram_id": 123, "title": "Test", "owner_id": 1} + channel_res = client.post("/channels", json=channel_data, headers=HEADERS) + assert channel_res.status_code == 200 + channel_id = channel_res.json()["id"] + + with patch("main.test_rss_feed", new_callable=AsyncMock) as mock_test_feed: + mock_test_feed.return_value = {"valid": True, "title": "Test Feed"} + feed_data = {"url": "http://test.com/rss", "channel_id": channel_id} + feed_res = client.post("/feeds", json=feed_data, headers=HEADERS) + assert feed_res.status_code == 200 + feed_id = feed_res.json()["id"] + + mock_check_feed.return_value = [{"title": "New Post", "link": "http://test.com/post"}] + + # Act + response = client.get(f"/feeds/{feed_id}/check", headers=HEADERS) + + # Assert + assert response.status_code == 200 + assert response.json()["new_items"] == 1 + mock_check_feed.assert_called_once() diff --git a/src/rssbot/core/config.py b/src/rssbot/core/config.py index 8a99015..d7d1287 100644 --- a/src/rssbot/core/config.py +++ b/src/rssbot/core/config.py @@ -5,11 +5,7 @@ from typing import Optional from pydantic import Field -try: - from pydantic_settings import BaseSettings -except ImportError: - # Fallback for older pydantic versions - from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Config(BaseSettings): diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..a1af262 --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,9 @@ + +# Welcome to the RssBot Platform Wiki + +This is the central hub for all documentation related to the RssBot Platform. + +Please select your preferred language to get started: + +- [**English Documentation**](en/Home.md) +- [**مستندات فارسی (Persian Documentation)**](fa/Home.md) diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 4da69a8..32900e7 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -1,56 +1,45 @@ + # 📚 RssBot Platform Wiki ## 🏠 Home -- [**🚀 Main Wiki**](Home) -- [**🇺🇸 English Docs**](en/Home) -- [**🇮🇷 فارسی**](fa/Home) +- [**🚀 Main Wiki**](Home.md) +- [**🇺🇸 English Docs**](en/Home.md) +- [**🇮🇷 فارسی**](fa/Home.md) --- ## 🏁 Getting Started -- [📦 Installation](en/Installation) -- [⚡ Quick Start](en/Quick-Start) -- [⚙️ Configuration](en/Configuration) -- [🤖 First Bot](en/First-Bot) +- [📦 Installation](en/Installation.md) +- [⚡ Quick Start](en/Quick-Start.md) +- [🤖 First Bot](en/First-Bot.md) ## 🏗️ Architecture & Design -- [🏛️ Architecture Overview](en/Architecture) -- [🔍 Service Discovery](en/Service-Discovery) -- [🔀 Connection Methods](en/Connection-Methods) -- [📊 Performance](en/Performance) +- [🏛️ Architecture Overview](en/Architecture.md) +- [🔍 Service Discovery](en/Service-Discovery.md) +- [🔀 Connection Methods](en/Connection-Methods.md) ## 👨‍💻 Development -- [🛠️ Development Setup](en/Development) -- [📚 API Reference](en/API) -- [🧪 Testing Guide](en/Testing) -- [🤝 Contributing](en/Contributing) +- [📚 API Reference](en/API.md) +- [🧪 Testing Guide](en/Testing.md) ## 🚀 Deployment & Ops -- [🏭 Production](en/Production) -- [🐳 Docker Guide](en/Docker) -- [☸️ Kubernetes](en/Kubernetes) -- [📈 Monitoring](en/Monitoring) +- [📋 Deployment Checklist](en/Deployment-Checklist.md) ## 🔒 Security -- [🛡️ Security Policy](en/Security) -- [🔐 Authentication](en/Authentication) -- [🔒 Environment Security](en/Environment-Security) +- [🛡️ Security Policy](en/Security.md) ## 🛠️ Advanced -- [🔧 Custom Services](en/Custom-Services) -- [📋 Migration Guide](en/Migration) -- [🚨 Troubleshooting](en/Troubleshooting) -- [⚡ Performance Tuning](en/Performance-Tuning) +- [🚨 Troubleshooting](en/Troubleshooting.md) --- ## 🇮🇷 مستندات فارسی ### شروع کار -- [راهنمای شروع](fa/راهنمای-شروع) -- [شروع سریع](fa/شروع-سریع) -- [پیکربندی](fa/پیکربندی) +- [راهنمای شروع](fa/راهنمای-شروع.md) +- [شروع سریع](fa/شروع-سریع.md) +- [پیکربندی](fa/پیکربندی.md) ### معماری -- [معماری سیستم](fa/معماری-سیستم) -- [راهنمای مهاجرت](fa/راهنمای-مهاجرت-معماری) \ No newline at end of file +- [معماری سیستم](fa/معماری-سیستم.md) +- [راهنمای مهاجرت معماری](fa/راهنمای-مهاجرت-معماری.md) diff --git a/wiki/en/Architecture.md b/wiki/en/Architecture.md index a2846a8..60f19f5 100644 --- a/wiki/en/Architecture.md +++ b/wiki/en/Architecture.md @@ -1,3 +1,56 @@ +# RssBot System Architecture + +The RssBot platform is designed based on a Hybrid Microservices architecture, aiming to provide maximum flexibility, stability, and scalability. + +## Core Components + +The RssBot architecture consists of two main parts: + +1. **Core Platform:** The brain of the system, located in the `src/rssbot/` path. +2. **Services:** Independent functional units, each responsible for a specific task, located in the `services/` directory. + +--- + +### 1. Core Platform + +The core platform includes the following critical components that manage and coordinate the entire system: + +#### **Core Controller** + +- **Path:** `src/rssbot/core/controller.py` +- **Responsibility:** This controller is the beating heart of the platform. Its main task is Service Discovery, managing their lifecycle, and deciding how to route requests. +- **Functionality:** On startup, the controller identifies all available services and, based on each service's configuration, decides whether to load it as an **In-Process Router** or communicate with it via a **REST API**. + +#### **Cached Registry** + +- **Path:** `src/rssbot/discovery/cached_registry.py` +- **Responsibility:** This component caches information about active services, their Health Status, and their Connection Method in Redis. +- **Advantage:** By using Redis, service discovery is performed in under a millisecond, which significantly increases the speed of communication between services. + +#### **ServiceProxy** + +- **Path:** `src/rssbot/discovery/proxy.py` +- **Responsibility:** This class is an intelligent tool for communication between services. Developers can easily call methods of a target service without worrying about its implementation details. +- **Functionality:** The `ServiceProxy` automatically queries the cached registry and selects the best communication method: + - **Router Mode:** If the target service is loaded as an internal router, its method is called directly without network overhead. + - **REST Mode:** If the service is running independently, the `ServiceProxy` sends an HTTP request to the corresponding endpoint. + - **Hybrid Mode:** A combination of the two modes above, maximizing flexibility. + +--- + +### 2. Services + +Each service is an independent FastAPI application that provides a specific functionality. This independence allows teams to develop and deploy their service without affecting other parts of the system. + +**Examples of Services:** + +- **`channel_mgr_svc`:** Manages channels and RSS feeds. +- **`ai_svc`:** Provides artificial intelligence capabilities like content summarization. +- **`bot_svc`:** Communicates with the Telegram API and sends messages. +- **`user_svc`:** Manages users and subscriptions. + +This modular and flexible architecture makes RssBot a powerful platform ready for future developments. +======= # 🏗️ Architecture Overview Complete guide to RssBot Platform's revolutionary hybrid microservices architecture. diff --git a/wiki/en/Configuration.md b/wiki/en/Configuration.md index a4a8ea4..88956c5 100644 --- a/wiki/en/Configuration.md +++ b/wiki/en/Configuration.md @@ -7,7 +7,9 @@ Complete configuration reference for RssBot Platform environment variables, serv RssBot Platform uses a **hierarchical configuration system** with the following priority order: 1. **Environment Variables** (highest priority) -2. **`.env` File** + +2. **`.env` File** + 3. **Code Defaults** (lowest priority) ## 🔧 Environment Variables Reference @@ -284,7 +286,9 @@ curl -X POST http://localhost:8004/services/{service_name}/connection-method \ # Available methods: # - router: Direct function calls (fastest) -# - rest: HTTP API calls (most scalable) + +# - rest: HTTP API calls (most scalable) + # - hybrid: Intelligent switching (best of both) # - disabled: Service disabled ``` @@ -465,17 +469,17 @@ RssBot Platform automatically validates critical configuration: class ConfigValidator: def validate_production_config(self, config: Config): errors = [] - if config.is_production(): if config.service_token == "dev_service_token_change_in_production": errors.append("SERVICE_TOKEN must be changed in production") - + if not config.database_url.startswith("postgresql://"): errors.append("Production should use PostgreSQL") - + if not config.telegram_webhook_mode: errors.append("Production should use webhook mode") - + + if errors: raise ConfigurationError("\n".join(errors)) ``` @@ -492,7 +496,8 @@ curl http://localhost:8004/admin/config/validate # Test database connection curl http://localhost:8004/admin/config/test-database -# Test Redis connection +# Test Redis connection + curl http://localhost:8004/admin/config/test-redis ``` diff --git a/wiki/en/Contributing.md b/wiki/en/Contributing.md index 3df9925..a21e1ce 100644 --- a/wiki/en/Contributing.md +++ b/wiki/en/Contributing.md @@ -200,19 +200,19 @@ async def process_services( ) -> Dict[str, Union[str, bool]]: """ Process multiple services with specified connection method. - + Args: service_names: List of service names to process connection_method: Connection method ("router", "rest", "hybrid") - + Returns: Dictionary with processing results for each service - + Raises: HTTPException: If service configuration fails """ results = {} - + for service_name in service_names: try: result = await configure_service(service_name, connection_method) @@ -222,7 +222,6 @@ async def process_services( status_code=500, detail=f"Failed to configure {service_name}: {str(e)}" ) - return results ``` @@ -254,34 +253,34 @@ Use **Google-style** docstrings: class ServiceRegistry: """ Redis-backed service registry with database persistence. - - This class manages service discovery, health monitoring, and + + This class manages service discovery, health monitoring, and connection method configuration for the hybrid microservices platform. - + Attributes: redis_client: Redis client for caching db_session: Database session factory - + Example: ```python registry = ServiceRegistry() await registry.initialize() - + # Check if service should use router use_router = await registry.should_use_router("ai_svc") ``` """ - + async def should_use_router(self, service_name: str) -> bool: """ Determine if service should use router connection method. - + Args: service_name: Name of the service (e.g., 'ai_svc') - + Returns: True if service should use router mode, False for REST - + Raises: ServiceNotFoundError: If service is not registered CacheConnectionError: If Redis is unavailable and DB fails @@ -305,10 +304,9 @@ async def configure_service(service_name: str, method: str) -> bool: service = await self.get_service(service_name) if not service: raise ServiceNotFoundError(f"Service {service_name} not found") - + # Configure service await service.set_connection_method(method) - except ServiceNotFoundError: # Re-raise specific exceptions raise @@ -345,7 +343,6 @@ from rssbot.discovery.cached_registry import CachedServiceRegistry class TestCachedServiceRegistry: """Test suite for CachedServiceRegistry.""" - @pytest.fixture async def registry(self): """Create test registry instance.""" @@ -354,7 +351,6 @@ class TestCachedServiceRegistry: registry._redis = AsyncMock() registry._redis_available = True return registry - @pytest.mark.asyncio async def test_should_use_router_returns_true_for_router_services(self, registry): """Test that router services return True for router decision.""" @@ -363,14 +359,14 @@ class TestCachedServiceRegistry: registry._get_cached_connection_method = AsyncMock( return_value=ConnectionMethod.ROUTER ) - + # Act result = await registry.should_use_router(service_name) - + # Assert assert result is True registry._get_cached_connection_method.assert_called_once_with(service_name) - + @pytest.mark.asyncio async def test_cache_fallback_when_redis_unavailable(self, registry): """Test that system falls back to database when Redis is down.""" @@ -379,10 +375,10 @@ class TestCachedServiceRegistry: mock_service = Mock() mock_service.get_effective_connection_method.return_value = ConnectionMethod.REST registry.registry_manager.get_service_by_name = AsyncMock(return_value=mock_service) - + # Act result = await registry.get_effective_connection_method("test_svc") - + # Assert assert result == ConnectionMethod.REST ``` diff --git a/wiki/en/Development.md b/wiki/en/Development.md index d54356d..95aa2bd 100644 --- a/wiki/en/Development.md +++ b/wiki/en/Development.md @@ -108,23 +108,24 @@ async def process_services( ) -> Dict[str, Union[str, bool]]: """ Process multiple services with specified connection method. - + + Args: service_names: List of service names to process connection_method: How services should connect - + Returns: Dictionary with processing results for each service - + Raises: ValueError: If service_names is empty ServiceError: If processing fails """ if not service_names: raise ValueError("service_names cannot be empty") - + results: Dict[str, Union[str, bool]] = {} - + for service_name in service_names: try: success = await configure_service(service_name, connection_method) @@ -132,7 +133,7 @@ async def process_services( except ServiceError as e: logger.error(f"Failed to process {service_name}: {e}") results[service_name] = False - + return results # ❌ Incorrect: Missing type hints @@ -149,19 +150,19 @@ def process_services(service_names, connection_method=None): class CachedServiceRegistry: """ High-performance service registry with Redis caching. - + This class provides service discovery, health monitoring, and connection method management using Redis for caching and database for persistence. - + Attributes: redis_client: Redis client for caching operations db_session: Database session for persistent storage - + Example: ```python registry = CachedServiceRegistry() await registry.initialize() - + # Check if service should use router use_router = await registry.should_use_router("ai_svc") if use_router: @@ -169,26 +170,27 @@ class CachedServiceRegistry: pass ``` """ - + async def should_use_router(self, service_name: str) -> bool: """ Determine if service should use router connection method. - + This is the primary method for making per-service connection decisions. It checks cached configuration and service health to determine the optimal connection method. - + Args: service_name: Name of the service (e.g., 'ai_svc', 'formatting_svc') - + Returns: True if service should be mounted as FastAPI router (in-process), False if service should use REST HTTP calls - + Raises: ValueError: If service_name is empty or invalid format CacheConnectionError: If Redis is down and database is unreachable - + + Example: ```python # Check AI service connection method @@ -214,7 +216,6 @@ from rssbot.models.service_registry import ConnectionMethod class TestCachedServiceRegistry: """Comprehensive test suite for CachedServiceRegistry.""" - @pytest.fixture async def mock_registry(self) -> CachedServiceRegistry: """Create mock registry for testing.""" @@ -222,7 +223,7 @@ class TestCachedServiceRegistry: registry._redis = AsyncMock() registry._redis_available = True return registry - + @pytest.mark.asyncio async def test_should_use_router_validates_input( self, mock_registry: CachedServiceRegistry @@ -231,11 +232,13 @@ class TestCachedServiceRegistry: # Test empty service name with pytest.raises(ValueError, match="service_name must be non-empty"): await mock_registry.should_use_router("") - + + # Test None input with pytest.raises(ValueError): await mock_registry.should_use_router(None) # type: ignore - + + @pytest.mark.asyncio async def test_should_use_router_returns_correct_decision( self, mock_registry: CachedServiceRegistry @@ -245,14 +248,15 @@ class TestCachedServiceRegistry: mock_registry._get_cached_connection_method = AsyncMock( return_value=ConnectionMethod.ROUTER ) - + + # Act result = await mock_registry.should_use_router("ai_svc") - + # Assert assert result is True mock_registry._get_cached_connection_method.assert_called_once_with("ai_svc") - + @pytest.mark.asyncio async def test_cache_fallback_behavior( self, mock_registry: CachedServiceRegistry @@ -265,10 +269,10 @@ class TestCachedServiceRegistry: mock_registry.registry_manager.get_service_by_name = AsyncMock( return_value=mock_service ) - + # Act result = await mock_registry.get_effective_connection_method("test_svc") - + # Assert assert result == ConnectionMethod.REST ``` @@ -343,7 +347,7 @@ formatting_service = ServiceProxy("formatting_svc") async def health_check() -> Dict[str, str]: """ Service health check endpoint. - + Returns: Health status information """ @@ -360,20 +364,20 @@ async def process_data( ) -> ProcessResponse: """ Process data using AI and formatting services. - + Args: request: Processing request with data and options token: Service authentication token - + Returns: Processing results with metadata - + Raises: HTTPException: If processing fails """ import time start_time = time.time() - + try: # Use other services via ServiceProxy if request.options.get("use_ai", False): @@ -381,7 +385,7 @@ async def process_data( processed_data = ai_result.get("result", request.data) else: processed_data = request.data - + if request.options.get("format", False): formatted_result = await formatting_service.format( content=processed_data, @@ -390,9 +394,11 @@ async def process_data( final_result = formatted_result.get("formatted_content", processed_data) else: final_result = processed_data - + + processing_time = (time.time() - start_time) * 1000 - + + return ProcessResponse( result=final_result, metadata={ @@ -528,7 +534,7 @@ class TestNewService: def client(self): """Create test client.""" return TestClient(app) - + @pytest.fixture def mock_ai_service(self): """Mock AI service dependency.""" diff --git "a/wiki/fa/\331\205\330\271\331\205\330\247\330\261\333\214-\330\263\333\214\330\263\330\252\331\205.md" "b/wiki/fa/\331\205\330\271\331\205\330\247\330\261\333\214-\330\263\333\214\330\263\330\252\331\205.md" index e1ca9c0..1bd46fa 100644 --- "a/wiki/fa/\331\205\330\271\331\205\330\247\330\261\333\214-\330\263\333\214\330\263\330\252\331\205.md" +++ "b/wiki/fa/\331\205\330\271\331\205\330\247\330\261\333\214-\330\263\333\214\330\263\330\252\331\205.md" @@ -1,494 +1,53 @@ -# 🏗️ معماری سیستم پلتفرم RssBot -**طراحی Hybrid Microservices با قابلیت تصمیم‌گیری Per-Service** +# معماری سیستم RssBot -## 🎯 نگاهی کلی +پلتفرم RssBot بر اساس یک معماری میکروسرویس ترکیبی (Hybrid Microservices) طراحی شده است که هدف آن ارائه حداکثر انعطاف‌پذیری، پایداری و توسعه‌پذیری است. -پلتفرم RssBot یک معماری انقلابی **Hybrid Microservices** است که هر سرویس می‌تواند بصورت مستقل تصمیم بگیرد که چگونه متصل شود: +## اجزای اصلی -- 🔗 **Router Mode**: اتصال مستقیم از طریق controller (سریع‌ترین) -- 🌐 **REST Mode**: HTTP API مستقل (مقیاس‌پذیرترین) -- ⚡ **Hybrid Mode**: ترکیب هوشمند router + REST -- 🚫 **Disabled Mode**: غیرفعال‌سازی کامل سرویس +معماری RssBot از دو بخش اصلی تشکیل شده است: -## 🏛️ معماری کلی سیستم +1. **هسته پلتفرم (Core Platform):** مغز متفکر سیستم که در مسیر `src/rssbot/` قرار دارد. +2. **سرویس‌ها (Services):** واحدهای مستقل عملکردی که هر کدام مسئولیت خاصی را بر عهده دارند و در پوشه `services/` قرار گرفته‌اند. -```mermaid -graph TB - subgraph "🎯 Core Platform (src/rssbot/)" - CTRL[Core Controller
🎮 Platform Engine] - REG[Service Registry
📋 Redis-Cached] - DISC[Service Discovery
🔍 Health Monitor] - PROXY[Service Proxy
🔀 Smart Router] - end - - subgraph "📡 Independent Services" - DB[Database Service
🗄️ PostgreSQL/SQLite] - BOT[Bot Service
🤖 Telegram Integration] - AI[AI Service
🧠 OpenAI Processing] - FMT[Formatting Service
📝 Template Engine] - USER[User Service
👥 Management] - PAY[Payment Service
💳 Stripe Integration] - ADMIN[Admin Service
⚙️ Management Panel] - end - - subgraph "🗄️ Data Layer" - REDIS[(Redis Cache
⚡ Registry + Performance)] - POSTGRES[(PostgreSQL
🗃️ Primary Database)] - SQLITE[(SQLite
📦 Local/Testing)] - end - - subgraph "🌐 External APIs" - TELEGRAM[Telegram Bot API] - OPENAI[OpenAI API] - STRIPE[Stripe API] - RSS[RSS Feeds] - end - - CTRL --> REG - REG --> REDIS - CTRL --> DISC - CTRL --> PROXY - - PROXY --> DB - PROXY --> BOT - PROXY --> AI - PROXY --> FMT - PROXY --> USER - PROXY --> PAY - PROXY --> ADMIN - - DB --> POSTGRES - DB --> SQLITE - BOT --> TELEGRAM - AI --> OPENAI - PAY --> STRIPE - BOT --> RSS -``` - -## 🔄 انواع Connection Methods - -### 1️⃣ Router Mode (مستقیم) -```python -# سرویس مستقیماً در controller mount می‌شود -app.include_router(service_router, prefix="/services/ai_svc") - -# مزایا: -✅ سریع‌ترین عملکرد (بدون HTTP overhead) -✅ Type safety کامل -✅ Error handling یکپارچه -✅ مناسب برای سرویس‌های core - -# نحوه‌ی کار: -Client -> Controller -> Service Function (Direct Call) -``` - -### 2️⃣ REST Mode (HTTP API) -```python -# سرویس بصورت مستقل HTTP API ارائه می‌دهد -service_url = "http://ai-service:8080" -response = await httpx.post(f"{service_url}/process", json=data) - -# مزایا: -✅ مقیاس‌پذیری بالا -✅ جدایی کامل سرویس‌ها -✅ Language agnostic -✅ مناسب برای microservices واقعی - -# نحوه‌ی کار: -Client -> Controller -> HTTP Request -> Service -> Response -``` - -### 3️⃣ Hybrid Mode (هوشمند) -```python -# ترکیب router و REST بر اساس شرایط -if service.is_local and load < threshold: - result = await direct_call(service_function, data) -else: - result = await http_call(service_url, data) - -# مزایا: -✅ بهترین عملکرد در شرایط مختلف -✅ Failover خودکار -✅ Load balancing هوشمند -✅ مناسب برای production - -# نحوه‌ی کار: -Client -> Controller -> Smart Decision -> Best Method -``` - -### 4️⃣ Disabled Mode (غیرفعال) -```python -# سرویس کاملاً غیرفعال می‌شود -service.status = "disabled" -# تمام درخواست‌ها با error مناسب پاسخ داده می‌شوند - -# کاربردها: -🔧 Maintenance mode -🚫 Security isolation -⚡ Resource optimization -🧪 Testing scenarios -``` - -## 🧠 Service Discovery Engine - -### Redis-Backed Registry -```python -# کش Redis برای performance بالا -class CachedServiceRegistry: - async def get_service(self, name: str) -> ServiceInfo: - # 1. چک کردن Redis cache (sub-millisecond) - cached = await self.redis.get(f"service:{name}") - if cached: - return ServiceInfo.parse_raw(cached) - - # 2. Query از database (fallback) - service = await self.db.get_service(name) - - # 3. کش کردن برای دفعات بعد - await self.redis.set(f"service:{name}", service.json(), ex=300) - - return service - -# Performance: 1000x سریع‌تر از DB query -``` - -### Health Monitoring -```python -# نظارت خودکار بر سلامت سرویس‌ها -class HealthChecker: - async def check_service_health(self, service: ServiceInfo): - try: - if service.connection_method == "router": - # تست function call مستقیم - return await self.test_direct_call(service) - else: - # تست HTTP endpoint - return await self.test_http_endpoint(service.url) - except Exception as e: - # خودکار switch به backup method - await self.failover_service(service, e) -``` - -## 📂 ساختار Core Platform - -### `src/rssbot/core/` -``` -core/ -├── controller.py # 🎮 هسته اصلی پلتفرم -├── config.py # ⚙️ مدیریت تنظیمات -├── exceptions.py # 🚨 Exception handling -└── security.py # 🔒 امنیت و authentication -``` - -### `src/rssbot/discovery/` -``` -discovery/ -├── cached_registry.py # 📋 Service registry با Redis -├── health_checker.py # 🏥 Health monitoring -├── proxy.py # 🔀 Service proxy و routing -├── registry.py # 📊 Core registry logic -└── scanner.py # 🔍 Auto service discovery -``` - -### `src/rssbot/models/` -``` -models/ -└── service_registry.py # 🗂️ SQLModel data models -``` - -## 🔀 Service Proxy Engine - -### Smart Routing -```python -class ServiceProxy: - async def call_service(self, service_name: str, method: str, data: dict): - service = await self.registry.get_service(service_name) - - # تصمیم‌گیری هوشمند - if service.connection_method == "router": - return await self._direct_call(service, method, data) - - elif service.connection_method == "rest": - return await self._http_call(service, method, data) - - elif service.connection_method == "hybrid": - # انتخاب بهترین روش بر اساس شرایط - if await self._should_use_direct(service): - return await self._direct_call(service, method, data) - else: - return await self._http_call(service, method, data) - - else: # disabled - raise ServiceDisabledException(service_name) -``` - -### Load Balancing -```python -# توزیع load بین instance های مختلف -class LoadBalancer: - async def select_instance(self, service_name: str) -> ServiceInstance: - instances = await self.registry.get_service_instances(service_name) - - # انتخاب بر اساس: - # 1. Health status - # 2. Response time - # 3. Current load - # 4. Geographic proximity - - healthy_instances = [i for i in instances if i.is_healthy] - return self.weighted_round_robin(healthy_instances) -``` - -## 🗄️ Data Architecture - -### Multi-Database Support -```python -# پشتیبانی همزمان از چند نوع database -class DatabaseManager: - def __init__(self): - # Primary database (production) - self.postgres = PostgresEngine(DATABASE_URL) - - # Cache layer (performance) - self.redis = RedisEngine(REDIS_URL) - - # Local database (development/testing) - self.sqlite = SQLiteEngine("test.db") - - async def get_connection(self) -> DatabaseConnection: - if ENVIRONMENT == "production": - return self.postgres - elif REDIS_URL and await self.redis.ping(): - return self.postgres # با Redis cache - else: - return self.sqlite # Fallback -``` - -### Schema Management -```python -# مدیریت schema با SQLModel -from sqlmodel import SQLModel, Field -from typing import Optional - -class ServiceRegistryModel(SQLModel, table=True): - __tablename__ = "service_registry" - - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(unique=True, index=True) - connection_method: str = Field(default="router") - port: Optional[int] = None - health_status: str = Field(default="unknown") - last_seen: datetime = Field(default_factory=datetime.utcnow) - - # Automatic validation - @validator("connection_method") - def validate_connection_method(cls, v): - if v not in ["router", "rest", "hybrid", "disabled"]: - raise ValueError("Invalid connection method") - return v -``` - -## 📊 Performance Architecture - -### Caching Strategy -```python -# چندسطحی caching برای performance بهینه -class CacheManager: - def __init__(self): - # L1: In-memory cache (سریع‌ترین) - self.memory_cache = LRUCache(maxsize=1000) - - # L2: Redis cache (سریع) - self.redis_cache = RedisCache() - - # L3: Database (کندترین) - self.database = DatabaseConnection() - - async def get(self, key: str): - # 1. چک کردن memory - if key in self.memory_cache: - return self.memory_cache[key] - - # 2. چک کردن Redis - redis_value = await self.redis_cache.get(key) - if redis_value: - self.memory_cache[key] = redis_value - return redis_value - - # 3. چک کردن Database - db_value = await self.database.get(key) - if db_value: - await self.redis_cache.set(key, db_value, ex=300) - self.memory_cache[key] = db_value - return db_value - - return None -``` - -### Async Performance -```python -# همه عملیات async برای performance بالا -class AsyncServiceManager: - async def call_multiple_services(self, calls: List[ServiceCall]): - # اجرای موازی چندین service call - tasks = [ - self.call_service(call.service, call.method, call.data) - for call in calls - ] - - # منتظر تمام نتایج - results = await asyncio.gather(*tasks, return_exceptions=True) - - # پردازش نتایج و خطاها - return self.process_results(results) -``` - -## 🔒 Security Architecture - -### Service Authentication -```python -# احراز هویت بین سرویس‌ها -class ServiceAuth: - def __init__(self): - self.secret_key = os.getenv("SERVICE_SECRET_KEY") - - def generate_service_token(self, service_name: str) -> str: - payload = { - "service": service_name, - "exp": datetime.utcnow() + timedelta(hours=1), - "iat": datetime.utcnow() - } - return jwt.encode(payload, self.secret_key, algorithm="HS256") - - async def verify_service_token(self, token: str) -> str: - try: - payload = jwt.decode(token, self.secret_key, algorithms=["HS256"]) - return payload["service"] - except jwt.InvalidTokenError: - raise UnauthorizedException("Invalid service token") -``` - -### Input Validation -```python -# اعتبارسنجی ورودی با Pydantic -from pydantic import BaseModel, validator +--- -class ServiceCallRequest(BaseModel): - service_name: str - method: str - data: dict - - @validator("service_name") - def validate_service_name(cls, v): - if not re.match(r"^[a-z_]+$", v): - raise ValueError("Invalid service name format") - return v - - @validator("data") - def validate_data_size(cls, v): - if len(str(v)) > 1_000_000: # 1MB limit - raise ValueError("Request data too large") - return v -``` +### ۱. هسته پلتفرم -## 🚀 Deployment Architecture +هسته پلتفرم شامل اجزای حیاتی زیر است که مدیریت و هماهنگی کل سیستم را بر عهده دارند: -### Container Strategy -```dockerfile -# Multi-stage build برای optimization -FROM python:3.11-slim as builder -WORKDIR /build -COPY requirements.lock . -RUN pip install --no-deps -r requirements.lock +#### **کنترلر مرکزی (Core Controller)** -FROM python:3.11-slim as runtime -COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages -COPY src/ /app/src/ -WORKDIR /app -ENTRYPOINT ["python", "-m", "rssbot"] -``` +- **مسیر:** `src/rssbot/core/controller.py` +- **وظیفه:** این کنترلر قلب تپنده پلتفرم است. وظیفه اصلی آن کشف سرویس‌ها (Service Discovery)، مدیریت چرخه حیات آن‌ها و تصمیم‌گیری در مورد نحوه مسیریابی درخواست‌ها است. +- **عملکرد:** هنگام راه‌اندازی، کنترلر تمام سرویس‌های موجود را شناسایی کرده و بر اساس پیکربندی هر سرویس، تصمیم می‌گیرد که آیا آن را به صورت یک **روتر داخلی (In-Process Router)** بارگذاری کند یا از طریق **REST API** با آن ارتباط برقرار کند. -### Service Orchestration -```yaml -# Docker Compose برای توسعه -version: '3.8' -services: - controller: - build: . - ports: - - "8004:8004" - environment: - - REDIS_URL=redis://redis:6379 - - DATABASE_URL=postgresql://user:pass@postgres:5432/rssbot - depends_on: - - redis - - postgres - - redis: - image: redis:7-alpine - volumes: - - redis_data:/data - - postgres: - image: postgres:15-alpine - environment: - POSTGRES_DB: rssbot - volumes: - - postgres_data:/var/lib/postgresql/data +#### **رجیستری کش‌شده (Cached Registry)** -volumes: - redis_data: - postgres_data: -``` +- **مسیر:** `src/rssbot/discovery/cached_registry.py` +- **وظیفه:** این بخش اطلاعات مربوط به سرویس‌های فعال، وضعیت سلامت (Health Status) و روش ارتباطی (Connection Method) آن‌ها را در Redis کش می‌کند. +- **مزیت:** با استفاده از Redis، کشف سرویس‌ها در کمتر از یک میلی‌ثانیه انجام می‌شود که سرعت ارتباط بین سرویس‌ها را به شدت افزایش می‌دهد. -## 📈 Monitoring & Observability +#### **پراکسی سرویس (ServiceProxy)** -### Health Checks -```python -# نظارت بر سلامت سیستم -class SystemHealthChecker: - async def get_system_health(self) -> SystemHealth: - return SystemHealth( - controller_status=await self.check_controller(), - services_status=await self.check_all_services(), - database_status=await self.check_database(), - cache_status=await self.check_redis(), - external_apis=await self.check_external_apis() - ) -``` +- **مسیر:** `src/rssbot/discovery/proxy.py` +- **وظیفه:** این کلاس یک ابزار هوشمند برای برقراری ارتباط بین سرویس‌هاست. توسعه‌دهندگان بدون نگرانی از نحوه پیاده‌سازی سرویس مقصد، می‌توانند به سادگی متدهای آن را فراخوانی کنند. +- **عملکرد:** `ServiceProxy` به طور خودکار از رجیستری کش‌شده استعلام می‌گیرد و بهترین روش ارتباطی را انتخاب می‌کند: + - **Router Mode:** اگر سرویس مقصد به صورت روتر داخلی بارگذاری شده باشد، متد آن به صورت مستقیم و بدون سربار شبکه فراخوانی می‌شود. + - **REST Mode:** اگر سرویس به صورت مستقل در حال اجرا باشد، `ServiceProxy` یک درخواست HTTP به اندپوینت مربوطه ارسال می‌کند. + - **Hybrid Mode:** ترکیبی از دو حالت بالا که انعطاف‌پذیری را به حداکثر می‌رساند. -### Metrics Collection -```python -# جمع‌آوری metrics برای monitoring -class MetricsCollector: - def __init__(self): - self.request_counter = Counter("requests_total") - self.response_time = Histogram("response_time_seconds") - self.error_counter = Counter("errors_total") - - def record_request(self, service: str, method: str, duration: float): - self.request_counter.labels(service=service, method=method).inc() - self.response_time.labels(service=service, method=method).observe(duration) -``` +--- -## 🎯 فلسفه معماری +### ۲. سرویس‌ها -### Design Principles -1. **Per-Service Autonomy**: هر سرویس مستقل تصمیم می‌گیرد -2. **Performance First**: Redis cache و async programming -3. **Type Safety**: SQLModel و Pydantic در همه‌جا -4. **Zero Downtime**: Hot reconfiguration بدون restart -5. **Developer Experience**: ساده، واضح و قابل debug +هر سرویس یک برنامه FastAPI مستقل است که یک قابلیت خاص را ارائه می‌دهد. این استقلال به تیم‌ها اجازه می‌دهد تا بدون تأثیر بر سایر بخش‌های سیستم، سرویس خود را توسعه داده و منتشر کنند. -### Trade-offs -| جنبه | Router Mode | REST Mode | Hybrid Mode | -|------|------------|-----------|-------------| -| **Performance** | 🟢 سریع‌ترین | 🟡 متوسط | 🟢 بهینه | -| **Scalability** | 🟡 محدود | 🟢 بالا | 🟢 انعطاف‌پذیر | -| **Complexity** | 🟢 ساده | 🟡 متوسط | 🔴 پیچیده | -| **Debugging** | 🟢 آسان | 🟡 متوسط | 🟡 متوسط | +**مثال‌هایی از سرویس‌ها:** ---- +- **`channel_mgr_svc`:** مدیریت کانال‌ها و فیدهای RSS. +- **`ai_svc`:** ارائه قابلیت‌های هوش مصنوعی مانند خلاصه‌سازی محتوا. +- **`bot_svc`:** ارتباط با API تلگرام و ارسال پیام‌ها. +- **`user_svc`:** مدیریت کاربران و اشتراک‌ها. -**این معماری RssBot را به یک پلتفرم منحصربه‌فرد تبدیل می‌کند که بهترین‌های monolithic و microservices را ترکیب کرده است! 🚀** \ No newline at end of file +این معماری ماژولار و انعطاف‌پذیر، RssBot را به یک پلتفرم قدرتمند و آماده برای توسعه‌های آینده تبدیل کرده است.