pipeline_outbox_repo.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. from __future__ import annotations
  2. import uuid
  3. from datetime import datetime
  4. from typing import Any
  5. from sqlalchemy import select
  6. from supply_infra.db.models.pipeline_outbox import PipelineOutbox
  7. from supply_infra.db.repositories.base import BaseRepository
  8. from supply_infra.pipeline.enums import OutboxStatus
  9. class PipelineOutboxRepository(BaseRepository[PipelineOutbox]):
  10. model = PipelineOutbox
  11. def get_by_idempotency_key(self, key: str) -> PipelineOutbox | None:
  12. stmt = select(PipelineOutbox).where(PipelineOutbox.idempotency_key == key)
  13. return self.session.scalar(stmt)
  14. def get(self, outbox_id: str, *, for_update: bool = False) -> PipelineOutbox | None:
  15. stmt = select(PipelineOutbox).where(PipelineOutbox.outbox_id == outbox_id)
  16. if for_update:
  17. stmt = stmt.with_for_update()
  18. return self.session.scalar(stmt)
  19. def enqueue(
  20. self,
  21. *,
  22. run_id: str,
  23. step_run_id: str,
  24. effect_type: str,
  25. idempotency_key: str,
  26. payload_hash: str,
  27. payload: dict[str, Any] | None,
  28. payload_uri: str | None = None,
  29. dry_run: bool = False,
  30. ) -> PipelineOutbox:
  31. existing = self.get_by_idempotency_key(idempotency_key)
  32. if existing is not None:
  33. if existing.payload_hash != payload_hash:
  34. raise ValueError(
  35. "idempotency key reused with a different payload: "
  36. f"{idempotency_key}"
  37. )
  38. return existing
  39. return self.add(
  40. PipelineOutbox(
  41. outbox_id=str(uuid.uuid4()),
  42. run_id=run_id,
  43. step_run_id=step_run_id,
  44. effect_type=effect_type,
  45. idempotency_key=idempotency_key,
  46. payload_hash=payload_hash,
  47. payload_json=payload,
  48. payload_uri=payload_uri,
  49. status=OutboxStatus.PENDING.value,
  50. dry_run=dry_run,
  51. )
  52. )
  53. def mark_sending(self, row: PipelineOutbox, *, now: datetime) -> None:
  54. row.status = OutboxStatus.SENDING.value
  55. row.attempt_count = int(row.attempt_count or 0) + 1
  56. row.last_attempt_at = now
  57. row.next_retry_at = None
  58. row.record_error = None
  59. self.session.flush()
  60. def mark_created(
  61. self,
  62. row: PipelineOutbox,
  63. *,
  64. external_id: str,
  65. external_name: str | None,
  66. response: dict[str, Any] | None,
  67. ) -> None:
  68. row.status = OutboxStatus.CREATED.value
  69. row.external_id = external_id
  70. row.external_name = external_name
  71. row.response_json = response
  72. row.record_error = None
  73. self.session.flush()
  74. def mark_retryable_failed(
  75. self,
  76. row: PipelineOutbox,
  77. *,
  78. error: str,
  79. response: dict[str, Any] | None = None,
  80. ) -> None:
  81. row.status = OutboxStatus.RETRYABLE_FAILED.value
  82. row.record_error = error
  83. if response is not None:
  84. row.response_json = response
  85. self.session.flush()
  86. def mark_ambiguous(self, row: PipelineOutbox, *, error: str) -> None:
  87. row.status = OutboxStatus.AMBIGUOUS.value
  88. row.record_error = error
  89. self.session.flush()
  90. def mark_succeeded(
  91. self,
  92. row: PipelineOutbox,
  93. *,
  94. response: dict[str, Any] | None = None,
  95. ) -> None:
  96. row.status = OutboxStatus.SUCCEEDED.value
  97. row.record_error = None
  98. if response is not None:
  99. row.response_json = response
  100. self.session.flush()
  101. def list_for_run(self, run_id: str) -> list[PipelineOutbox]:
  102. stmt = (
  103. select(PipelineOutbox)
  104. .where(PipelineOutbox.run_id == run_id)
  105. .order_by(PipelineOutbox.created_at)
  106. )
  107. return list(self.session.scalars(stmt).all())