run_hive_gap_mysql.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """Run DemandAgent from Hive supply-gap rows and write only MySQL demand_content.
  2. This entrypoint restores the old business starting point (ODPS/Hive supply
  3. gap) without restoring prepare/run_mining or any Hive writes.
  4. """
  5. from __future__ import annotations
  6. import argparse
  7. import asyncio
  8. import json
  9. import os
  10. import sys
  11. from datetime import datetime
  12. from pathlib import Path
  13. from typing import Any
  14. sys.path.insert(0, str(Path(__file__).parent.parent.parent))
  15. from examples.demand.pg_pattern_repository import normalize_platform
  16. def _load_project_env() -> None:
  17. try:
  18. from dotenv import load_dotenv
  19. except ImportError:
  20. return
  21. load_dotenv(Path(__file__).parent.parent.parent / ".env")
  22. def _safe_path_segment(value: str) -> str:
  23. return "".join(ch if ch.isalnum() or ch in {"-", "_"} else "_" for ch in value).strip("_") or "run"
  24. def _prepare_run_output_paths(execution_id: int, run_label: str) -> None:
  25. run_root = Path(__file__).parent / "test_output_data" / "mysql_runs" / _safe_path_segment(run_label)
  26. result_base_dir = run_root / "intermediate" / "result"
  27. os.environ["DEMAND_RESULT_BASE_DIR"] = str(result_base_dir)
  28. os.environ["DEMAND_OUTPUT_BASE_DIR"] = str(run_root / "output")
  29. os.environ["DEMAND_TRACE_STORE_PATH"] = str(run_root / ".trace")
  30. os.environ["DEMAND_WEIGHT_DATA_DIR"] = str(run_root / "intermediate" / "data")
  31. demand_items_path = result_base_dir / str(execution_id) / f"execution_id_{execution_id}_demand_items.json"
  32. if demand_items_path.exists():
  33. demand_items_path.unlink()
  34. def _configure_mysql_env(run_label: str) -> None:
  35. os.environ["DEMAND_OUTPUT_MODE"] = "mysql_demand_content"
  36. os.environ["DEMAND_MYSQL_ENTRYPOINT"] = "run_hive_gap_mysql"
  37. os.environ["DEMAND_RUN_LABEL"] = run_label
  38. def _validate_execution_success(execution_id: int) -> None:
  39. from examples.demand.db_manager import query_execution_for_evidence
  40. execution = query_execution_for_evidence(execution_id)
  41. if not execution:
  42. raise ValueError(f"execution_id={execution_id} 不存在,Hive gap 入口只允许跑已有 PG execution")
  43. if str(execution.get("status") or "").strip().lower() != "success":
  44. raise ValueError(
  45. f"execution_id={execution_id} status={execution.get('status')},不是 success,拒绝生成需求"
  46. )
  47. def _build_scope(row: dict[str, Any], execution_id: int, count: int) -> dict[str, Any]:
  48. return {
  49. "scope_source": "odps_gap",
  50. "merge_leve2": row["cluster_name"],
  51. "platform": normalize_platform(row.get("platform_type") or "piaoquan"),
  52. "gap_dt": row.get("gap_dt"),
  53. "requested_count": int(count),
  54. "lack_count": row.get("lack_count"),
  55. "pattern_execution_id": int(execution_id),
  56. }
  57. def parse_args() -> argparse.Namespace:
  58. parser = argparse.ArgumentParser(
  59. description="Run DemandAgent from Hive gap rows and write only MySQL demand_content."
  60. )
  61. parser.add_argument("--execution-id", type=int, default=None, help="Existing PG pattern_mining_execution.id")
  62. parser.add_argument("--skip-categories", type=int, default=0, help="Skip the first N Hive gap categories")
  63. parser.add_argument("--max-categories", type=int, default=None, help="Only run the first N gap categories")
  64. parser.add_argument("--max-total-count", type=int, default=None, help="Cap total requested demand count")
  65. parser.add_argument("--per-category-count", type=int, default=None, help="Override each selected gap row count")
  66. parser.add_argument("--run-label-prefix", default=None, help="Prefix stored in ext_data.run_label")
  67. parser.add_argument("--dry-run", action="store_true", help="Print gap plan only; do not call the agent")
  68. parser.add_argument(
  69. "--fail-on-partition-not-ready",
  70. action="store_true",
  71. help="Exit non-zero when yesterday's Hive source partition is not ready; useful for systemd retry.",
  72. )
  73. return parser.parse_args()
  74. async def async_main() -> dict[str, Any]:
  75. _load_project_env()
  76. args = parse_args()
  77. from examples.demand.data_query_tools import (
  78. get_demand_gap_source_partition_status,
  79. get_demand_merge_level2_names,
  80. )
  81. from examples.demand.db_manager import query_latest_success_execution_id
  82. execution_id = args.execution_id or query_latest_success_execution_id()
  83. if not execution_id:
  84. raise ValueError("未找到 PG Pattern V2 success execution")
  85. _validate_execution_success(int(execution_id))
  86. partition_status = get_demand_gap_source_partition_status()
  87. if not partition_status.get("ready"):
  88. return {
  89. "execution_id": int(execution_id),
  90. "dry_run": bool(args.dry_run),
  91. "skipped_reason": "hive_gap_source_partition_not_ready",
  92. "partition_status": partition_status,
  93. "planned": [],
  94. "results": [],
  95. "_exit_code": 75 if args.fail_on_partition_not_ready else 0,
  96. }
  97. gap_rows = get_demand_merge_level2_names(day=str(partition_status["dt"]))
  98. if not gap_rows:
  99. return {
  100. "execution_id": int(execution_id),
  101. "dry_run": bool(args.dry_run),
  102. "skipped_reason": "hive_gap_rows_empty",
  103. "partition_status": partition_status,
  104. "planned": [],
  105. "results": [],
  106. }
  107. if args.skip_categories:
  108. gap_rows = gap_rows[max(int(args.skip_categories), 0):]
  109. if args.max_categories is not None:
  110. gap_rows = gap_rows[: max(int(args.max_categories), 0)]
  111. remaining = int(args.max_total_count) if args.max_total_count is not None else None
  112. prefix = args.run_label_prefix or f"hive_gap_mysql_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
  113. results: list[dict[str, Any]] = []
  114. planned: list[dict[str, Any]] = []
  115. for index, row in enumerate(gap_rows, start=1):
  116. requested = int(row.get("count") or 0)
  117. if args.per_category_count is not None:
  118. requested = max(int(args.per_category_count), 0)
  119. if remaining is not None:
  120. if remaining <= 0:
  121. break
  122. requested = min(requested, remaining)
  123. if requested <= 0:
  124. continue
  125. cluster_name = str(row["cluster_name"])
  126. platform_type = str(row.get("platform_type") or "piaoquan")
  127. run_label = f"{prefix}_{index:02d}_{_safe_path_segment(cluster_name)}"
  128. scope = _build_scope(row, int(execution_id), requested)
  129. planned.append({"run_label": run_label, "count": requested, "demand_scope": scope})
  130. if args.dry_run:
  131. if remaining is not None:
  132. remaining -= requested
  133. continue
  134. from examples.demand.run import main as run_main
  135. _configure_mysql_env(run_label)
  136. _prepare_run_output_paths(int(execution_id), run_label)
  137. result = await run_main(
  138. cluster_name=cluster_name,
  139. platform_type=platform_type,
  140. count=requested,
  141. execution_id=int(execution_id),
  142. task_id=None,
  143. demand_scope=scope,
  144. )
  145. result["run_label"] = run_label
  146. result["demand_scope"] = scope
  147. results.append(result)
  148. if remaining is not None:
  149. remaining -= requested
  150. return {
  151. "execution_id": int(execution_id),
  152. "dry_run": bool(args.dry_run),
  153. "partition_status": partition_status,
  154. "planned": planned,
  155. "results": results,
  156. }
  157. def main() -> None:
  158. result = asyncio.run(async_main())
  159. exit_code = int(result.pop("_exit_code", 0) or 0)
  160. print(json.dumps(result, ensure_ascii=False, indent=2))
  161. if exit_code:
  162. raise SystemExit(exit_code)
  163. if __name__ == "__main__":
  164. main()