config.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. from __future__ import annotations
  2. from functools import lru_cache
  3. from pathlib import Path
  4. from urllib.parse import quote_plus
  5. from pydantic import Field, model_validator
  6. from pydantic_settings import BaseSettings, SettingsConfigDict
  7. # supply_infra/config.py -> project root; avoid depending on process cwd
  8. _PROJECT_ROOT = Path(__file__).resolve().parent.parent
  9. _ENV_FILE = _PROJECT_ROOT / ".env"
  10. class InfraSettings(BaseSettings):
  11. """Shared infrastructure settings."""
  12. model_config = SettingsConfigDict(
  13. env_file=str(_ENV_FILE),
  14. env_file_encoding="utf-8",
  15. extra="ignore",
  16. )
  17. # MySQL
  18. mysql_host: str = Field(default="127.0.0.1", alias="MYSQL_HOST")
  19. mysql_port: int = Field(default=3306, alias="MYSQL_PORT")
  20. mysql_user: str = Field(default="root", alias="MYSQL_USER")
  21. mysql_password: str = Field(default="", alias="MYSQL_PASSWORD")
  22. mysql_database: str = Field(default="supply_agent", alias="MYSQL_DATABASE")
  23. mysql_pool_size: int = Field(default=1, ge=1, alias="MYSQL_POOL_SIZE")
  24. mysql_connection_budget: int = Field(
  25. default=40,
  26. ge=30,
  27. le=50,
  28. alias="MYSQL_CONNECTION_BUDGET",
  29. )
  30. mysql_pool_size_api: int = Field(default=6, ge=1, alias="MYSQL_POOL_SIZE_API")
  31. mysql_pool_size_control: int = Field(
  32. default=1,
  33. ge=1,
  34. alias="MYSQL_POOL_SIZE_CONTROL",
  35. )
  36. mysql_max_overflow: int = Field(default=0, ge=0, alias="MYSQL_MAX_OVERFLOW")
  37. mysql_pool_timeout_seconds: int = Field(
  38. default=10,
  39. ge=1,
  40. alias="MYSQL_POOL_TIMEOUT_SECONDS",
  41. )
  42. mysql_pool_recycle_seconds: int = Field(
  43. default=1800,
  44. ge=60,
  45. alias="MYSQL_POOL_RECYCLE_SECONDS",
  46. )
  47. mysql_operational_reserve: int = Field(
  48. default=4,
  49. ge=2,
  50. alias="MYSQL_OPERATIONAL_RESERVE",
  51. )
  52. mysql_echo: bool = Field(default=False, alias="MYSQL_ECHO")
  53. # ODPS (MaxCompute)
  54. odps_access_id: str = Field(default="", alias="ODPS_ACCESS_ID")
  55. odps_access_key: str = Field(default="", alias="ODPS_ACCESS_KEY")
  56. odps_project: str = Field(default="", alias="ODPS_PROJECT")
  57. odps_endpoint: str = Field(
  58. default="https://service.cn.maxcompute.aliyun.com/api",
  59. alias="ODPS_ENDPOINT",
  60. )
  61. # Scheduler
  62. scheduler_timezone: str = Field(default="Asia/Shanghai", alias="SCHEDULER_TIMEZONE")
  63. scheduler_enabled: bool = Field(default=False, alias="SCHEDULER_ENABLED")
  64. scheduler_cron_hour: int = Field(default=15, ge=0, le=23, alias="SCHEDULER_CRON_HOUR")
  65. scheduler_cron_minute: int = Field(
  66. default=0,
  67. ge=0,
  68. le=59,
  69. alias="SCHEDULER_CRON_MINUTE",
  70. )
  71. # Pipeline process/control plane
  72. process_role: str = Field(default="api", alias="PROCESS_ROLE")
  73. database_auto_create: bool = Field(default=False, alias="DATABASE_AUTO_CREATE")
  74. pipeline_worker_processes: int = Field(
  75. default=4,
  76. ge=1,
  77. le=20,
  78. alias="PIPELINE_WORKER_PROCESSES",
  79. )
  80. pipeline_max_active_steps: int = Field(
  81. default=4,
  82. ge=1,
  83. le=20,
  84. alias="PIPELINE_MAX_ACTIVE_STEPS",
  85. )
  86. pipeline_worker_poll_seconds: float = Field(
  87. default=2.0,
  88. gt=0,
  89. alias="PIPELINE_WORKER_POLL_SECONDS",
  90. )
  91. pipeline_lease_seconds: int = Field(
  92. default=120,
  93. ge=30,
  94. alias="PIPELINE_LEASE_SECONDS",
  95. )
  96. pipeline_heartbeat_seconds: int = Field(
  97. default=30,
  98. ge=5,
  99. alias="PIPELINE_HEARTBEAT_SECONDS",
  100. )
  101. pipeline_reconcile_seconds: int = Field(
  102. default=60,
  103. ge=10,
  104. alias="PIPELINE_RECONCILE_SECONDS",
  105. )
  106. pipeline_missed_run_grace_seconds: int = Field(
  107. default=600,
  108. ge=60,
  109. alias="PIPELINE_MISSED_RUN_GRACE_SECONDS",
  110. )
  111. pipeline_shutdown_grace_seconds: int = Field(
  112. default=300,
  113. ge=10,
  114. alias="PIPELINE_SHUTDOWN_GRACE_SECONDS",
  115. )
  116. pipeline_warn_after_hours: int = Field(
  117. default=12,
  118. ge=1,
  119. alias="PIPELINE_WARN_AFTER_HOURS",
  120. )
  121. pipeline_critical_before_next_minutes: int = Field(
  122. default=120,
  123. ge=1,
  124. alias="PIPELINE_CRITICAL_BEFORE_NEXT_MINUTES",
  125. )
  126. pipeline_final_warn_before_next_minutes: int = Field(
  127. default=30,
  128. ge=1,
  129. alias="PIPELINE_FINAL_WARN_BEFORE_NEXT_MINUTES",
  130. )
  131. pipeline_log_dir: str = Field(default="logs/pipeline", alias="PIPELINE_LOG_DIR")
  132. pipeline_external_effects_enabled: bool = Field(
  133. default=False,
  134. alias="PIPELINE_EXTERNAL_EFFECTS_ENABLED",
  135. )
  136. # Aliyun OSS (agent run log publishing)
  137. aliyun_oss_access_key_id: str = Field(default="", alias="ALIYUN_OSS_ACCESS_KEY_ID")
  138. aliyun_oss_access_key_secret: str = Field(default="", alias="ALIYUN_OSS_ACCESS_KEY_SECRET")
  139. aliyun_oss_region: str = Field(default="cn-hangzhou", alias="ALIYUN_OSS_REGION")
  140. aliyun_oss_bucket: str = Field(default="art-pubbucket", alias="ALIYUN_OSS_BUCKET")
  141. aliyun_oss_root_prefix: str = Field(default="supply_agent", alias="ALIYUN_OSS_ROOT_PREFIX")
  142. aliyun_oss_manual_log_prefix: str = Field(
  143. default="supply_agent/manual_logs",
  144. alias="ALIYUN_OSS_MANUAL_LOG_PREFIX",
  145. )
  146. aliyun_oss_public_base_url: str = Field(
  147. default="http://rescdn.yishihui.com",
  148. alias="ALIYUN_OSS_PUBLIC_BASE_URL",
  149. )
  150. log_oss_upload_enabled: bool = Field(default=True, alias="LOG_OSS_UPLOAD_ENABLED")
  151. # AIGC platform
  152. aigc_api_token: str = Field(default="", alias="AIGC_API_TOKEN")
  153. aigc_dry_run: bool = Field(default=True, alias="AIGC_DRY_RUN")
  154. @model_validator(mode="after")
  155. def validate_pipeline_runtime(self) -> "InfraSettings":
  156. roles = {"api", "scheduler", "worker", "reconciler", "step", "cli"}
  157. if self.process_role not in roles:
  158. raise ValueError(
  159. f"PROCESS_ROLE must be one of {sorted(roles)}, got {self.process_role!r}"
  160. )
  161. if self.pipeline_heartbeat_seconds >= self.pipeline_lease_seconds:
  162. raise ValueError(
  163. "PIPELINE_HEARTBEAT_SECONDS must be smaller than PIPELINE_LEASE_SECONDS"
  164. )
  165. if self.pipeline_max_active_steps > self.pipeline_worker_processes:
  166. raise ValueError(
  167. "PIPELINE_MAX_ACTIVE_STEPS cannot exceed PIPELINE_WORKER_PROCESSES"
  168. )
  169. if self.mysql_max_overflow != 0:
  170. raise ValueError("MYSQL_MAX_OVERFLOW must remain 0 for connection budgeting")
  171. required = self.pipeline_required_connections
  172. if required > self.mysql_connection_budget:
  173. raise ValueError(
  174. "Pipeline connection budget exceeded: "
  175. f"required={required} budget={self.mysql_connection_budget}"
  176. )
  177. if (
  178. self.pipeline_external_effects_enabled
  179. and not self.aigc_dry_run
  180. ):
  181. raise ValueError(
  182. "Real AIGC effects are disabled in the current migration phase; "
  183. "set AIGC_DRY_RUN=true"
  184. )
  185. return self
  186. @property
  187. def mysql_url(self) -> str:
  188. user = quote_plus(self.mysql_user)
  189. password = quote_plus(self.mysql_password)
  190. return (
  191. f"mysql+pymysql://{user}:{password}"
  192. f"@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
  193. f"?charset=utf8mb4"
  194. )
  195. @property
  196. def selected_mysql_pool_size(self) -> int:
  197. if self.process_role == "api":
  198. return self.mysql_pool_size_api
  199. if self.process_role in {"scheduler", "worker", "reconciler", "step"}:
  200. return self.mysql_pool_size_control
  201. return self.mysql_pool_size
  202. @property
  203. def pipeline_required_connections(self) -> int:
  204. control_processes = 2 # scheduler + reconciler
  205. return (
  206. self.mysql_pool_size_api
  207. + control_processes * self.mysql_pool_size_control
  208. + self.pipeline_worker_processes * self.mysql_pool_size_control
  209. + self.pipeline_max_active_steps * self.mysql_pool_size_control
  210. + self.mysql_operational_reserve
  211. )
  212. @lru_cache
  213. def get_infra_settings() -> InfraSettings:
  214. return InfraSettings()