dlq.go 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. // Package dlq is the M8 dead-letter queue writer used by
  2. // deliverd-fcm and deliverd-telegram. When a delivery
  3. // exhausts its retry budget, the worker calls Write()
  4. // once to insert a forensic row into deliveries_dlq and
  5. // mark the live deliveries row as status='dlq'.
  6. //
  7. // The package is intentionally tiny: one function, one
  8. // row insert, one row update. The retry loop itself
  9. // stays in the deliverd main.go (it's small and per-
  10. // channel, since the HTTP target and the request shape
  11. // differ per channel).
  12. //
  13. // Schema reference: migrations/008_dlq.up.sql.
  14. package dlq
  15. import (
  16. "context"
  17. "encoding/json"
  18. "fmt"
  19. "git3.techno-world.net/lrosales/broad-announce/internal/postgres"
  20. )
  21. // Entry is the minimal payload we need to write a DLQ
  22. // row. It mirrors the columns of `deliveries_dlq` that
  23. // the worker fills in; everything else (id, created_at,
  24. // discarded) is set by the database.
  25. type Entry struct {
  26. AlertID string
  27. CompanyID string
  28. IndividualID string
  29. Channel string // fcm | telegram | …
  30. Target string // fcm_token, chat_id, …
  31. OriginalSubject string // deliveries.fcm.<co>, for replay
  32. Attempts int
  33. LastError string
  34. Payload json.RawMessage // raw NATS envelope bytes
  35. }
  36. // Write inserts one row into deliveries_dlq. It does
  37. // NOT touch the per-attempt `deliveries` rows — those
  38. // are the audit trail of what each attempt saw (status
  39. // 'failed' or 'sent'), and we keep them as-is so the
  40. // operator can see "this alert had N attempts and all
  41. // failed" at a glance. The DLQ row is the source of
  42. // truth for "this alert hit the DLQ" and the gateway
  43. // for replay.
  44. //
  45. // Returns the new DLQ row's id (for logging).
  46. func Write(ctx context.Context, pool *postgres.Pool, e Entry) (int64, error) {
  47. if e.OriginalSubject == "" {
  48. return 0, fmt.Errorf("dlq.Write: OriginalSubject is required for replay")
  49. }
  50. if e.Payload == nil {
  51. // Store the literal JSON null rather than an
  52. // empty byte slice so CH's String column gets
  53. // a sensible value.
  54. e.Payload = json.RawMessage("null")
  55. }
  56. // The DLQ insert.
  57. var newID int64
  58. err := pool.QueryRow(ctx, `
  59. INSERT INTO deliveries_dlq
  60. (alert_id, company_id, individual_id, channel, target,
  61. original_subject, attempts, last_error, payload)
  62. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
  63. RETURNING id
  64. `,
  65. e.AlertID, e.CompanyID, e.IndividualID, e.Channel, e.Target,
  66. e.OriginalSubject, e.Attempts, e.LastError, e.Payload,
  67. ).Scan(&newID)
  68. if err != nil {
  69. return 0, fmt.Errorf("dlq insert: %w", err)
  70. }
  71. return newID, nil
  72. }