c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
249 lines
9.8 KiB
Python
249 lines
9.8 KiB
Python
import asyncio
|
|
from typing import Optional, List
|
|
from datetime import datetime, timezone
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import SQLAlchemyError, DisconnectionError, OperationalError, TimeoutError
|
|
from cognee.modules.sync.models import SyncOperation, SyncStatus
|
|
from cognee.infrastructure.databases.relational import get_relational_engine
|
|
from cognee.shared.logging_utils import get_logger
|
|
from cognee.infrastructure.utils.calculate_backoff import calculate_backoff
|
|
|
|
logger = get_logger("sync.db_operations")
|
|
|
|
|
|
async def _retry_db_operation(operation_func, run_id: str, max_retries: int = 3):
|
|
"""
|
|
Retry database operations with exponential backoff for transient failures.
|
|
|
|
Args:
|
|
operation_func: Async function to retry
|
|
run_id: Run ID for logging context
|
|
max_retries: Maximum number of retry attempts
|
|
|
|
Returns:
|
|
Result of the operation function
|
|
|
|
Raises:
|
|
Exception: Re-raises the last exception if all retries fail
|
|
"""
|
|
attempt = 0
|
|
last_exception = None
|
|
|
|
while attempt < max_retries:
|
|
try:
|
|
return await operation_func()
|
|
except (DisconnectionError, OperationalError, TimeoutError) as e:
|
|
attempt += 1
|
|
last_exception = e
|
|
|
|
if attempt >= max_retries:
|
|
logger.error(
|
|
f"Database operation failed after {max_retries} attempts for run_id {run_id}: {str(e)}"
|
|
)
|
|
break
|
|
|
|
backoff_time = calculate_backoff(attempt - 1) # calculate_backoff is 0-indexed
|
|
logger.warning(
|
|
f"Database operation failed for run_id {run_id}, retrying in {backoff_time:.2f}s (attempt {attempt}/{max_retries}): {str(e)}"
|
|
)
|
|
await asyncio.sleep(backoff_time)
|
|
|
|
except Exception as e:
|
|
# Non-transient errors should not be retried
|
|
logger.error(f"Non-retryable database error for run_id {run_id}: {str(e)}")
|
|
raise
|
|
|
|
# If we get here, all retries failed
|
|
raise last_exception
|
|
|
|
|
|
async def update_sync_operation(
|
|
run_id: str,
|
|
status: Optional[SyncStatus] = None,
|
|
progress_percentage: Optional[int] = None,
|
|
records_downloaded: Optional[int] = None,
|
|
total_records_to_sync: Optional[int] = None,
|
|
total_records_to_download: Optional[int] = None,
|
|
total_records_to_upload: Optional[int] = None,
|
|
records_uploaded: Optional[int] = None,
|
|
bytes_downloaded: Optional[int] = None,
|
|
bytes_uploaded: Optional[int] = None,
|
|
dataset_sync_hashes: Optional[dict] = None,
|
|
error_message: Optional[str] = None,
|
|
retry_count: Optional[int] = None,
|
|
started_at: Optional[datetime] = None,
|
|
completed_at: Optional[datetime] = None,
|
|
) -> Optional[SyncOperation]:
|
|
"""
|
|
Update a sync operation record with new status/progress information.
|
|
|
|
Args:
|
|
run_id: The public run_id of the sync operation to update
|
|
status: New status for the operation
|
|
progress_percentage: Progress percentage (0-100)
|
|
records_downloaded: Number of records downloaded so far
|
|
total_records_to_sync: Total number of records that need to be synced
|
|
total_records_to_download: Total number of records to download from cloud
|
|
total_records_to_upload: Total number of records to upload to cloud
|
|
records_uploaded: Number of records uploaded so far
|
|
bytes_downloaded: Total bytes downloaded from cloud
|
|
bytes_uploaded: Total bytes uploaded to cloud
|
|
dataset_sync_hashes: Dict mapping dataset_id -> {uploaded: [hashes], downloaded: [hashes]}
|
|
error_message: Error message if operation failed
|
|
retry_count: Number of retry attempts
|
|
started_at: When the actual processing started
|
|
completed_at: When the operation completed (success or failure)
|
|
|
|
Returns:
|
|
SyncOperation: The updated sync operation record, or None if not found
|
|
"""
|
|
|
|
async def _perform_update():
|
|
db_engine = get_relational_engine()
|
|
|
|
async with db_engine.get_async_session() as session:
|
|
try:
|
|
# Find the sync operation
|
|
query = select(SyncOperation).where(SyncOperation.run_id == run_id)
|
|
result = await session.execute(query)
|
|
sync_operation = result.scalars().first()
|
|
|
|
if not sync_operation:
|
|
logger.warning(f"Sync operation not found for run_id: {run_id}")
|
|
return None
|
|
|
|
# Log what we're updating for debugging
|
|
updates = []
|
|
if status is not None:
|
|
updates.append(f"status={status.value}")
|
|
if progress_percentage is not None:
|
|
updates.append(f"progress={progress_percentage}%")
|
|
if records_downloaded is not None:
|
|
updates.append(f"downloaded={records_downloaded}")
|
|
if records_uploaded is not None:
|
|
updates.append(f"uploaded={records_uploaded}")
|
|
if total_records_to_sync is not None:
|
|
updates.append(f"total_sync={total_records_to_sync}")
|
|
if total_records_to_download is not None:
|
|
updates.append(f"total_download={total_records_to_download}")
|
|
if total_records_to_upload is not None:
|
|
updates.append(f"total_upload={total_records_to_upload}")
|
|
|
|
if updates:
|
|
logger.debug(f"Updating sync operation {run_id}: {', '.join(updates)}")
|
|
|
|
# Update fields that were provided
|
|
if status is not None:
|
|
sync_operation.status = status
|
|
|
|
if progress_percentage is not None:
|
|
sync_operation.progress_percentage = max(0, min(100, progress_percentage))
|
|
|
|
if records_downloaded is not None:
|
|
sync_operation.records_downloaded = records_downloaded
|
|
|
|
if records_uploaded is not None:
|
|
sync_operation.records_uploaded = records_uploaded
|
|
|
|
if total_records_to_sync is not None:
|
|
sync_operation.total_records_to_sync = total_records_to_sync
|
|
|
|
if total_records_to_download is not None:
|
|
sync_operation.total_records_to_download = total_records_to_download
|
|
|
|
if total_records_to_upload is not None:
|
|
sync_operation.total_records_to_upload = total_records_to_upload
|
|
|
|
if bytes_downloaded is not None:
|
|
sync_operation.bytes_downloaded = bytes_downloaded
|
|
|
|
if bytes_uploaded is not None:
|
|
sync_operation.bytes_uploaded = bytes_uploaded
|
|
|
|
if dataset_sync_hashes is not None:
|
|
sync_operation.dataset_sync_hashes = dataset_sync_hashes
|
|
|
|
if error_message is not None:
|
|
sync_operation.error_message = error_message
|
|
|
|
if retry_count is not None:
|
|
sync_operation.retry_count = retry_count
|
|
|
|
if started_at is not None:
|
|
sync_operation.started_at = started_at
|
|
|
|
if completed_at is not None:
|
|
sync_operation.completed_at = completed_at
|
|
|
|
# Auto-set completion timestamp for terminal statuses
|
|
if (
|
|
status in [SyncStatus.COMPLETED, SyncStatus.FAILED, SyncStatus.CANCELLED]
|
|
and completed_at is None
|
|
):
|
|
sync_operation.completed_at = datetime.now(timezone.utc)
|
|
|
|
# Auto-set started timestamp when moving to IN_PROGRESS
|
|
if status == SyncStatus.IN_PROGRESS and sync_operation.started_at is None:
|
|
sync_operation.started_at = datetime.now(timezone.utc)
|
|
|
|
await session.commit()
|
|
await session.refresh(sync_operation)
|
|
|
|
logger.debug(f"Successfully updated sync operation {run_id}")
|
|
return sync_operation
|
|
|
|
except SQLAlchemyError as e:
|
|
logger.error(
|
|
f"Database error updating sync operation {run_id}: {str(e)}", exc_info=True
|
|
)
|
|
await session.rollback()
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Unexpected error updating sync operation {run_id}: {str(e)}", exc_info=True
|
|
)
|
|
await session.rollback()
|
|
raise
|
|
|
|
# Use retry logic for the database operation
|
|
return await _retry_db_operation(_perform_update, run_id)
|
|
|
|
|
|
async def mark_sync_started(run_id: str) -> Optional[SyncOperation]:
|
|
"""Convenience method to mark a sync operation as started."""
|
|
return await update_sync_operation(
|
|
run_id=run_id, status=SyncStatus.IN_PROGRESS, started_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
|
|
async def mark_sync_completed(
|
|
run_id: str,
|
|
records_downloaded: int = 0,
|
|
records_uploaded: int = 0,
|
|
bytes_downloaded: int = 0,
|
|
bytes_uploaded: int = 0,
|
|
dataset_sync_hashes: Optional[dict] = None,
|
|
) -> Optional[SyncOperation]:
|
|
"""Convenience method to mark a sync operation as completed successfully."""
|
|
return await update_sync_operation(
|
|
run_id=run_id,
|
|
status=SyncStatus.COMPLETED,
|
|
progress_percentage=100,
|
|
records_downloaded=records_downloaded,
|
|
records_uploaded=records_uploaded,
|
|
bytes_downloaded=bytes_downloaded,
|
|
bytes_uploaded=bytes_uploaded,
|
|
dataset_sync_hashes=dataset_sync_hashes,
|
|
completed_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
|
|
async def mark_sync_failed(run_id: str, error_message: str) -> Optional[SyncOperation]:
|
|
"""Convenience method to mark a sync operation as failed."""
|
|
return await update_sync_operation(
|
|
run_id=run_id,
|
|
status=SyncStatus.FAILED,
|
|
error_message=error_message,
|
|
completed_at=datetime.now(timezone.utc),
|
|
)
|