pattern_pg.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """M9C:只读直连 pattern PG(open_aigc),判需求是否含分类树终端元素(叶子)。
  2. 叶子 = pattern_mining_category 中某 id 没有任何行 parent_id=该 id(同 execution)。
  3. Gate 1:需求 itemset_items 的 category_id 里至少有一个叶子 → 放行;否则不做。
  4. """
  5. from __future__ import annotations
  6. from typing import Any
  7. from content_agent.errors import ContentAgentError, ErrorCode
  8. from content_agent.integrations import timeout_config
  9. from content_agent.integrations.crawapi_http import _env, _load_env_file
  10. _LEAF_SQL = (
  11. "SELECT 1 FROM pattern_mining_category c "
  12. "WHERE c.execution_id = %s AND c.id = ANY(%s) "
  13. "AND NOT EXISTS (SELECT 1 FROM pattern_mining_category ch "
  14. "WHERE ch.execution_id = c.execution_id AND ch.parent_id = c.id) LIMIT 1"
  15. )
  16. class PatternPgClient:
  17. def __init__(
  18. self,
  19. *,
  20. host: str,
  21. port: int,
  22. user: str,
  23. password: str,
  24. database: str,
  25. timeout_seconds: float = 30.0,
  26. ) -> None:
  27. self.host = host
  28. self.port = port
  29. self.user = user
  30. self.password = password
  31. self.database = database
  32. self.timeout_seconds = timeout_seconds
  33. @classmethod
  34. def from_env(cls, env_path: str = ".env") -> "PatternPgClient":
  35. env = _load_env_file(env_path)
  36. return cls(
  37. host=_env("OPEN_AIGC_PG_HOST", env, required=True),
  38. port=int(_env("OPEN_AIGC_PG_PORT", env, default="5432")),
  39. user=_env("OPEN_AIGC_PG_USER", env, required=True),
  40. password=_env("OPEN_AIGC_PG_PASSWORD", env, required=True),
  41. database=_env("OPEN_AIGC_PG_DB_NAME", env, default="open_aigc"),
  42. timeout_seconds=timeout_config.total_timeout("pg", env=env),
  43. )
  44. def has_terminal_element(self, execution_id: int, category_ids: list[int]) -> bool:
  45. ids = [int(c) for c in category_ids if c is not None]
  46. if not ids:
  47. return True # 无可判定 category → 不阻断(交给后续判定)
  48. import pg8000.dbapi # 惰性导入:mock 测试用 fake client,不触发依赖
  49. try:
  50. conn = pg8000.dbapi.connect(
  51. host=self.host,
  52. port=self.port,
  53. user=self.user,
  54. password=self.password,
  55. database=self.database,
  56. ssl_context=None, # 服务器拒绝 SSL,必须明确关闭
  57. timeout=self.timeout_seconds,
  58. )
  59. except Exception as exc: # PG 不可达:明确报错,不静默放过
  60. raise ContentAgentError(
  61. ErrorCode.DB_CONNECTION_FAILED,
  62. "pattern pg unreachable for gate1 terminal-element check",
  63. {"exception_type": type(exc).__name__},
  64. ) from exc
  65. try:
  66. cur = conn.cursor()
  67. # connect timeout 只管握手;execute/fetch 用服务端 statement_timeout 兜住,防查询永久阻塞。
  68. # pg8000/PG 不支持在 SET statement_timeout 里用 $1 占位符;这里先转 int 再内联。
  69. timeout_ms = max(1, int(self.timeout_seconds * 1000))
  70. cur.execute(f"SET statement_timeout = {timeout_ms}")
  71. cur.execute(_LEAF_SQL, (int(execution_id), ids))
  72. return cur.fetchone() is not None
  73. except Exception as exc:
  74. raise ContentAgentError(
  75. ErrorCode.DB_SCHEMA_NOT_READY,
  76. "pattern pg gate1 terminal-element query failed",
  77. {
  78. "exception_type": type(exc).__name__,
  79. "error_message": str(exc)[:500],
  80. "execution_id": int(execution_id),
  81. "category_id_count": len(ids),
  82. },
  83. ) from exc
  84. finally:
  85. conn.close()