amo-server/workers/broker.py
Maxim Snesarev 33d6bb7ebd Refactor AMO CRM Data Collection Service to use PostgreSQL
- Updated database configuration to switch from SQLite to PostgreSQL, including changes to alembic.ini, Docker Compose, and environment settings.
- Refactored application code to utilize PostgreSQL database adapters, ensuring compatibility with the new database structure.
- Enhanced API routes and data handling to support the new database, including adjustments in data models and query logic.
- Introduced new job processing mechanisms for full synchronization of AMO CRM entities, leveraging FastStream for background tasks.
- Improved logging and error handling across the application to facilitate better monitoring and debugging.
- Removed obsolete SQLite adapter files and migrations, streamlining the project structure for PostgreSQL integration.
2025-11-05 00:38:37 +03:00

195 lines
6.4 KiB
Python

"""
FastStream broker setup for AMO CRM service background processing.
This module sets up the Redis-based message broker and defines worker
functions for processing export jobs, data synchronization, and scheduled tasks.
"""
import logging
from typing import Dict, Any
from faststream import FastStream
from faststream.redis import RedisBroker
from utils.config import settings
# Configure logging
logging.basicConfig(level=settings.LOG_LEVEL)
logger = logging.getLogger(__name__)
# Initialize Redis broker
broker = RedisBroker(settings.REDIS_URL)
app = FastStream(broker)
@broker.subscriber("export-jobs")
async def process_export_job(job_data: Dict[str, Any]) -> None:
"""
Process Google Sheets export jobs.
Args:
job_data: Dictionary containing job_id, configuration_id, and metadata
"""
logger.info(f"Processing export job: {job_data.get('job_id')}")
try:
from servers.export_server import ExportServer
export_server = ExportServer()
await export_server.process_export_job(job_data)
logger.info(f"Export job {job_data.get('job_id')} completed successfully")
except Exception as e:
logger.error(f"Export job {job_data.get('job_id')} failed: {str(e)}")
raise
@broker.subscriber("sync-jobs")
async def process_sync_job(sync_data: Dict[str, Any]) -> None:
"""
Process AMO CRM data synchronization jobs.
Args:
sync_data: Dictionary containing job_id, entity_type, and parameters
"""
logger.info(f"Processing sync job: {sync_data.get('job_id')} for {sync_data.get('entity_type')}")
try:
from servers.sync_server import SyncServer
sync_server = SyncServer()
await sync_server.process_sync_job(sync_data)
logger.info(f"Sync job {sync_data.get('job_id')} completed successfully")
except Exception as e:
logger.error(f"Sync job {sync_data.get('job_id')} failed: {str(e)}")
raise
@broker.subscriber("refresh-jobs")
async def process_refresh_job(entity_type: str) -> None:
"""
Process scheduled entity data refresh jobs.
Args:
entity_type: Type of entity to refresh (deals, contacts, companies, etc.)
"""
logger.info(f"Processing refresh job for entity type: {entity_type}")
try:
from servers.sync_server import SyncServer
sync_server = SyncServer()
await sync_server.refresh_entity_data(entity_type)
logger.info(f"Refresh job for {entity_type} completed successfully")
except Exception as e:
logger.error(f"Refresh job for {entity_type} failed: {str(e)}")
raise
@broker.subscriber("full-sync-jobs")
async def process_full_sync_job(job_data: Dict[str, Any]) -> None:
"""
Process full synchronization jobs for AMO CRM entities.
Supports syncing individual entity types or all entities when entity_type='all'.
This is a long-running operation that can take several hours.
Args:
job_data: Dictionary containing job_id, entity_type, batch_size, etc.
"""
job_id = job_data.get("job_id", "unknown")
entity_type = job_data.get("entity_type")
batch_size = job_data.get("batch_size", 250)
if not entity_type:
logger.error(f"Full sync job {job_id} missing entity_type")
raise ValueError("entity_type is required for full sync job")
logger.info(f"Processing full sync job {job_id} for entity type: {entity_type}")
try:
from servers.sync_server import SyncServer
sync_server = SyncServer()
# Handle "all" entity type
if entity_type == "all":
# IMPORTANT: Sync order matters due to foreign key constraints
# 1. Users must be synced first (referenced by all other entities)
# 2. Pipelines must be before deals (deals reference pipeline stages)
# 3. Companies, contacts, deals, events can follow
all_entities = ["users", "pipelines", "companies", "contacts", "deals", "events"]
total_all_records = 0
logger.info(f"Starting full sync for ALL entities: {all_entities}")
for entity in all_entities:
logger.info(f"Starting full sync for {entity} (part of 'all' job)")
try:
records = await sync_server.full_sync_entity(entity, batch_size=batch_size)
total_all_records += records
logger.info(f"Completed full sync for {entity}: {records} records")
except Exception as entity_error:
logger.error(f"Error syncing {entity} in 'all' job: {str(entity_error)}")
# Continue with next entity even if one fails
continue
logger.info(
f"Full sync job {job_id} for ALL entities completed: "
f"{total_all_records} total records across all entities"
)
else:
# Single entity sync
total_records = await sync_server.full_sync_entity(entity_type, batch_size=batch_size)
logger.info(f"Full sync job {job_id} for {entity_type} completed: {total_records} records")
except Exception as e:
logger.error(f"Full sync job {job_id} failed: {str(e)}")
raise
@broker.subscriber("failed-jobs")
async def handle_failed_jobs(job_data: Dict[str, Any]) -> None:
"""
Handle permanently failed jobs (dead letter queue).
Args:
job_data: Failed job data for logging and potential manual intervention
"""
logger.error(f"Job permanently failed: {job_data}")
# TODO: Implement notification system (email, Slack, etc.)
# TODO: Store failed jobs for manual review
# For now, just log the failure
job_id = job_data.get("job_id", "unknown")
job_type = job_data.get("job_type", "unknown")
error_message = job_data.get("error_message", "No error message provided")
logger.error(
f"DEAD LETTER QUEUE - Job ID: {job_id}, "
f"Type: {job_type}, Error: {error_message}"
)
@app.after_startup
async def startup_handler() -> None:
"""Handle broker startup tasks."""
logger.info("FastStream broker started successfully")
logger.info(f"Connected to Redis: {settings.REDIS_URL}")
@app.on_shutdown
async def shutdown_handler() -> None:
"""Handle broker shutdown tasks."""
logger.info("FastStream broker shutting down")
if __name__ == "__main__":
# This allows running the worker directly with: python workers/broker.py
import asyncio
asyncio.run(app.run())