broker.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // Package broker is the NATS JetStream wrapper. Every service that
  2. // publishes or subscribes goes through this package so subject
  3. // construction and stream creation stay consistent.
  4. //
  5. // Subject layout (SPEC ARCHITECTURE §6):
  6. // alerts.<company_id> – ingestd → routerd
  7. // deliveries.<channel>.<company_id> – routerd → deliverd
  8. // dlq.<channel>.<company_id> – deliverd → operator
  9. //
  10. // JetStream streams declared in EnsureStreams():
  11. // ALERTS – alerts.* (24h retention, replicas=1 for M0)
  12. // DELIVERIES – deliveries.* (1h retention)
  13. // DLQ – dlq.* (7d retention)
  14. package broker
  15. import (
  16. "context"
  17. "fmt"
  18. "time"
  19. "github.com/nats-io/nats.go"
  20. "github.com/nats-io/nats.go/jetstream"
  21. )
  22. // Client is the wrapper. One per service.
  23. type Client struct {
  24. nc *nats.Conn
  25. js jetstream.JetStream
  26. url string
  27. }
  28. // Connect dials NATS and ensures the streams exist.
  29. func Connect(ctx context.Context, url string) (*Client, error) {
  30. nc, err := nats.Connect(url, nats.Name("broad-announce"),
  31. nats.MaxReconnects(-1),
  32. nats.ReconnectWait(2*time.Second),
  33. nats.Timeout(5*time.Second),
  34. )
  35. if err != nil {
  36. return nil, fmt.Errorf("nats connect %s: %w", url, err)
  37. }
  38. js, err := jetstream.New(nc)
  39. if err != nil {
  40. nc.Close()
  41. return nil, fmt.Errorf("jetstream init: %w", err)
  42. }
  43. c := &Client{nc: nc, js: js, url: url}
  44. if err := c.EnsureStreams(ctx); err != nil {
  45. nc.Close()
  46. return nil, err
  47. }
  48. return c, nil
  49. }
  50. // Close drains and closes the connection.
  51. func (c *Client) Close() {
  52. if c.nc != nil {
  53. c.nc.Drain()
  54. }
  55. }
  56. // JS exposes the underlying JetStream context for callers that need
  57. // it (e.g. consumer creation). Prefer the helpers below for normal use.
  58. func (c *Client) JS() jetstream.JetStream { return c.js }
  59. // NC exposes the raw NATS connection for ping/flush operations.
  60. func (c *Client) NC() *nats.Conn { return c.nc }
  61. // EnsureStreams creates the three JetStream streams if they don't
  62. // exist. M0: single-node, no replication.
  63. func (c *Client) EnsureStreams(ctx context.Context) error {
  64. streams := []struct {
  65. name string
  66. subjects []string
  67. age time.Duration
  68. }{
  69. {"ALERTS", []string{"alerts.>"}, 24 * time.Hour},
  70. {"DELIVERIES", []string{"deliveries.>"}, 1 * time.Hour},
  71. {"DLQ", []string{"dlq.>"}, 7 * 24 * time.Hour},
  72. }
  73. for _, s := range streams {
  74. _, err := c.js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
  75. Name: s.name,
  76. Subjects: s.subjects,
  77. MaxAge: s.age,
  78. Storage: jetstream.FileStorage,
  79. })
  80. if err != nil {
  81. return fmt.Errorf("ensure stream %s: %w", s.name, err)
  82. }
  83. }
  84. return nil
  85. }
  86. // AlertsSubject returns the subject for an alert destined to a company.
  87. func AlertsSubject(companyID string) string {
  88. return "alerts." + companyID
  89. }
  90. // DeliveriesSubject returns the per-channel subject for a (company, channel).
  91. func DeliveriesSubject(channel, companyID string) string {
  92. return "deliveries." + channel + "." + companyID
  93. }
  94. // DLQSubject returns the per-channel DLQ subject.
  95. func DLQSubject(channel, companyID string) string {
  96. return "dlq." + channel + "." + companyID
  97. }