test_dataflow_draft_reservation_postgres.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. from __future__ import annotations
  2. import os
  3. from concurrent.futures import ThreadPoolExecutor
  4. import pytest
  5. from sqlalchemy import create_engine, text
  6. from sqlalchemy.exc import IntegrityError
  7. from sqlalchemy.orm import Session
  8. from app.core.common.identifiers import new_governance_uid
  9. from app.core.data_rules.repository import DataRuleRepository
  10. def _claim(url, receipt, actor):
  11. engine = create_engine(url)
  12. try:
  13. with Session(engine) as session:
  14. try:
  15. value = DataRuleRepository(session).begin_dataflow_create(
  16. receipt, actor_uid=actor
  17. )
  18. session.commit()
  19. return value
  20. except ValueError as exc:
  21. session.rollback()
  22. return {"status": str(exc)}
  23. finally:
  24. engine.dispose()
  25. def test_real_postgres_saga_fk_expiry_concurrent_claim_replay_and_lease_recovery():
  26. url = os.environ.get("DATA_RULE_POSTGRES_ACCEPTANCE_URL")
  27. if not url:
  28. pytest.skip("real PostgreSQL acceptance URL is not configured")
  29. engine = create_engine(url)
  30. created_ids = []
  31. actor = new_governance_uid()
  32. try:
  33. with Session(engine) as session:
  34. session.execute(
  35. text(
  36. "INSERT INTO public.users "
  37. "(id, username, display_name, password_hash, status) "
  38. "VALUES (CAST(:id AS uuid), :username, "
  39. "'Saga Acceptance', 'not-a-login-hash', 'active')"
  40. ),
  41. {"id": actor, "username": f"saga-{actor}"},
  42. )
  43. session.commit()
  44. receipt = DataRuleRepository(session).reserve_dataflow_draft(
  45. actor_uid=actor
  46. )
  47. created_ids.append(receipt["reservation_id"])
  48. session.commit()
  49. closed = {
  50. key: receipt[key]
  51. for key in ("reservation_id", "dataflow_uid", "nonce")
  52. }
  53. with ThreadPoolExecutor(max_workers=2) as pool:
  54. outcomes = list(
  55. pool.map(lambda _index: _claim(url, closed, actor), range(2))
  56. )
  57. claimed = [value for value in outcomes if value["status"] == "claimed"]
  58. assert len(claimed) == 1
  59. assert sorted(value["status"] for value in outcomes) == [
  60. "claimed",
  61. "dataflow_create_in_progress",
  62. ]
  63. result = {"id": 73, "uid": receipt["dataflow_uid"]}
  64. with Session(engine) as session:
  65. repository = DataRuleRepository(session)
  66. assert repository.complete_dataflow_create(
  67. reservation_id=receipt["reservation_id"],
  68. dataflow_uid=receipt["dataflow_uid"],
  69. lease_token=claimed[0]["lease_token"],
  70. dataflow_node_id=73,
  71. result=result,
  72. ) == result
  73. session.commit()
  74. replay = _claim(url, closed, actor)
  75. assert replay["status"] == "completed"
  76. assert replay["result"] == result
  77. with Session(engine) as session:
  78. expired = DataRuleRepository(session).reserve_dataflow_draft(
  79. actor_uid=actor
  80. )
  81. created_ids.append(expired["reservation_id"])
  82. session.execute(
  83. text(
  84. "UPDATE public.dataflow_draft_reservations "
  85. "SET created_at = CURRENT_TIMESTAMP - INTERVAL '2 seconds', "
  86. "expires_at = CURRENT_TIMESTAMP - INTERVAL '1 second' "
  87. "WHERE id = CAST(:id AS uuid)"
  88. ),
  89. {"id": expired["reservation_id"]},
  90. )
  91. session.commit()
  92. expired_closed = {
  93. key: expired[key]
  94. for key in ("reservation_id", "dataflow_uid", "nonce")
  95. }
  96. assert _claim(url, expired_closed, actor)["status"] == (
  97. "draft_reservation_refresh_required"
  98. )
  99. with Session(engine) as session:
  100. recoverable = DataRuleRepository(session).reserve_dataflow_draft(
  101. actor_uid=actor
  102. )
  103. created_ids.append(recoverable["reservation_id"])
  104. session.commit()
  105. first = DataRuleRepository(session).begin_dataflow_create(
  106. {
  107. key: recoverable[key]
  108. for key in ("reservation_id", "dataflow_uid", "nonce")
  109. },
  110. actor_uid=actor,
  111. lease_seconds=10,
  112. )
  113. session.commit()
  114. session.execute(
  115. text(
  116. "UPDATE public.dataflow_draft_reservations "
  117. "SET lease_expires_at = CURRENT_TIMESTAMP - INTERVAL '1 second' "
  118. "WHERE id = CAST(:id AS uuid)"
  119. ),
  120. {"id": recoverable["reservation_id"]},
  121. )
  122. session.commit()
  123. recovered = _claim(
  124. url,
  125. {
  126. key: recoverable[key]
  127. for key in ("reservation_id", "dataflow_uid", "nonce")
  128. },
  129. actor,
  130. )
  131. assert first["attempt"] == 1
  132. assert recovered["status"] == "claimed"
  133. assert recovered["attempt"] == 2
  134. with Session(engine) as session:
  135. with pytest.raises(IntegrityError):
  136. DataRuleRepository(session).reserve_dataflow_draft(
  137. actor_uid=new_governance_uid()
  138. )
  139. session.commit()
  140. session.rollback()
  141. finally:
  142. with Session(engine) as session:
  143. session.execute(
  144. text(
  145. "DELETE FROM public.dataflow_draft_reservations "
  146. "WHERE id = ANY(CAST(:ids AS uuid[]))"
  147. ),
  148. {"ids": created_ids},
  149. )
  150. session.execute(
  151. text(
  152. "DELETE FROM public.users WHERE id = CAST(:id AS uuid)"
  153. ),
  154. {"id": actor},
  155. )
  156. session.commit()
  157. engine.dispose()