test_data_factory_postgres_lifecycle.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. from __future__ import annotations
  2. import json
  3. import pytest
  4. from sqlalchemy import create_engine, text
  5. from sqlalchemy.orm import Session
  6. from app.core.common.identifiers import new_governance_uid
  7. from app.core.data_rules.repository import DataRuleRepository
  8. pytestmark = pytest.mark.integration
  9. DATABASE_URL = (
  10. "postgresql://dataops:dataops-test-password@127.0.0.1:15432/dataops"
  11. )
  12. def _insert_deployment(
  13. connection,
  14. *,
  15. deployment_id: str,
  16. version_id: str,
  17. dataflow_uid: str,
  18. actor_uid: str,
  19. status: str,
  20. idempotency_key: str,
  21. ) -> None:
  22. hashes = {
  23. "package_hash": "a" * 64,
  24. "binding_hash": "b" * 64,
  25. "schema_snapshot_hash": "c" * 64,
  26. "workflow_spec_hash": "d" * 64,
  27. "schedule_hash": "e" * 64,
  28. "engine_definition_hash": "f" * 64,
  29. }
  30. connection.execute(
  31. text(
  32. """
  33. INSERT INTO public.dataflow_deployments
  34. (id, dataflow_version_id, dataflow_uid, environment, status,
  35. package, package_hash, binding_snapshot, binding_hash,
  36. schema_snapshots, schema_snapshot_hash, physical_plan_hashes,
  37. workflow_spec, workflow_spec_hash, schedule_snapshot,
  38. schedule_hash, engine_namespace, engine_definition_id,
  39. engine_definition_hash, created_by, create_reason,
  40. create_idempotency_key, correlation_id)
  41. VALUES
  42. (CAST(:id AS uuid), CAST(:version_id AS uuid),
  43. CAST(:dataflow_uid AS uuid), 'production', :status,
  44. CAST(:package AS jsonb), :package_hash,
  45. CAST(:binding AS jsonb), :binding_hash,
  46. CAST(:schemas AS jsonb), :schema_snapshot_hash,
  47. CAST(:plans AS jsonb), CAST(:workflow AS jsonb),
  48. :workflow_spec_hash, CAST(:schedule AS jsonb), :schedule_hash,
  49. 'dataops.factory', :definition_id, :engine_definition_hash,
  50. CAST(:actor_uid AS uuid), 'integration acceptance', :key,
  51. CAST(:correlation_id AS uuid))
  52. """
  53. ),
  54. {
  55. "id": deployment_id,
  56. "version_id": version_id,
  57. "dataflow_uid": dataflow_uid,
  58. "status": status,
  59. "package": json.dumps({"schema_version": "1.0"}),
  60. "binding": json.dumps(
  61. {"input": {"binding_id": "in"}, "output": {"binding_id": "out"}}
  62. ),
  63. "schemas": json.dumps({"input": {}, "output": {}}),
  64. "plans": json.dumps(["0" * 64]),
  65. "workflow": json.dumps(
  66. {
  67. "schema_version": "1.0",
  68. "dataflow_uid": dataflow_uid,
  69. "name": "factory postgres acceptance",
  70. "nodes": [],
  71. "edges": [],
  72. "parameters": {},
  73. }
  74. ),
  75. "schedule": json.dumps(
  76. {
  77. "schema_version": "1.0",
  78. "timezone": "Asia/Shanghai",
  79. "triggers": [{"type": "manual"}],
  80. "max_concurrency": 1,
  81. "timeout_seconds": 60,
  82. "retry": {"max_attempts": 1, "delay_seconds": 0},
  83. "conflict_policy": "queue",
  84. "backfill": {"max_days": 1, "max_runs": 1},
  85. }
  86. ),
  87. "definition_id": f"flow-{version_id}",
  88. "actor_uid": actor_uid,
  89. "key": idempotency_key,
  90. "correlation_id": new_governance_uid(),
  91. **hashes,
  92. },
  93. )
  94. def test_postgres_activation_and_rollback_are_atomic_and_idempotent():
  95. engine = create_engine(DATABASE_URL, pool_pre_ping=True)
  96. actor_uid = new_governance_uid()
  97. dataflow_uid = new_governance_uid()
  98. version_ids = [new_governance_uid(), new_governance_uid()]
  99. deployment_ids = [new_governance_uid(), new_governance_uid()]
  100. evidence_id = new_governance_uid()
  101. operation_ids: list[str] = []
  102. try:
  103. with engine.begin() as connection:
  104. connection.execute(
  105. text(
  106. "INSERT INTO public.users "
  107. "(id, username, password_hash) "
  108. "VALUES (CAST(:id AS uuid), :username, 'not-used')"
  109. ),
  110. {"id": actor_uid, "username": f"factory-{actor_uid}"},
  111. )
  112. for index, version_id in enumerate(version_ids, start=1):
  113. connection.execute(
  114. text(
  115. """
  116. INSERT INTO public.dataflow_versions
  117. (id, dataflow_uid, version_no, name, dataflow_spec,
  118. input_schema_hashes, output_schema_hash, status,
  119. created_by)
  120. VALUES
  121. (CAST(:id AS uuid), CAST(:uid AS uuid), :version,
  122. 'factory postgres acceptance', '{}'::jsonb,
  123. '{}'::jsonb, :hash, 'released',
  124. CAST(:actor AS uuid))
  125. """
  126. ),
  127. {
  128. "id": version_id,
  129. "uid": dataflow_uid,
  130. "version": index,
  131. "hash": "9" * 64,
  132. "actor": actor_uid,
  133. },
  134. )
  135. _insert_deployment(
  136. connection,
  137. deployment_id=deployment_ids[0],
  138. version_id=version_ids[0],
  139. dataflow_uid=dataflow_uid,
  140. actor_uid=actor_uid,
  141. status="active",
  142. idempotency_key="factory-pg-create-v1",
  143. )
  144. _insert_deployment(
  145. connection,
  146. deployment_id=deployment_ids[1],
  147. version_id=version_ids[1],
  148. dataflow_uid=dataflow_uid,
  149. actor_uid=actor_uid,
  150. status="canary",
  151. idempotency_key="factory-pg-create-v2",
  152. )
  153. connection.execute(
  154. text(
  155. """
  156. INSERT INTO public.dataflow_canary_evidence
  157. (id, deployment_id, execution_id, status, package_hash,
  158. binding_hash, schema_snapshot_hash, physical_plan_hashes,
  159. workflow_spec_hash, schedule_hash, engine_definition_hash,
  160. verified_by, expires_at)
  161. VALUES
  162. (CAST(:id AS uuid), CAST(:deployment AS uuid), 'exec-pg',
  163. 'passed', :package_hash, :binding_hash, :schema_hash,
  164. CAST(:plans AS jsonb), :workflow_hash, :schedule_hash,
  165. :engine_hash, CAST(:actor AS uuid),
  166. CURRENT_TIMESTAMP + INTERVAL '15 minutes')
  167. """
  168. ),
  169. {
  170. "id": evidence_id,
  171. "deployment": deployment_ids[1],
  172. "actor": actor_uid,
  173. "package_hash": "a" * 64,
  174. "binding_hash": "b" * 64,
  175. "schema_hash": "c" * 64,
  176. "plans": json.dumps(["0" * 64]),
  177. "workflow_hash": "d" * 64,
  178. "schedule_hash": "e" * 64,
  179. "engine_hash": "f" * 64,
  180. },
  181. )
  182. connection.execute(
  183. text(
  184. "UPDATE public.dataflow_deployments "
  185. "SET canary_evidence_id = CAST(:evidence AS uuid) "
  186. "WHERE id = CAST(:id AS uuid)"
  187. ),
  188. {"evidence": evidence_id, "id": deployment_ids[1]},
  189. )
  190. with Session(engine) as session:
  191. repository = DataRuleRepository(session)
  192. claimed, activation = repository.claim_operation(
  193. deployment_ids[1],
  194. "activate",
  195. "factory-pg-activate",
  196. actor_uid,
  197. new_governance_uid(),
  198. "activate acceptance candidate",
  199. {"evidence_id": evidence_id},
  200. )
  201. assert claimed is True
  202. operation_ids.append(activation["id"])
  203. active = repository.activate_atomic(
  204. deployment_ids[1],
  205. 0,
  206. evidence_id,
  207. actor_uid,
  208. activation["id"],
  209. )
  210. session.commit()
  211. assert active["status"] == "active"
  212. assert (
  213. active["previous_active_deployment_id"]
  214. == deployment_ids[0]
  215. )
  216. assert repository.get_deployment(deployment_ids[0])[
  217. "status"
  218. ] == "superseded"
  219. claimed, rollback = repository.claim_operation(
  220. deployment_ids[1],
  221. "rollback",
  222. "factory-pg-rollback",
  223. actor_uid,
  224. new_governance_uid(),
  225. "rollback acceptance candidate",
  226. {"candidate_deployment_id": deployment_ids[1]},
  227. )
  228. assert claimed is True
  229. operation_ids.append(rollback["id"])
  230. restored = repository.rollback_atomic(
  231. deployment_ids[1],
  232. active["lock_version"],
  233. actor_uid,
  234. rollback["id"],
  235. )
  236. session.commit()
  237. assert restored["rolled_back"]["status"] == "rolled_back"
  238. assert restored["active"]["id"] == deployment_ids[0]
  239. assert restored["active"]["status"] == "active"
  240. replayed, prior = repository.claim_operation(
  241. deployment_ids[1],
  242. "rollback",
  243. "factory-pg-rollback",
  244. actor_uid,
  245. rollback["correlation_id"],
  246. "rollback acceptance candidate",
  247. {"candidate_deployment_id": deployment_ids[1]},
  248. )
  249. assert replayed is False
  250. assert prior["status"] == "completed"
  251. assert prior["result"]["active"]["id"] == deployment_ids[0]
  252. session.rollback()
  253. finally:
  254. with engine.begin() as connection:
  255. connection.execute(
  256. text(
  257. "UPDATE public.dataflow_deployments "
  258. "SET canary_evidence_id = NULL, "
  259. "previous_active_deployment_id = NULL "
  260. "WHERE id = ANY(CAST(:ids AS uuid[]))"
  261. ),
  262. {"ids": deployment_ids},
  263. )
  264. connection.execute(
  265. text(
  266. "DELETE FROM public.dataflow_deployment_transitions "
  267. "WHERE deployment_id = ANY(CAST(:ids AS uuid[]))"
  268. ),
  269. {"ids": deployment_ids},
  270. )
  271. connection.execute(
  272. text(
  273. "DELETE FROM public.dataflow_deployment_operations "
  274. "WHERE deployment_id = ANY(CAST(:ids AS uuid[]))"
  275. ),
  276. {"ids": deployment_ids},
  277. )
  278. connection.execute(
  279. text(
  280. "DELETE FROM public.dataflow_canary_evidence "
  281. "WHERE deployment_id = ANY(CAST(:ids AS uuid[]))"
  282. ),
  283. {"ids": deployment_ids},
  284. )
  285. connection.execute(
  286. text(
  287. "DELETE FROM public.dataflow_deployments "
  288. "WHERE id = ANY(CAST(:ids AS uuid[]))"
  289. ),
  290. {"ids": deployment_ids},
  291. )
  292. connection.execute(
  293. text(
  294. "DELETE FROM public.dataflow_versions "
  295. "WHERE id = ANY(CAST(:ids AS uuid[]))"
  296. ),
  297. {"ids": version_ids},
  298. )
  299. connection.execute(
  300. text("DELETE FROM public.users WHERE id = CAST(:id AS uuid)"),
  301. {"id": actor_uid},
  302. )
  303. engine.dispose()