| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- """计划组落库后执行分级任务(默认定时任务路径为代码自动分配,非统筹 Agent)。"""
- from __future__ import annotations
- import logging
- from concurrent.futures import ThreadPoolExecutor, as_completed
- from datetime import datetime
- from typing import Any
- from zoneinfo import ZoneInfo
- from agents.demand_grade_agent.run import main as grade_demand_words
- from supply_infra.config import get_infra_settings
- from supply_infra.scheduler.auto_assign_grade_plan import auto_assign_daily_grade_plan
- from supply_infra.db.repositories.demand_grade_plan_repo import DemandGradePlanRepository
- from supply_infra.db.repositories.demand_grade_repo import DemandGradeRepository
- from supply_infra.db.repositories.multi_demand_pool_di_repo import MultiDemandPoolDiRepository
- from supply_infra.db.session import get_session
- from supply_infra.scheduler.plan_group_batch import MAX_DEMANDS_PER_BATCH, split_even_batches
- logger = logging.getLogger(__name__)
- _DEFAULT_WORKERS = 5
- def _resolve_biz_dt(biz_dt: str | None) -> str:
- if biz_dt:
- return biz_dt
- return datetime.now(ZoneInfo(get_infra_settings().scheduler_timezone)).strftime("%Y%m%d")
- def _materialize_pending_group_items(biz_dt: str) -> int:
- """执行前为 pending 计划组物化待分级需求明细。"""
- with get_session() as session:
- graded_names = DemandGradeRepository(session).get_existing_demand_names(biz_dt)
- return DemandGradePlanRepository(session).materialize_pending_groups(
- biz_dt,
- graded_names=graded_names,
- )
- def _group_success(item_summary: dict[str, int], processed_batches: int) -> bool:
- if item_summary.get("pending", 0) > 0:
- return False
- total = sum(item_summary.values())
- if total == 0:
- return True
- if processed_batches > 0:
- return True
- return item_summary.get("failed", 0) == 0 and (
- item_summary.get("finished", 0) > 0 or item_summary.get("skipped", 0) > 0
- )
- def _run_group(biz_dt: str, group_id: int, *, max_demands_per_batch: int) -> int:
- """领取并执行一个固定任务,从组内需求明细表按批读取。"""
- with get_session() as session:
- group = DemandGradePlanRepository(session).claim_group(biz_dt, group_id)
- if group is None:
- logger.warning("分级任务已被其他 worker 领取或状态已变化: biz_dt=%s group_id=%s", biz_dt, group_id)
- return 0
- gid = int(group["id"])
- processed_batches = 0
- batch_errors: list[str] = []
- try:
- with get_session() as session:
- items = DemandGradePlanRepository(session).list_pending_group_items(gid)
- for batch_items in split_even_batches(items, max_per_batch=max_demands_per_batch):
- demands = [
- {"pool_id": item["pool_id"], "demand_name": item["demand_name"]}
- for item in batch_items
- ]
- item_ids = [int(item["item_id"]) for item in batch_items]
- try:
- grade_demand_words(demands, biz_dt=biz_dt)
- with get_session() as session:
- DemandGradePlanRepository(session).mark_group_items_status(
- item_ids,
- status="finished",
- )
- processed_batches += 1
- except Exception as exc:
- logger.exception(
- "分级子批次失败,跳过并继续本组其余需求: biz_dt=%s group=%s count=%s",
- biz_dt,
- group["group_key"],
- len(demands),
- )
- batch_errors.append(str(exc))
- with get_session() as session:
- DemandGradePlanRepository(session).mark_group_items_status(
- item_ids,
- status="failed",
- error_message=str(exc),
- )
- with get_session() as session:
- repo = DemandGradePlanRepository(session)
- item_summary = repo.summarize_group_items(gid)
- repo.finish_group(
- gid,
- success=_group_success(item_summary, processed_batches),
- error_message="; ".join(batch_errors) if batch_errors else None,
- )
- except Exception as exc:
- logger.exception(
- "分级计划任务失败: biz_dt=%s group=%s",
- biz_dt,
- group["group_key"],
- )
- try:
- with get_session() as session:
- DemandGradePlanRepository(session).finish_group(
- gid,
- success=False,
- error_message=str(exc),
- )
- except Exception:
- logger.exception(
- "记录分级计划任务失败状态时发生错误: biz_dt=%s group=%s",
- biz_dt,
- group["group_key"],
- )
- return 0
- return processed_batches
- def _execute_plan_tasks(
- biz_dt: str,
- *,
- workers: int,
- max_demands_per_batch: int,
- ) -> dict[str, Any]:
- """并发执行当天全部 pending 计划任务,仅执行一轮。"""
- with get_session() as session:
- group_ids = DemandGradePlanRepository(session).list_pending_group_ids(biz_dt)
- if not group_ids:
- with get_session() as session:
- final_snapshot = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
- return {
- "attempted_groups": 0,
- "groups_run": 0,
- "workers": 0,
- "final_snapshot": final_snapshot,
- "execution_complete": final_snapshot["execution_complete"],
- }
- worker_count = max(1, min(int(workers), len(group_ids)))
- groups_run = 0
- with ThreadPoolExecutor(max_workers=worker_count) as executor:
- futures = [
- executor.submit(_run_group, biz_dt, group_id, max_demands_per_batch=max_demands_per_batch)
- for group_id in group_ids
- ]
- for future in as_completed(futures):
- try:
- groups_run += future.result()
- except Exception:
- logger.exception("分级任务 worker 出现未捕获错误: biz_dt=%s", biz_dt)
- with get_session() as session:
- final_snapshot = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
- return {
- "attempted_groups": len(group_ids),
- "groups_run": groups_run,
- "workers": worker_count,
- "final_snapshot": final_snapshot,
- "execution_complete": final_snapshot["execution_complete"],
- }
- def execute_plan_tasks_until_complete(
- biz_dt: str,
- *,
- workers: int,
- max_demands_per_batch: int = MAX_DEMANDS_PER_BATCH,
- max_rounds: int = 0,
- ) -> dict[str, Any]:
- """循环执行 pending 计划任务,直到全部完成或达到 max_rounds。"""
- rounds: list[dict[str, Any]] = []
- groups_run = 0
- round_no = 0
- while True:
- if max_rounds > 0 and round_no >= max_rounds:
- break
- round_no += 1
- result = _execute_plan_tasks(
- biz_dt,
- workers=workers,
- max_demands_per_batch=max_demands_per_batch,
- )
- rounds.append(result)
- groups_run += int(result.get("groups_run") or 0)
- if int(result.get("attempted_groups") or 0) == 0:
- break
- final_snapshot = rounds[-1]["final_snapshot"] if rounds else None
- if final_snapshot is None:
- with get_session() as session:
- final_snapshot = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
- return {
- "rounds": round_no,
- "groups_run": groups_run,
- "workers": max(1, int(workers)),
- "round_details": rounds,
- "final_snapshot": final_snapshot,
- "execution_complete": bool(final_snapshot["execution_complete"]),
- }
- def _grade_demand_pool_impl(
- resolved_biz_dt: str,
- *,
- workers: int,
- max_demands_per_batch: int = MAX_DEMANDS_PER_BATCH,
- with_orchestrate: bool = True,
- max_rounds: int = 0,
- ) -> dict[str, Any]:
- with get_session() as session:
- total = MultiDemandPoolDiRepository(session).count_distinct_demand_names(resolved_biz_dt)
- graded_before = DemandGradeRepository(session).count_by_biz_dt(resolved_biz_dt)
- if with_orchestrate:
- try:
- auto_assign_daily_grade_plan(
- biz_dt=resolved_biz_dt,
- max_demands_per_group=max_demands_per_batch,
- )
- except Exception:
- logger.exception("自动分配计划组失败,继续处理数据库中已有任务: biz_dt=%s", resolved_biz_dt)
- materialized = _materialize_pending_group_items(resolved_biz_dt)
- logger.info("物化计划组需求明细: biz_dt=%s items=%s", resolved_biz_dt, materialized)
- plan_execution = execute_plan_tasks_until_complete(
- resolved_biz_dt,
- workers=max(1, int(workers)),
- max_demands_per_batch=max(1, min(int(max_demands_per_batch), MAX_DEMANDS_PER_BATCH)),
- max_rounds=max_rounds,
- )
- with get_session() as session:
- graded_after = DemandGradeRepository(session).count_by_biz_dt(resolved_biz_dt)
- final_snapshot = plan_execution["final_snapshot"]
- result = {
- "success": bool(plan_execution["execution_complete"]),
- "biz_dt": resolved_biz_dt,
- "total": total,
- "graded_before": graded_before,
- "graded_after": graded_after,
- "materialized_items": materialized,
- "planned_category_count": len(final_snapshot["assigned_category_ids"]),
- "planned_groups": final_snapshot["planned_groups"],
- "group_status": final_snapshot["group_status"],
- "plan_execution": plan_execution,
- "workers": plan_execution["workers"],
- "groups_run": plan_execution["groups_run"],
- "run_at": datetime.now().isoformat(),
- }
- logger.info("Grade demand pool completed: %s", result)
- return result
- def retry_failed_plan_group_items(
- biz_dt: str | None = None,
- *,
- workers: int = _DEFAULT_WORKERS,
- max_demands_per_batch: int = MAX_DEMANDS_PER_BATCH,
- group_ids: list[int] | None = None,
- dry_run: bool = False,
- ) -> dict[str, Any]:
- """将 demand_grade_plan_group_item 中 failed 记录重置后重新执行分级。"""
- resolved_biz_dt = _resolve_biz_dt(biz_dt)
- try:
- with get_session() as session:
- failed_items = DemandGradePlanRepository(session).list_failed_group_items(
- resolved_biz_dt,
- group_ids=group_ids,
- )
- if not failed_items:
- with get_session() as session:
- snapshot = DemandGradePlanRepository(session).get_execution_snapshot(resolved_biz_dt)
- return {
- "success": True,
- "biz_dt": resolved_biz_dt,
- "dry_run": dry_run,
- "failed_items": 0,
- "reset": {"reset_items": 0, "reset_groups": 0, "group_ids": [], "items": []},
- "execution": None,
- "final_snapshot": snapshot,
- "run_at": datetime.now().isoformat(),
- }
- if dry_run:
- affected_group_ids = sorted({int(item["group_id"]) for item in failed_items})
- return {
- "success": True,
- "biz_dt": resolved_biz_dt,
- "dry_run": True,
- "failed_items": len(failed_items),
- "reset": {
- "reset_items": len(failed_items),
- "reset_groups": len(affected_group_ids),
- "group_ids": affected_group_ids,
- "items": failed_items,
- },
- "execution": None,
- "run_at": datetime.now().isoformat(),
- }
- with get_session() as session:
- reset_result = DemandGradePlanRepository(session).reset_failed_items_to_pending(
- resolved_biz_dt,
- group_ids=group_ids,
- )
- with get_session() as session:
- graded_before = DemandGradeRepository(session).count_by_biz_dt(resolved_biz_dt)
- plan_execution = execute_plan_tasks_until_complete(
- resolved_biz_dt,
- workers=max(1, int(workers)),
- max_demands_per_batch=max(1, min(int(max_demands_per_batch), MAX_DEMANDS_PER_BATCH)),
- )
- with get_session() as session:
- graded_after = DemandGradeRepository(session).count_by_biz_dt(resolved_biz_dt)
- remaining_failed = DemandGradePlanRepository(session).list_failed_group_items(
- resolved_biz_dt,
- group_ids=group_ids,
- )
- final_snapshot = plan_execution["final_snapshot"]
- return {
- "success": len(remaining_failed) == 0,
- "biz_dt": resolved_biz_dt,
- "dry_run": False,
- "failed_items": len(failed_items),
- "reset": reset_result,
- "graded_before": graded_before,
- "graded_after": graded_after,
- "remaining_failed": len(remaining_failed),
- "execution": plan_execution,
- "final_snapshot": final_snapshot,
- "group_status": final_snapshot["group_status"],
- "run_at": datetime.now().isoformat(),
- }
- except Exception as exc:
- logger.exception(
- "重试 failed plan group items 发生未捕获错误: biz_dt=%s",
- resolved_biz_dt,
- )
- return {
- "success": False,
- "biz_dt": resolved_biz_dt,
- "error": str(exc),
- "run_at": datetime.now().isoformat(),
- }
- def grade_demand_pool(
- biz_dt: str | None = None,
- *,
- workers: int = _DEFAULT_WORKERS,
- max_demands_per_batch: int = MAX_DEMANDS_PER_BATCH,
- with_orchestrate: bool = True,
- max_rounds: int = 0,
- ) -> dict[str, Any]:
- """执行完整分级闭环;任何错误只记录日志并返回,不向上中断定时任务。"""
- resolved_biz_dt = str(biz_dt or "")
- try:
- resolved_biz_dt = _resolve_biz_dt(biz_dt)
- return _grade_demand_pool_impl(
- resolved_biz_dt,
- workers=workers,
- max_demands_per_batch=max_demands_per_batch,
- with_orchestrate=with_orchestrate,
- max_rounds=max_rounds,
- )
- except Exception as exc:
- logger.exception("需求分级任务发生未捕获错误,已阻止异常中断定时任务: biz_dt=%s", resolved_biz_dt)
- return {
- "success": False,
- "biz_dt": resolved_biz_dt,
- "error": str(exc),
- "run_at": datetime.now().isoformat(),
- }
- if __name__ == "__main__":
- import sys
- from supply_infra.scheduler.cli_result import run_cli
- _argv = [a for a in sys.argv[1:] if a != "--retry-failed"]
- _retry_failed = "--retry-failed" in sys.argv[1:]
- _biz_dt = _argv[0] if len(_argv) > 0 else None
- _workers = int(_argv[1]) if len(_argv) > 1 else 5
- def _main() -> dict[str, Any]:
- if _retry_failed:
- return retry_failed_plan_group_items(_biz_dt, workers=_workers)
- return grade_demand_pool(
- _biz_dt,
- workers=_workers,
- with_orchestrate=True,
- )
- run_cli(_main, label="grade_demand_pool")
|