consumer.py 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. from __future__ import annotations
  2. import re
  3. from dataclasses import dataclass
  4. from datetime import datetime, timedelta, timezone
  5. from typing import Any, Callable
  6. @dataclass(frozen=True)
  7. class ClaimedEvent:
  8. event_id: str
  9. event_type: str
  10. payload: dict[str, Any]
  11. attempts: int
  12. @dataclass(frozen=True)
  13. class DispatchResult:
  14. status: str
  15. attempts: int
  16. available_at: datetime | None = None
  17. last_error: str | None = None
  18. record_consumption: bool = False
  19. def retry_delay(
  20. attempts: int,
  21. *,
  22. base_seconds: int = 2,
  23. maximum_seconds: int = 300,
  24. ) -> timedelta:
  25. seconds = min(maximum_seconds, base_seconds * (2 ** max(0, attempts - 1)))
  26. return timedelta(seconds=seconds)
  27. def _redact_error(error: Exception) -> str:
  28. message = str(error)
  29. message = re.sub(
  30. r"(?i)(secret[-_ ]?token|api[-_ ]?key|authorization|bearer)(?:[=: ]+\S+)?",
  31. "[redacted]",
  32. message,
  33. )
  34. return message[:1000]
  35. def dispatch_event(
  36. event: ClaimedEvent,
  37. handler: Callable[[dict[str, Any]], None],
  38. *,
  39. already_processed: bool = False,
  40. max_attempts: int = 5,
  41. now: datetime | None = None,
  42. ) -> DispatchResult:
  43. if already_processed:
  44. return DispatchResult(status="published", attempts=event.attempts)
  45. now = now or datetime.now(timezone.utc)
  46. try:
  47. handler(event.payload)
  48. except Exception as exc:
  49. attempts = event.attempts + 1
  50. if attempts >= max_attempts:
  51. return DispatchResult(
  52. status="dead_letter",
  53. attempts=attempts,
  54. last_error=_redact_error(exc),
  55. )
  56. return DispatchResult(
  57. status="pending",
  58. attempts=attempts,
  59. available_at=now + retry_delay(attempts),
  60. last_error=_redact_error(exc),
  61. )
  62. return DispatchResult(
  63. status="published",
  64. attempts=event.attempts + 1,
  65. record_consumption=True,
  66. )