Files
planet/backend/app/services/earth_news_worker.py
linkong 93eb41a9f7
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.58.0
Release 0.58.0 includes the Earth high-precision boundary PMTiles/MVT pipeline, standardized Earth boundary source collectors, China POV boundary configuration templates, and removal of the legacy low-precision GeoJSON fallback. It also adds Earth news target-location queueing/archive support, fixes datasource task status visibility, documents the Earth surface depth-spacing rules that prevent far-zoom z-fighting snow/black blocks, and updates bilingual operations/developer docs.
2026-05-15 17:40:07 +08:00

134 lines
4.4 KiB
Python

from __future__ import annotations
import asyncio
from contextlib import suppress
from socket import gethostname
from typing import Any
from app.core.logging import get_logger
from app.core.websocket.broadcaster import broadcaster
from app.db.session import async_session_factory
from app.services.ai_client import AIProviderClient
from app.services.earth_news import (
_infer_news_target_location,
build_target_location_patch,
parsed_news_item_from_job_payload,
)
from app.services.earth_news_queue import (
NewsTargetLocationMessage,
get_news_target_location_queue,
save_target_location_patch,
)
from app.services.earth_news_store import update_earth_news_item_location
logger = get_logger(__name__, service="earth_news")
WORKER_BATCH_SIZE = 4
WORKER_BLOCK_MS = 5000
WORKER_BACKOFF_SECONDS = 5.0
_worker_task: asyncio.Task | None = None
async def _build_provider_client() -> AIProviderClient | None:
try:
from app.api.v1.settings import get_runtime_ai_provider_config
async with async_session_factory() as session:
runtime_config = await get_runtime_ai_provider_config(session)
return AIProviderClient(
service_url=runtime_config["service_url"],
service_token=runtime_config["service_token"],
timeout=runtime_config["timeout_seconds"],
retry_attempts=runtime_config["retry_attempts"],
llm_config=runtime_config.get("llm_config") or {},
)
except Exception as exc:
logger.warning_event(
"Failed to build Earth news AI provider client",
event="earth_news.target_location.provider_unavailable",
context={"error": str(exc)},
)
return None
async def process_target_location_message(
message: NewsTargetLocationMessage,
*,
provider_client: AIProviderClient | None,
) -> dict[str, Any]:
item = parsed_news_item_from_job_payload(message.payload)
target = await _infer_news_target_location(item, provider_client=provider_client)
item.target_location = target
patch = build_target_location_patch(item, target)
await save_target_location_patch(item.id, patch)
async with async_session_factory() as session:
await update_earth_news_item_location(session, item_id=item.id, patch=patch)
await session.commit()
await broadcaster.broadcast_custom(
"earth_news",
{
"item_id": item.id,
"patch": patch,
},
)
return patch
async def _run_target_location_worker() -> None:
consumer_name = f"{gethostname()}:{id(asyncio.current_task())}"
queue = get_news_target_location_queue()
while True:
try:
messages = await queue.consume_batch(
consumer_name=consumer_name,
count=WORKER_BATCH_SIZE,
block_ms=WORKER_BLOCK_MS,
)
except asyncio.CancelledError:
raise
except Exception as exc:
logger.warning_event(
"Earth news target location worker queue read failed",
event="earth_news.target_location.worker_read_failed",
context={"error": str(exc)},
)
await asyncio.sleep(WORKER_BACKOFF_SECONDS)
continue
if not messages:
continue
provider_client = await _build_provider_client()
for message in messages:
try:
await process_target_location_message(message, provider_client=provider_client)
await queue.ack(message.message_id)
except asyncio.CancelledError:
raise
except Exception as exc:
logger.warning_event(
"Earth news target location worker job failed",
event="earth_news.target_location.worker_job_failed",
context={"item_id": message.item_id, "error": str(exc)},
)
with suppress(Exception):
await queue.retry_or_dead_letter(message, error=str(exc))
def start_earth_news_target_worker() -> None:
global _worker_task
if _worker_task is None or _worker_task.done():
_worker_task = asyncio.create_task(_run_target_location_worker())
async def stop_earth_news_target_worker() -> None:
global _worker_task
task = _worker_task
if task is None:
return
task.cancel()
with suppress(asyncio.CancelledError):
await task
_worker_task = None