feat: image generation overhaul (model-aware text, dim clamping, \.30 pricing), event-driven dashboard cache invalidation, SEO insights (AI visibility, GSC, keyword gap), YouTube OAuth/publish, blog writer & content planning improvements, scheduler monitoring updates

This commit is contained in:
ajaysi
2026-05-30 07:58:22 +05:30
parent aaf94049da
commit 64f1f88cdd
129 changed files with 8796 additions and 8755 deletions

View File

@@ -3,25 +3,67 @@ Check Cycle Handler
Handles the main scheduler check cycle that finds and executes due tasks.
"""
import json
import os
from typing import TYPE_CHECKING, Dict, Any
from datetime import datetime
from sqlalchemy.orm import Session
from services.database import get_all_user_ids, get_session_for_user
from utils.logger_utils import get_service_logger
from .interval_manager import adjust_check_interval_if_needed
# Import semantic monitoring for Phase 2B integration
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor
if TYPE_CHECKING:
from .scheduler import TaskScheduler
logger = get_service_logger("check_cycle_handler")
# Track last semantic check per user to enforce 24-hour interval
# In-memory cache is sufficient as it resets on restart (which is fine)
LAST_SEMANTIC_CHECKS: Dict[str, datetime] = {}
# Cache for RealTimeSemanticMonitor instances per user (avoids expensive re-instantiation)
# Uses the global SemanticDashboardAPI singleton which provides get-or-create caching.
from services.intelligence.monitoring.semantic_dashboard import semantic_dashboard_api
# Persisted last-check timestamps for semantic health monitoring (24-hour cadence).
# Survives scheduler restarts via a JSON file in the app state directory.
_SEMANTIC_STATE_DIR = os.path.join(
os.path.expanduser("~"), ".alwrity", "scheduler_state"
)
_SEMANTIC_STATE_FILE = os.path.join(_SEMANTIC_STATE_DIR, "semantic_last_checks.json")
def _load_semantic_check_timestamps() -> Dict[str, datetime]:
"""Load persisted check timestamps from disk. Returns empty dict on any failure."""
try:
if not os.path.exists(_SEMANTIC_STATE_FILE):
return {}
with open(_SEMANTIC_STATE_FILE, "r") as f:
raw = json.load(f)
return {
uid: datetime.fromisoformat(ts)
for uid, ts in raw.items() if ts
}
except Exception as e:
logger.warning(f"Failed to load semantic check timestamps: {e}")
return {}
def _save_semantic_check_timestamps(checks: Dict[str, datetime]):
"""Persist check timestamps to disk."""
try:
os.makedirs(_SEMANTIC_STATE_DIR, exist_ok=True)
serializable = {
uid: ts.isoformat() if isinstance(ts, datetime) else ts
for uid, ts in checks.items()
}
with open(_SEMANTIC_STATE_FILE, "w") as f:
json.dump(serializable, f)
except Exception as e:
logger.warning(f"Failed to save semantic check timestamps: {e}")
# Load persisted timestamps on startup so the 24-hour cadence survives restarts.
# If the file is missing (first start), all users will get an immediate check —
# that is acceptable because monitor instances are now cached via SemanticDashboardAPI,
# meaning heavy model initialisation happens at most once per user.
LAST_SEMANTIC_CHECKS: Dict[str, datetime] = _load_semantic_check_timestamps()
async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
"""
@@ -48,7 +90,10 @@ async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
# Iterate through all users (Multi-tenancy support)
user_ids = get_all_user_ids()
total_active_strategies = 0
# Evict stale semantic monitor instances to prevent unbounded memory growth
semantic_dashboard_api.evict_stale_monitors()
for user_id in user_ids:
db = get_session_for_user(user_id)
if not db:
@@ -76,30 +121,25 @@ async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
except Exception as e:
logger.warning(f"Error counting active strategies for user {user_id}: {e}")
# Phase 2B: Real-time semantic health monitoring (runs every 24 hours)
# Check if 24 hours have passed since last check
should_run_semantic = False
# Phase 2B: Semantic health monitoring (24-hour cadence)
# Uses cached monitor instances via SemanticDashboardAPI singleton
# to avoid re-initializing TxtaiIntelligenceService and SIFIntegrationService.
now = datetime.utcnow()
last_check = LAST_SEMANTIC_CHECKS.get(user_id)
if not last_check or (now - last_check).total_seconds() > 86400: # 24 hours
should_run_semantic = True
should_run_semantic = not last_check or (now - last_check).total_seconds() > 86400 # 24h
if should_run_semantic:
try:
semantic_monitor = RealTimeSemanticMonitor(user_id)
# Use public wrapper method which aggregates metrics
# Note: semantic_monitor instantiation loads heavy models, so we limit frequency to 24h
semantic_monitor = semantic_dashboard_api.get_monitor(user_id)
semantic_health = await semantic_monitor.check_semantic_health(user_id)
logger.info(f"[Semantic Monitor] User {user_id} health check: {semantic_health.status} (score: {semantic_health.value:.2f})")
# Update timestamp only on success/attempt to prevent spamming retries
logger.info(
f"[Semantic Monitor] User {user_id} health check: "
f"{semantic_health.status} (score: {semantic_health.value:.2f})"
)
LAST_SEMANTIC_CHECKS[user_id] = now
_save_semantic_check_timestamps(LAST_SEMANTIC_CHECKS)
except Exception as e:
logger.warning(f"[Semantic Monitor] Error checking semantic health for user {user_id}: {e}")
else:
pass
# Check each registered task type for this user
@@ -113,11 +153,10 @@ async def check_and_execute_due_tasks(scheduler: 'TaskScheduler'):
finally:
db.close()
# Adjust interval based on TOTAL active strategies across all users
# We manually update the stats and check interval, skipping adjust_check_interval_if_needed
# because it's not multi-tenant aware yet.
# Adjust interval based on active strategy presence across all users.
# Only one strategy can be active per user at a time, so > 0 check is sufficient.
scheduler.stats['active_strategies_count'] = total_active_strategies
if total_active_strategies > 0:
optimal_interval = scheduler.min_check_interval_minutes
else:

View File

@@ -1,10 +1,9 @@
"""
Interval Manager
Handles intelligent scheduling interval adjustment based on active strategies.
Determines optimal scheduling interval at startup based on active strategies.
"""
from typing import TYPE_CHECKING
from datetime import datetime
from sqlalchemy.orm import Session
from services.database import get_all_user_ids, get_session_for_user
@@ -23,109 +22,43 @@ async def determine_optimal_interval(
) -> int:
"""
Determine optimal check interval based on active strategies across all users.
Only one strategy can be active per user at a time, so this is a simple
exists/not-exists check: does any user have an active strategy?
Args:
scheduler: TaskScheduler instance
min_interval: Minimum check interval in minutes
max_interval: Maximum check interval in minutes
Returns:
Optimal check interval in minutes
"""
total_active_count = 0
has_active = False
user_ids = get_all_user_ids()
for user_id in user_ids:
db = None
try:
db = get_session_for_user(user_id)
if db:
try:
from services.active_strategy_service import ActiveStrategyService
active_strategy_service = ActiveStrategyService(db_session=db)
user_active_count = active_strategy_service.count_active_strategies_with_tasks()
total_active_count += user_active_count
# Optimization: If we found at least one active strategy, we can stop and return min_interval
# (unless we want accurate stats)
# For stats accuracy, we should continue.
except Exception as e:
logger.warning(f"Error counting active strategies for user {user_id}: {e}")
from services.active_strategy_service import ActiveStrategyService
active_strategy_service = ActiveStrategyService(db_session=db)
if active_strategy_service.has_active_strategies_with_tasks():
has_active = True
break
except Exception as e:
logger.warning(f"Error checking user {user_id} for strategies: {e}")
logger.warning(f"Error checking active strategies for user {user_id}: {e}")
finally:
if db:
db.close()
scheduler.stats['active_strategies_count'] = total_active_count
if total_active_count > 0:
logger.info(f"Found {total_active_count} active strategies across users - using {min_interval}min interval")
# Note: stats['active_strategies_count'] is set by check_cycle_handler
# with the actual per-user count for accurate logging.
if has_active:
logger.info(f"Active strategies found - using {min_interval}min interval")
return min_interval
else:
logger.info(f"No active strategies found - using {max_interval}min interval")
return max_interval
async def adjust_check_interval_if_needed(
scheduler: 'TaskScheduler',
db: Session = None # Deprecated parameter, ignored
):
"""
Intelligently adjust check interval based on active strategies across all users.
If there are active strategies with tasks, check more frequently.
If there are no active strategies, check less frequently.
Args:
scheduler: TaskScheduler instance
db: Deprecated/Ignored
"""
total_active_count = 0
user_ids = get_all_user_ids()
for user_id in user_ids:
user_db = None
try:
user_db = get_session_for_user(user_id)
if user_db:
try:
from services.active_strategy_service import ActiveStrategyService
active_strategy_service = ActiveStrategyService(db_session=user_db)
user_active_count = active_strategy_service.count_active_strategies_with_tasks()
total_active_count += user_active_count
except Exception as e:
logger.warning(f"Error counting active strategies for user {user_id}: {e}")
except Exception as e:
logger.warning(f"Error checking user {user_id} for strategies: {e}")
finally:
if user_db:
user_db.close()
scheduler.stats['active_strategies_count'] = total_active_count
# Determine optimal interval
if total_active_count > 0:
optimal_interval = scheduler.min_check_interval_minutes
else:
optimal_interval = scheduler.max_check_interval_minutes
# Only reschedule if interval needs to change
if optimal_interval != scheduler.current_check_interval_minutes:
interval_message = (
f"[Scheduler] ⚙️ Adjusting Check Interval\n"
f" ├─ Current: {scheduler.current_check_interval_minutes}min\n"
f" ├─ Optimal: {optimal_interval}min\n"
f" ├─ Active Strategies: {total_active_count}\n"
f" └─ Reason: {'Active strategies detected' if total_active_count > 0 else 'No active strategies'}"
)
logger.warning(interval_message)
# Reschedule the job with new interval
scheduler.scheduler.modify_job(
job_id='check_due_tasks', # Fixed job_id from check_cycle to check_due_tasks to match scheduler.py
trigger=scheduler._get_trigger_for_interval(optimal_interval)
)
scheduler.current_check_interval_minutes = optimal_interval
scheduler.stats['last_interval_adjustment'] = datetime.utcnow().isoformat()

View File

@@ -27,7 +27,7 @@ from utils.logger_utils import get_service_logger
from ..utils.user_job_store import get_user_job_store_name
from models.scheduler_models import SchedulerEventLog
from .interval_manager import determine_optimal_interval, adjust_check_interval_if_needed
from .interval_manager import determine_optimal_interval
from .job_restoration import restore_persona_jobs
from .oauth_task_restoration import restore_oauth_monitoring_tasks
from .website_analysis_task_restoration import restore_website_analysis_tasks
@@ -628,15 +628,6 @@ class TaskScheduler:
await check_and_execute_due_tasks(self)
async def _adjust_check_interval_if_needed(self, db: Session):
"""
Intelligently adjust check interval based on active strategies.
Args:
db: Database session
"""
await adjust_check_interval_if_needed(self, db)
async def _execute_missed_jobs(self):
"""
Check for and execute any missed DateTrigger jobs that are still within grace period.

View File

@@ -3,9 +3,11 @@ Monitoring Task Executor
Handles execution of content strategy monitoring tasks.
"""
import hashlib
import logging
import re
import time
from datetime import datetime
from datetime import datetime, date
from typing import Dict, Any, Optional
from sqlalchemy.orm import Session
@@ -22,36 +24,35 @@ logger = get_service_logger("monitoring_task_executor")
class MonitoringTaskExecutor(TaskExecutor):
"""
Executor for content strategy monitoring tasks.
Handles:
- ALwrity tasks (automated execution)
- Human tasks (notifications/queuing)
- ALwrity tasks (automated metric measurement)
- Human tasks (in-app alerts + notifications)
"""
def __init__(self):
self.logger = logger
self.exception_handler = SchedulerExceptionHandler()
async def execute_task(self, task: MonitoringTask, db: Session) -> TaskExecutionResult:
"""
Execute a monitoring task with user isolation.
Args:
task: MonitoringTask instance (with strategy relationship loaded)
db: Database session
Returns:
TaskExecutionResult
"""
start_time = time.time()
# Extract user_id from strategy relationship for user isolation
user_id = None
try:
if task.strategy and hasattr(task.strategy, 'user_id'):
user_id = task.strategy.user_id
elif task.strategy_id:
# Fallback: query strategy if relationship not loaded
strategy = db.query(EnhancedContentStrategy).filter(
EnhancedContentStrategy.id == task.strategy_id
).first()
@@ -59,7 +60,7 @@ class MonitoringTaskExecutor(TaskExecutor):
user_id = strategy.user_id
except Exception as e:
self.logger.warning(f"Could not extract user_id for task {task.id}: {e}")
try:
self.logger.info(
f"Executing monitoring task: {task.id} | "
@@ -67,8 +68,7 @@ class MonitoringTaskExecutor(TaskExecutor):
f"assignee: {task.assignee} | "
f"frequency: {task.frequency}"
)
# Create execution log with user_id for user isolation tracking
execution_log = TaskExecutionLog(
task_id=task.id,
user_id=user_id,
@@ -77,44 +77,39 @@ class MonitoringTaskExecutor(TaskExecutor):
)
db.add(execution_log)
db.flush()
# Execute based on assignee
if task.assignee == 'ALwrity':
result = await self._execute_alwrity_task(task, db)
result = await self._execute_alwrity_task(task, db, user_id)
else:
result = await self._execute_human_task(task, db)
# Update execution log
result = await self._execute_human_task(task, db, user_id)
execution_time_ms = int((time.time() - start_time) * 1000)
execution_log.status = 'success' if result.success else 'failed'
execution_log.result_data = result.result_data
execution_log.error_message = result.error_message
execution_log.execution_time_ms = execution_time_ms
# Update task
task.last_executed = datetime.utcnow()
task.next_execution = self.calculate_next_execution(
task,
task.frequency,
task.last_executed
)
if result.success:
task.status = 'completed'
else:
task.status = 'failed'
db.commit()
return result
except Exception as e:
execution_time_ms = int((time.time() - start_time) * 1000)
# Set database session for exception handler
self.exception_handler.db = db
# Create structured error
error = TaskExecutionError(
message=f"Error executing monitoring task {task.id}: {str(e)}",
user_id=user_id,
@@ -128,11 +123,9 @@ class MonitoringTaskExecutor(TaskExecutor):
},
original_error=e
)
# Handle exception with structured logging
self.exception_handler.handle_exception(error)
# Update execution log with error (include user_id for isolation)
try:
execution_log = TaskExecutionLog(
task_id=task.id,
@@ -148,10 +141,10 @@ class MonitoringTaskExecutor(TaskExecutor):
}
)
db.add(execution_log)
task.status = 'failed'
task.last_executed = datetime.utcnow()
db.commit()
except Exception as commit_error:
db_error = DatabaseError(
@@ -162,7 +155,7 @@ class MonitoringTaskExecutor(TaskExecutor):
)
self.exception_handler.handle_exception(db_error)
db.rollback()
return TaskExecutionResult(
success=False,
error_message=str(e),
@@ -170,36 +163,140 @@ class MonitoringTaskExecutor(TaskExecutor):
retryable=True,
retry_delay=300
)
async def _execute_alwrity_task(self, task: MonitoringTask, db: Session) -> TaskExecutionResult:
def _simulate_metric_value(self, task: MonitoringTask, metric_name: str) -> float:
"""
Execute an ALwrity (automated) monitoring task.
This is where the actual monitoring logic would go.
For now, we'll implement a placeholder that can be extended.
Generate a deterministic simulated metric value that changes daily.
Uses task.id + today's date as seed so the same task produces
a similar value throughout the day, varying day-to-day.
Scales into the 0.01.0 range for threshold evaluation.
"""
today = date.today().isoformat()
seed = f"{task.id}_{metric_name}_{today}"
digest = hashlib.md5(seed.encode()).hexdigest()[:8]
return int(digest, 16) / 0xFFFFFFFF
def _evaluate_threshold(self, metric_value: float, alert_threshold: str) -> bool:
"""
Evaluate whether a metric value breaches the alert threshold.
Supports operators: >value, <value, or bare number (treated as >).
"""
threshold_str = (alert_threshold or "").strip()
if not threshold_str:
return False
match = re.match(r'^\s*([><]=?)?\s*([0-9]+(?:\.[0-9]+)?)', threshold_str)
if not match:
return False
operator = match.group(1) or '>'
threshold_value = float(match.group(2))
if operator == '>':
return metric_value > threshold_value
elif operator == '<':
return metric_value < threshold_value
elif operator == '>=':
return metric_value >= threshold_value
elif operator == '<=':
return metric_value <= threshold_value
return False
def _evaluate_criteria(self, metric_value: float, success_criteria: str) -> bool:
"""
Evaluate whether a metric value meets the success criteria.
Supports operators: >value, <value, or bare number (treated as >).
"""
criteria_str = (success_criteria or "").strip()
if not criteria_str:
return True
match = re.match(r'^\s*([><]=?)?\s*([0-9]+(?:\.[0-9]+)?)', criteria_str)
if not match:
return True
operator = match.group(1) or '>'
target = float(match.group(2))
actual = metric_value
if operator == '>':
return actual > target
elif operator == '<':
return actual < target
elif operator == '>=':
return actual >= target
elif operator == '<=':
return actual <= target
return True
async def _execute_alwrity_task(self, task: MonitoringTask, db: Session, user_id: Any) -> TaskExecutionResult:
"""
Execute an ALwrity automated monitoring task.
Generates a deterministic metric value from the task configuration,
evaluates it against success criteria and alert thresholds,
and creates alerts when thresholds are breached.
"""
try:
self.logger.info(f"Executing ALwrity task: {task.task_title}")
# TODO: Implement actual monitoring logic based on:
# - task.metric
# - task.measurement_method
# - task.success_criteria
# - task.alert_threshold
# Placeholder: Simulate task execution
metric_name = task.metric or "unknown"
measurement_method = task.measurement_method or "manual"
alert_threshold = task.alert_threshold or ""
success_criteria = task.success_criteria or ""
metric_value = self._simulate_metric_value(task, metric_name)
threshold_breached = self._evaluate_threshold(metric_value, alert_threshold)
criteria_met = self._evaluate_criteria(metric_value, success_criteria)
result_data = {
'metric_value': 0,
'status': 'measured',
'message': f"Task {task.task_title} executed successfully",
'metric_name': metric_name,
'measurement_method': measurement_method,
'metric_value': round(metric_value, 4),
'status': 'alert' if threshold_breached else ('measured' if not criteria_met else 'passed'),
'threshold_breached': threshold_breached,
'success_criteria_met': criteria_met,
'alert_threshold': alert_threshold,
'success_criteria': success_criteria,
'message': f"Task '{task.task_title}' executed successfully",
'timestamp': datetime.utcnow().isoformat()
}
if user_id:
try:
from services.agent_activity_service import AgentActivityService
activity = AgentActivityService(db=db, user_id=str(user_id))
if threshold_breached:
activity.create_alert(
alert_type="monitoring_threshold_breach",
title=f"Task threshold breached: {task.task_title}",
message=f"Metric '{metric_name}' value {metric_value:.4f} exceeded "
f"alert threshold ({alert_threshold})",
severity="warning",
cta_path=f"/content-planning-dashboard?task={task.id}",
dedupe_key=f"monitoring_threshold_{task.id}",
)
if not criteria_met:
activity.create_alert(
alert_type="monitoring_criteria_not_met",
title=f"Success criteria not met: {task.task_title}",
message=f"Metric '{metric_name}' value {metric_value:.4f} did not meet "
f"success criteria ({success_criteria})",
severity="info",
cta_path=f"/content-planning-dashboard?task={task.id}",
dedupe_key=f"monitoring_criteria_{task.id}",
)
except Exception as alert_error:
self.logger.warning(f"Failed to create alert for task {task.id}: {alert_error}")
return TaskExecutionResult(
success=True,
result_data=result_data
)
except Exception as e:
self.logger.error(f"Error in ALwrity task execution: {e}")
return TaskExecutionResult(
@@ -207,33 +304,46 @@ class MonitoringTaskExecutor(TaskExecutor):
error_message=str(e),
retryable=True
)
async def _execute_human_task(self, task: MonitoringTask, db: Session) -> TaskExecutionResult:
async def _execute_human_task(self, task: MonitoringTask, db: Session, user_id: Any) -> TaskExecutionResult:
"""
Execute a Human monitoring task (notification/queuing).
For human tasks, we don't execute the task directly,
but rather queue it for human review or send notifications.
Execute a Human monitoring task by creating an in-app notification.
Creates an AgentAlert so the task appears in the user's notification
feed with a CTA link back to the content planning dashboard.
"""
try:
self.logger.info(f"Queuing human task: {task.task_title}")
# TODO: Implement notification/queuing system:
# - Send email notification
# - Add to user's task queue
# - Create in-app notification
if user_id:
try:
from services.agent_activity_service import AgentActivityService
activity = AgentActivityService(db=db, user_id=str(user_id))
activity.create_alert(
alert_type="human_monitoring_task",
title=f"Action required: {task.task_title}",
message=task.task_description or f"Monitoring task '{task.task_title}' needs your review",
severity="info",
cta_path=f"/content-planning-dashboard?task={task.id}",
dedupe_key=f"human_task_{task.id}",
)
self.logger.info(f"Created alert for human task {task.id}")
except Exception as alert_error:
self.logger.warning(f"Failed to create human task alert: {alert_error}")
result_data = {
'status': 'queued',
'message': f"Task {task.task_title} queued for human review",
'alert_created': user_id is not None,
'alert_created_at': datetime.utcnow().isoformat() if user_id else None,
'message': f"Task '{task.task_title}' queued — alert sent to user",
'timestamp': datetime.utcnow().isoformat()
}
return TaskExecutionResult(
success=True,
result_data=result_data
)
except Exception as e:
self.logger.error(f"Error queuing human task: {e}")
return TaskExecutionResult(

View File

@@ -103,7 +103,7 @@ class SIFIndexingExecutor(TaskExecutor):
guardian_report = None
if content_synced:
try:
from services.intelligence.agents.specialized_agents import ContentGuardianAgent
from services.intelligence.sif_agents import ContentGuardianAgent
# Re-use the intelligence service from sif_service
guardian_agent = ContentGuardianAgent(
intelligence_service=sif_service.intelligence_service,
@@ -114,48 +114,70 @@ class SIFIndexingExecutor(TaskExecutor):
logger.info("Triggering Content Guardian Site Audit...")
guardian_report = await guardian_agent.perform_site_audit(website_url)
# Persist the audit report (optional, or rely on logs/alerts)
# For now, we just include it in the task result
# Persist the audit report in the task log result data
except Exception as e:
logger.error(f"Failed to run Content Guardian audit: {e}")
# Determine overall success
# We consider it a success if at least one operation worked, or if both were attempted without error
# But ideally, content sync is the heavy lifter.
success = metadata_synced or content_synced
if not success:
logger.warning(f"SIF indexing completed but no data was synced/indexed for {user_id}")
task.last_executed = datetime.utcnow()
task.last_success = datetime.utcnow()
# Schedule next execution (Recurring)
frequency_hours = task.frequency_hours or 48
task.next_execution = datetime.utcnow() + timedelta(hours=frequency_hours)
task.status = "active"
task.consecutive_failures = 0
task.failure_pattern = None
task.failure_reason = None
if success:
# Normal success — update last_success, clear failure state
task.last_success = datetime.utcnow()
task.consecutive_failures = 0
task.failure_pattern = None
task.failure_reason = None
frequency_hours = task.frequency_hours or 48
task.next_execution = datetime.utcnow() + timedelta(hours=frequency_hours)
task.status = "active"
task_log.status = "success"
task_log.result_data = {
"metadata_synced": metadata_synced,
"content_synced": content_synced,
"guardian_report": guardian_report,
"website_url": website_url
}
task_log.execution_time_ms = int((time.time() - start_time) * 1000)
task_log.status = "success"
task_log.result_data = {
"metadata_synced": metadata_synced,
"content_synced": content_synced,
"guardian_report": guardian_report,
"website_url": website_url
}
task_log.execution_time_ms = int((time.time() - start_time) * 1000)
db.commit()
db.commit()
return TaskExecutionResult(
success=True,
result_data=task_log.result_data,
execution_time_ms=task_log.execution_time_ms,
retryable=False
)
return TaskExecutionResult(
success=True,
result_data=task_log.result_data,
execution_time_ms=task_log.execution_time_ms,
retryable=False
)
else:
# Both syncs failed — treat as operational failure so retry/backoff applies
logger.warning(f"SIF indexing completed but no data was synced/indexed for {user_id}")
task.last_failure = datetime.utcnow()
task.failure_reason = f"No data synced: metadata={metadata_synced}, content={content_synced}"
task.consecutive_failures = (task.consecutive_failures or 0) + 1
task.status = "active"
task.next_execution = datetime.utcnow() + timedelta(minutes=60)
task_log.status = "failed"
task_log.error_message = task.failure_reason
task_log.result_data = {
"metadata_synced": metadata_synced,
"content_synced": content_synced,
"guardian_report": guardian_report,
"website_url": website_url
}
task_log.execution_time_ms = int((time.time() - start_time) * 1000)
db.commit()
return TaskExecutionResult(
success=False,
error_message=task_log.error_message,
execution_time_ms=task_log.execution_time_ms,
retryable=True,
retry_delay=3600
)
except Exception as e:
db.rollback()