broker.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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. //
  64. // Retention: tuned for the M11 NATS resource-limit finding. The
  65. // 24h MaxAge on ALERTS allowed 6+ GiB of test data to accumulate
  66. // and exceed the server-level max_storage cap. With a 1h MaxAge
  67. // plus a 1 GiB MaxBytes safety cap, the stream self-trims long
  68. // before the server cap is hit. routerd is the only consumer and
  69. // processes in real time, so 1h is a generous safety window.
  70. //
  71. // See M11_NATS_INVESTIGATION.md for the full analysis.
  72. func (c *Client) EnsureStreams(ctx context.Context) error {
  73. streams := []struct {
  74. name string
  75. subjects []string
  76. age time.Duration
  77. maxBytes int64
  78. }{
  79. {"ALERTS", []string{"alerts.>"}, 1 * time.Hour, 1 << 30}, // 1h, 1 GiB
  80. {"DELIVERIES", []string{"deliveries.>"}, 1 * time.Hour, 100 << 20}, // 1h, 100 MiB
  81. {"DLQ", []string{"dlq.>"}, 1 * time.Hour, 10 << 20}, // 1h, 10 MiB
  82. }
  83. for _, s := range streams {
  84. _, err := c.js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
  85. Name: s.name,
  86. Subjects: s.subjects,
  87. MaxAge: s.age,
  88. MaxBytes: s.maxBytes,
  89. Discard: jetstream.DiscardOld,
  90. Storage: jetstream.FileStorage,
  91. })
  92. if err != nil {
  93. return fmt.Errorf("ensure stream %s: %w", s.name, err)
  94. }
  95. }
  96. return nil
  97. }
  98. // AlertsSubject returns the subject for an alert destined to a company.
  99. func AlertsSubject(companyID string) string {
  100. return "alerts." + companyID
  101. }
  102. // DeliveriesSubject returns the per-channel subject for a (company, channel).
  103. func DeliveriesSubject(channel, companyID string) string {
  104. return "deliveries." + channel + "." + companyID
  105. }
  106. // DLQSubject returns the per-channel DLQ subject.
  107. func DLQSubject(channel, companyID string) string {
  108. return "dlq." + channel + "." + companyID
  109. }