|
|
@@ -2,12 +2,25 @@
|
|
|
从全部 S/A 级需求及其拓展点位触发 find_agent 视频发现;有效视频满 300 提前结束。
|
|
|
|
|
|
任务层负责查库与组装上下文;Agent 负责搜索、画像与分池落库。
|
|
|
+
|
|
|
+调度模型:
|
|
|
+1. 当天全部待执行需求先入内存任务队列(不预建 DB 记录);
|
|
|
+2. 维护最多 N 个 worker 线程循环取任务;
|
|
|
+3. 执行前检查当日通过视频数,达标后停止领取新任务(允许略超);
|
|
|
+4. 单任务超时(默认 600s)后该 worker 退出,监督线程自动补齐到 N。
|
|
|
"""
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import logging
|
|
|
+import queue
|
|
|
+import threading
|
|
|
import uuid
|
|
|
-from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
|
+from concurrent.futures import (
|
|
|
+ FIRST_COMPLETED,
|
|
|
+ ThreadPoolExecutor,
|
|
|
+ TimeoutError as FuturesTimeoutError,
|
|
|
+ wait,
|
|
|
+)
|
|
|
from datetime import datetime
|
|
|
from time import monotonic
|
|
|
from typing import Any
|
|
|
@@ -20,6 +33,7 @@ from agents.find_agent.demand_run import (
|
|
|
list_find_demand_contexts,
|
|
|
serialize_find_demand_context,
|
|
|
)
|
|
|
+from agents.find_agent.runtime import find_agent_timeout_seconds
|
|
|
from supply_infra.config import get_infra_settings
|
|
|
from supply_infra.db.repositories.demand_grade_repo import DemandGradeRepository
|
|
|
from supply_infra.db.session import ensure_mysql_pool_capacity, get_session
|
|
|
@@ -27,8 +41,10 @@ from supply_infra.services.video_discovery_service import get_video_discovery_se
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
-_DEFAULT_WORKERS = 1
|
|
|
+_DEFAULT_WORKERS = 5
|
|
|
_DAILY_PASSED_VIDEO_LIMIT = 300
|
|
|
+_SUPERVISOR_POLL_SECONDS = 0.5
|
|
|
+_QUEUE_GET_TIMEOUT_SECONDS = 1.0
|
|
|
|
|
|
|
|
|
def _resolve_biz_dt(biz_dt: str | None) -> str:
|
|
|
@@ -52,7 +68,10 @@ def process_single_discover(
|
|
|
*,
|
|
|
force: bool = False,
|
|
|
) -> dict[str, Any]:
|
|
|
- """并发 worker:对单条需求记录执行 find_agent。"""
|
|
|
+ """并发 worker:对单条需求记录执行 find_agent。
|
|
|
+
|
|
|
+ video_discovery_run 记录在 discover_videos_for_demand 开始执行时创建。
|
|
|
+ """
|
|
|
summary = serialize_find_demand_context(ctx)
|
|
|
logger.info(
|
|
|
"discover videos started: grade_id=%s demand=%s videos=%d points=%d",
|
|
|
@@ -127,6 +146,327 @@ def _count_passed_videos(biz_dt: str) -> int:
|
|
|
return get_video_discovery_service().count_passed_videos(biz_dt)
|
|
|
|
|
|
|
|
|
+def _apply_item_result(result: dict[str, Any], item_result: dict[str, Any]) -> None:
|
|
|
+ result["processed"] += 1
|
|
|
+ if item_result.get("skipped"):
|
|
|
+ result["skipped"] += 1
|
|
|
+ return
|
|
|
+ if item_result.get("success"):
|
|
|
+ result["succeeded"] += 1
|
|
|
+ business_outcome = item_result.get("business_outcome")
|
|
|
+ if business_outcome in {"goal_met", "partial", "no_match"}:
|
|
|
+ result[str(business_outcome)] += 1
|
|
|
+ return
|
|
|
+
|
|
|
+ result["failed"] += 1
|
|
|
+ result["errors"].append(
|
|
|
+ {
|
|
|
+ "demand_grade_id": item_result.get("demand_grade_id"),
|
|
|
+ "demand_name": item_result.get("demand_name"),
|
|
|
+ "video_count": item_result.get("video_count"),
|
|
|
+ "run_id": item_result.get("run_id"),
|
|
|
+ "error": item_result.get("error"),
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _run_task_with_timeout(
|
|
|
+ ctx: FindDemandContext,
|
|
|
+ *,
|
|
|
+ force: bool,
|
|
|
+ timeout_seconds: float,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ """在独立线程中执行单任务,超时后向上抛出 FuturesTimeoutError。
|
|
|
+
|
|
|
+ 超时后不阻塞等待底层线程(shutdown wait=False),以便 worker 能退出并由监督方补齐。
|
|
|
+ """
|
|
|
+ executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="find-task")
|
|
|
+ try:
|
|
|
+ future = executor.submit(process_single_discover, ctx, force=force)
|
|
|
+ return future.result(timeout=timeout_seconds)
|
|
|
+ finally:
|
|
|
+ executor.shutdown(wait=False, cancel_futures=True)
|
|
|
+
|
|
|
+
|
|
|
+def _drain_task_queue(task_queue: queue.Queue[FindDemandContext]) -> int:
|
|
|
+ discarded = 0
|
|
|
+ while True:
|
|
|
+ try:
|
|
|
+ task_queue.get_nowait()
|
|
|
+ except queue.Empty:
|
|
|
+ break
|
|
|
+ discarded += 1
|
|
|
+ task_queue.task_done()
|
|
|
+ return discarded
|
|
|
+
|
|
|
+
|
|
|
+def _run_worker_pool(
|
|
|
+ *,
|
|
|
+ contexts: list[FindDemandContext],
|
|
|
+ biz_dt: str,
|
|
|
+ worker_count: int,
|
|
|
+ force: bool,
|
|
|
+ result: dict[str, Any],
|
|
|
+ task_timeout_seconds: float,
|
|
|
+) -> None:
|
|
|
+ """任务队列 + 固定规模 worker 池;超时退出后自动补齐线程。"""
|
|
|
+ task_queue: queue.Queue[FindDemandContext] = queue.Queue()
|
|
|
+ for ctx in contexts:
|
|
|
+ task_queue.put(ctx)
|
|
|
+
|
|
|
+ stop_event = threading.Event()
|
|
|
+ stats_lock = threading.Lock()
|
|
|
+ worker_seq = 0
|
|
|
+ started_at = monotonic()
|
|
|
+
|
|
|
+ def worker_loop(worker_id: int) -> str:
|
|
|
+ """循环取任务;单任务超时则退出,由监督方补线程。"""
|
|
|
+ logger.info(
|
|
|
+ "discover worker started: worker_id=%s biz_dt=%s timeout_seconds=%.0f",
|
|
|
+ worker_id,
|
|
|
+ biz_dt,
|
|
|
+ task_timeout_seconds,
|
|
|
+ )
|
|
|
+ exit_reason = "queue_empty"
|
|
|
+ while not stop_event.is_set():
|
|
|
+ try:
|
|
|
+ ctx = task_queue.get(timeout=_QUEUE_GET_TIMEOUT_SECONDS)
|
|
|
+ except queue.Empty:
|
|
|
+ if stop_event.is_set() or task_queue.empty():
|
|
|
+ exit_reason = "queue_empty"
|
|
|
+ break
|
|
|
+ continue
|
|
|
+
|
|
|
+ try:
|
|
|
+ if stop_event.is_set():
|
|
|
+ exit_reason = "stopped"
|
|
|
+ break
|
|
|
+
|
|
|
+ try:
|
|
|
+ passed_videos = _count_passed_videos(biz_dt)
|
|
|
+ except Exception:
|
|
|
+ logger.exception(
|
|
|
+ "discover worker count_passed_videos failed: worker_id=%s",
|
|
|
+ worker_id,
|
|
|
+ )
|
|
|
+ with stats_lock:
|
|
|
+ result["failed"] += 1
|
|
|
+ result["processed"] += 1
|
|
|
+ result["errors"].append(
|
|
|
+ {
|
|
|
+ "demand_grade_id": ctx.demand_grade_id,
|
|
|
+ "demand_name": ctx.demand_name,
|
|
|
+ "error": "count_passed_videos_failed",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ with stats_lock:
|
|
|
+ result["passed_videos"] = passed_videos
|
|
|
+
|
|
|
+ if passed_videos >= _DAILY_PASSED_VIDEO_LIMIT:
|
|
|
+ with stats_lock:
|
|
|
+ result["stopped_by_passed_video_limit"] = True
|
|
|
+ stop_event.set()
|
|
|
+ exit_reason = "passed_video_limit"
|
|
|
+ logger.info(
|
|
|
+ "discover worker stop by daily limit before execute: "
|
|
|
+ "worker_id=%s grade_id=%s demand=%s passed_videos=%s",
|
|
|
+ worker_id,
|
|
|
+ ctx.demand_grade_id,
|
|
|
+ ctx.demand_name,
|
|
|
+ passed_videos,
|
|
|
+ )
|
|
|
+ break
|
|
|
+
|
|
|
+ logger.info(
|
|
|
+ "discover worker pickup: worker_id=%s grade_id=%s demand=%s "
|
|
|
+ "queue_remaining~=%s passed_videos=%s",
|
|
|
+ worker_id,
|
|
|
+ ctx.demand_grade_id,
|
|
|
+ ctx.demand_name,
|
|
|
+ task_queue.qsize(),
|
|
|
+ passed_videos,
|
|
|
+ )
|
|
|
+
|
|
|
+ try:
|
|
|
+ item_result = _run_task_with_timeout(
|
|
|
+ ctx,
|
|
|
+ force=force,
|
|
|
+ timeout_seconds=task_timeout_seconds,
|
|
|
+ )
|
|
|
+ except FuturesTimeoutError:
|
|
|
+ error_text = (
|
|
|
+ f"find_agent worker timed out after {task_timeout_seconds:.0f}s"
|
|
|
+ )
|
|
|
+ logger.error(
|
|
|
+ "discover worker task timeout, worker will exit: "
|
|
|
+ "worker_id=%s grade_id=%s demand=%s timeout_seconds=%.0f",
|
|
|
+ worker_id,
|
|
|
+ ctx.demand_grade_id,
|
|
|
+ ctx.demand_name,
|
|
|
+ task_timeout_seconds,
|
|
|
+ )
|
|
|
+ with stats_lock:
|
|
|
+ result["failed"] += 1
|
|
|
+ result["processed"] += 1
|
|
|
+ result["errors"].append(
|
|
|
+ {
|
|
|
+ "demand_grade_id": ctx.demand_grade_id,
|
|
|
+ "demand_name": ctx.demand_name,
|
|
|
+ "video_count": ctx.video_count,
|
|
|
+ "error": error_text,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ exit_reason = "task_timeout"
|
|
|
+ break
|
|
|
+ except Exception as exc:
|
|
|
+ logger.exception(
|
|
|
+ "discover worker unexpected error: worker_id=%s grade_id=%s demand=%s",
|
|
|
+ worker_id,
|
|
|
+ ctx.demand_grade_id,
|
|
|
+ ctx.demand_name,
|
|
|
+ )
|
|
|
+ with stats_lock:
|
|
|
+ result["failed"] += 1
|
|
|
+ result["processed"] += 1
|
|
|
+ result["errors"].append(
|
|
|
+ {
|
|
|
+ "demand_grade_id": ctx.demand_grade_id,
|
|
|
+ "demand_name": ctx.demand_name,
|
|
|
+ "error": str(exc),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ with stats_lock:
|
|
|
+ _apply_item_result(result, item_result)
|
|
|
+ logger.info(
|
|
|
+ "discover worker item finished: worker_id=%s grade_id=%s demand=%s "
|
|
|
+ "success=%s skipped=%s total_processed=%d",
|
|
|
+ worker_id,
|
|
|
+ ctx.demand_grade_id,
|
|
|
+ ctx.demand_name,
|
|
|
+ item_result.get("success"),
|
|
|
+ item_result.get("skipped"),
|
|
|
+ result["processed"],
|
|
|
+ )
|
|
|
+
|
|
|
+ try:
|
|
|
+ passed_videos = _count_passed_videos(biz_dt)
|
|
|
+ except Exception:
|
|
|
+ logger.exception(
|
|
|
+ "discover worker post-count failed: worker_id=%s",
|
|
|
+ worker_id,
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ with stats_lock:
|
|
|
+ result["passed_videos"] = passed_videos
|
|
|
+ if passed_videos >= _DAILY_PASSED_VIDEO_LIMIT:
|
|
|
+ result["stopped_by_passed_video_limit"] = True
|
|
|
+ stop_event.set()
|
|
|
+ exit_reason = "passed_video_limit"
|
|
|
+ break
|
|
|
+ finally:
|
|
|
+ task_queue.task_done()
|
|
|
+
|
|
|
+ logger.info(
|
|
|
+ "discover worker exited: worker_id=%s reason=%s elapsed_seconds=%.1f",
|
|
|
+ worker_id,
|
|
|
+ exit_reason,
|
|
|
+ monotonic() - started_at,
|
|
|
+ )
|
|
|
+ return exit_reason
|
|
|
+
|
|
|
+ with ThreadPoolExecutor(
|
|
|
+ max_workers=worker_count,
|
|
|
+ thread_name_prefix="find-agent",
|
|
|
+ ) as executor:
|
|
|
+ futures: set[Any] = set()
|
|
|
+
|
|
|
+ def _spawn_worker() -> None:
|
|
|
+ nonlocal worker_seq
|
|
|
+ worker_seq += 1
|
|
|
+ futures.add(executor.submit(worker_loop, worker_seq))
|
|
|
+
|
|
|
+ for _ in range(worker_count):
|
|
|
+ _spawn_worker()
|
|
|
+
|
|
|
+ while futures:
|
|
|
+ done, futures = wait(
|
|
|
+ futures,
|
|
|
+ timeout=_SUPERVISOR_POLL_SECONDS,
|
|
|
+ return_when=FIRST_COMPLETED,
|
|
|
+ )
|
|
|
+ if not done:
|
|
|
+ alive = len(futures)
|
|
|
+ logger.info(
|
|
|
+ "discover workers heartbeat: biz_dt=%s alive=%d/%d "
|
|
|
+ "processed=%d queue_remaining~=%s passed_videos=%s "
|
|
|
+ "elapsed_seconds=%d",
|
|
|
+ biz_dt,
|
|
|
+ alive,
|
|
|
+ worker_count,
|
|
|
+ result["processed"],
|
|
|
+ task_queue.qsize(),
|
|
|
+ result.get("passed_videos"),
|
|
|
+ int(monotonic() - started_at),
|
|
|
+ )
|
|
|
+ # 超时/异常退出后补齐到目标线程数
|
|
|
+ if (
|
|
|
+ not stop_event.is_set()
|
|
|
+ and not task_queue.empty()
|
|
|
+ and alive < worker_count
|
|
|
+ ):
|
|
|
+ for _ in range(worker_count - alive):
|
|
|
+ if task_queue.empty():
|
|
|
+ break
|
|
|
+ logger.info(
|
|
|
+ "discover worker replenish: biz_dt=%s alive=%d target=%d",
|
|
|
+ biz_dt,
|
|
|
+ alive,
|
|
|
+ worker_count,
|
|
|
+ )
|
|
|
+ _spawn_worker()
|
|
|
+ alive += 1
|
|
|
+ continue
|
|
|
+
|
|
|
+ for future in done:
|
|
|
+ try:
|
|
|
+ exit_reason = future.result()
|
|
|
+ except Exception:
|
|
|
+ logger.exception(
|
|
|
+ "discover worker future crashed: biz_dt=%s",
|
|
|
+ biz_dt,
|
|
|
+ )
|
|
|
+ exit_reason = "crashed"
|
|
|
+
|
|
|
+ if (
|
|
|
+ not stop_event.is_set()
|
|
|
+ and not task_queue.empty()
|
|
|
+ and len(futures) < worker_count
|
|
|
+ ):
|
|
|
+ logger.info(
|
|
|
+ "discover worker replenish after exit: biz_dt=%s "
|
|
|
+ "exit_reason=%s alive=%d target=%d",
|
|
|
+ biz_dt,
|
|
|
+ exit_reason,
|
|
|
+ len(futures),
|
|
|
+ worker_count,
|
|
|
+ )
|
|
|
+ _spawn_worker()
|
|
|
+
|
|
|
+ if stop_event.is_set():
|
|
|
+ discarded = _drain_task_queue(task_queue)
|
|
|
+ if discarded:
|
|
|
+ logger.info(
|
|
|
+ "discover discarded pending tasks after stop: biz_dt=%s discarded=%d",
|
|
|
+ biz_dt,
|
|
|
+ discarded,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
def discover_videos_from_demands(
|
|
|
biz_dt: str | None = None,
|
|
|
*,
|
|
|
@@ -138,16 +478,16 @@ def discover_videos_from_demands(
|
|
|
force: bool = False,
|
|
|
) -> dict[str, Any]:
|
|
|
"""
|
|
|
- 对指定业务日全部 S/A 级需求(有拓展点位)逐条调用 find_agent。
|
|
|
+ 对指定业务日全部 S/A 级需求(有拓展点位)以任务队列方式并发调用 find_agent。
|
|
|
|
|
|
- 每条记录对应一个 demand_grade + 其下全部视频与全部拓展点位。
|
|
|
- 执行前会预写 video_discovery_run,并按 biz_dt + demand_grade_id 跳过已执行记录。
|
|
|
- 默认处理全部 S/A(S 优先于 A、再按 score 排序);仅 CLI --top-limit 可人为截断。
|
|
|
- 当日 primary 去重视频达到上限时提前结束,否则跑完待处理队列。
|
|
|
- 默认单线程串行执行 find_agent(每条 S/A 需求依次处理)。
|
|
|
+ - 先把当天待执行需求全部放入内存队列(此时不写 video_discovery_run)
|
|
|
+ - 固定 worker 线程循环取任务;单任务开始执行时再创建 run 记录
|
|
|
+ - 执行前检查当日 primary 通过数,达标后不再领取新任务(允许略超)
|
|
|
+ - 单任务默认 600s 超时后该 worker 退出,监督方自动补齐线程
|
|
|
"""
|
|
|
started_at = datetime.now()
|
|
|
batch_run_id = uuid.uuid4().hex
|
|
|
+ task_timeout_seconds = find_agent_timeout_seconds()
|
|
|
|
|
|
try:
|
|
|
resolved_biz_dt, contexts = list_find_demand_contexts(
|
|
|
@@ -180,7 +520,9 @@ def discover_videos_from_demands(
|
|
|
passed_videos = _count_passed_videos(resolved_biz_dt)
|
|
|
|
|
|
logger.info(
|
|
|
- "discover_videos_from_demands start: biz_dt=%s batch_run_id=%s workers=%s top_limit=%s offset=%s pending=%s skipped=%s passed_videos=%s passed_video_limit=%s",
|
|
|
+ "discover_videos_from_demands start: biz_dt=%s batch_run_id=%s workers=%s "
|
|
|
+ "top_limit=%s offset=%s pending=%s skipped=%s passed_videos=%s "
|
|
|
+ "passed_video_limit=%s task_timeout_seconds=%.0f",
|
|
|
resolved_biz_dt,
|
|
|
batch_run_id,
|
|
|
workers,
|
|
|
@@ -190,6 +532,7 @@ def discover_videos_from_demands(
|
|
|
preload_stats.get("skipped_already_done", 0),
|
|
|
passed_videos,
|
|
|
_DAILY_PASSED_VIDEO_LIMIT,
|
|
|
+ task_timeout_seconds,
|
|
|
)
|
|
|
|
|
|
result: dict[str, Any] = {
|
|
|
@@ -202,6 +545,7 @@ def discover_videos_from_demands(
|
|
|
**preload_stats,
|
|
|
"total_records": len(contexts),
|
|
|
"workers": 0,
|
|
|
+ "task_timeout_seconds": task_timeout_seconds,
|
|
|
"processed": 0,
|
|
|
"succeeded": 0,
|
|
|
"goal_met": 0,
|
|
|
@@ -227,131 +571,22 @@ def discover_videos_from_demands(
|
|
|
ensure_mysql_pool_capacity(worker_count)
|
|
|
result["workers"] = worker_count
|
|
|
|
|
|
- with ThreadPoolExecutor(max_workers=worker_count) as executor:
|
|
|
- next_context = 0
|
|
|
- while next_context < len(contexts):
|
|
|
- remaining_slots = _DAILY_PASSED_VIDEO_LIMIT - passed_videos
|
|
|
- if remaining_slots <= 0:
|
|
|
- result["stopped_by_passed_video_limit"] = True
|
|
|
- break
|
|
|
-
|
|
|
- batch_size = min(
|
|
|
- worker_count,
|
|
|
- remaining_slots,
|
|
|
- len(contexts) - next_context,
|
|
|
- )
|
|
|
- batch = contexts[next_context : next_context + batch_size]
|
|
|
- next_context += batch_size
|
|
|
- logger.info(
|
|
|
- "discover videos batch start: biz_dt=%s batch_size=%d "
|
|
|
- "completed_batches=%d/%d demands=%s",
|
|
|
- resolved_biz_dt,
|
|
|
- len(batch),
|
|
|
- next_context // batch_size,
|
|
|
- (len(contexts) + batch_size - 1) // batch_size,
|
|
|
- [
|
|
|
- f"{ctx.demand_grade_id}:{ctx.demand_name}"
|
|
|
- for ctx in batch
|
|
|
- ],
|
|
|
- )
|
|
|
- future_to_ctx = {
|
|
|
- executor.submit(process_single_discover, ctx, force=force): ctx
|
|
|
- for ctx in batch
|
|
|
- }
|
|
|
- pending = set(future_to_ctx.keys())
|
|
|
- batch_started = monotonic()
|
|
|
- while pending:
|
|
|
- completed, pending = wait(
|
|
|
- pending,
|
|
|
- timeout=60,
|
|
|
- return_when=FIRST_COMPLETED,
|
|
|
- )
|
|
|
- if not completed:
|
|
|
- pending_demands = [
|
|
|
- f"{future_to_ctx[future].demand_grade_id}:"
|
|
|
- f"{future_to_ctx[future].demand_name}"
|
|
|
- for future in pending
|
|
|
- ]
|
|
|
- logger.info(
|
|
|
- "discover videos still running: biz_dt=%s "
|
|
|
- "batch_pending=%d processed=%d elapsed_seconds=%d "
|
|
|
- "pending_demands=%s",
|
|
|
- resolved_biz_dt,
|
|
|
- len(pending),
|
|
|
- result["processed"],
|
|
|
- int(monotonic() - batch_started),
|
|
|
- pending_demands,
|
|
|
- )
|
|
|
- continue
|
|
|
-
|
|
|
- for future in completed:
|
|
|
- ctx = future_to_ctx[future]
|
|
|
- try:
|
|
|
- item_result = future.result()
|
|
|
- except Exception as exc:
|
|
|
- logger.exception(
|
|
|
- "discover videos worker 出现未捕获错误: biz_dt=%s "
|
|
|
- "grade_id=%s demand=%s",
|
|
|
- resolved_biz_dt,
|
|
|
- ctx.demand_grade_id,
|
|
|
- ctx.demand_name,
|
|
|
- )
|
|
|
- result["failed"] += 1
|
|
|
- result["processed"] += 1
|
|
|
- result["errors"].append(
|
|
|
- {
|
|
|
- "demand_grade_id": ctx.demand_grade_id,
|
|
|
- "demand_name": ctx.demand_name,
|
|
|
- "error": str(exc),
|
|
|
- }
|
|
|
- )
|
|
|
- logger.info(
|
|
|
- "discover videos batch item failed: grade_id=%s "
|
|
|
- "demand=%s batch_remaining=%d total_processed=%d",
|
|
|
- ctx.demand_grade_id,
|
|
|
- ctx.demand_name,
|
|
|
- len(pending),
|
|
|
- result["processed"],
|
|
|
- )
|
|
|
- continue
|
|
|
-
|
|
|
- result["processed"] += 1
|
|
|
- logger.info(
|
|
|
- "discover videos batch item finished: grade_id=%s demand=%s "
|
|
|
- "success=%s skipped=%s batch_remaining=%d total_processed=%d",
|
|
|
- ctx.demand_grade_id,
|
|
|
- ctx.demand_name,
|
|
|
- item_result.get("success"),
|
|
|
- item_result.get("skipped"),
|
|
|
- len(pending),
|
|
|
- result["processed"],
|
|
|
- )
|
|
|
- if item_result.get("skipped"):
|
|
|
- result["skipped"] += 1
|
|
|
- continue
|
|
|
- if item_result.get("success"):
|
|
|
- result["succeeded"] += 1
|
|
|
- business_outcome = item_result.get("business_outcome")
|
|
|
- if business_outcome in {"goal_met", "partial", "no_match"}:
|
|
|
- result[str(business_outcome)] += 1
|
|
|
- continue
|
|
|
-
|
|
|
- result["failed"] += 1
|
|
|
- result["errors"].append(
|
|
|
- {
|
|
|
- "demand_grade_id": item_result.get("demand_grade_id"),
|
|
|
- "demand_name": item_result.get("demand_name"),
|
|
|
- "video_count": item_result.get("video_count"),
|
|
|
- "run_id": item_result.get("run_id"),
|
|
|
- "error": item_result.get("error"),
|
|
|
- }
|
|
|
- )
|
|
|
+ _run_worker_pool(
|
|
|
+ contexts=contexts,
|
|
|
+ biz_dt=resolved_biz_dt,
|
|
|
+ worker_count=worker_count,
|
|
|
+ force=force,
|
|
|
+ result=result,
|
|
|
+ task_timeout_seconds=task_timeout_seconds,
|
|
|
+ )
|
|
|
|
|
|
- passed_videos = _count_passed_videos(resolved_biz_dt)
|
|
|
- result["passed_videos"] = passed_videos
|
|
|
- if passed_videos >= _DAILY_PASSED_VIDEO_LIMIT:
|
|
|
- result["stopped_by_passed_video_limit"] = True
|
|
|
- break
|
|
|
+ # 最终再读一次,避免结果里的 passed_videos 过旧
|
|
|
+ try:
|
|
|
+ result["passed_videos"] = _count_passed_videos(resolved_biz_dt)
|
|
|
+ if result["passed_videos"] >= _DAILY_PASSED_VIDEO_LIMIT:
|
|
|
+ result["stopped_by_passed_video_limit"] = True
|
|
|
+ except Exception:
|
|
|
+ logger.exception("discover_videos_from_demands final count_passed_videos failed")
|
|
|
|
|
|
finished_at = datetime.now()
|
|
|
result["finished_at"] = finished_at.isoformat()
|