feat(push-gateway,msg): redis pubsub backplane for real-time notifications
M7: ADR-040 Redis Pub/Sub as state routing backplane
- push-gateway: remove Kafka consumer, add SSE endpoint
- SSE: subscribe to Redis user:{userId}:notify on connect
- msg: publish notifications to Redis Pub/Sub instead of HTTP push
- docker-compose: remove Kafka env from push-gateway
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
// variables. Required variables (JWT_SECRET in production, PUSH_INTERNAL_TOKEN
|
||||
// or INTERNAL_API_TOKEN when not in DevMode) cause a fatal exit when missing.
|
||||
//
|
||||
// v2 alignment (ARB-013 / msg contract): Kafka topic default changed to
|
||||
// edu.notify.notification.sent; PUSH_INTERNAL_TOKEN is the canonical env var
|
||||
// for /internal/* auth (INTERNAL_API_TOKEN kept as a backward-compat alias).
|
||||
// M7 (ADR-040): Kafka config removed — push-gateway no longer mounts Kafka
|
||||
// directly. Real-time push is routed through Redis Pub/Sub backplane
|
||||
// (channel: user:{userId}:notify). PUSH_INTERNAL_TOKEN is the canonical env
|
||||
// var for /internal/* auth (INTERNAL_API_TOKEN kept as a backward-compat
|
||||
// alias).
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -29,10 +31,6 @@ type Config struct {
|
||||
HeartbeatInterval int // seconds
|
||||
// JWT RS256 (via shared-go/jwks, iam /.well-known/jwks.json)
|
||||
JWKSURL string
|
||||
// Kafka
|
||||
KafkaBrokers []string
|
||||
KafkaNotificationTopic string
|
||||
KafkaConsumerGroup string
|
||||
// Instance identity (for Redis SET membership)
|
||||
InstanceID string
|
||||
}
|
||||
@@ -80,11 +78,6 @@ func Load() *Config {
|
||||
MaxConnsPerUser: env.GetInt("MAX_CONNS_PER_USER", 5),
|
||||
HeartbeatInterval: env.GetInt("HEARTBEAT_INTERVAL_SECONDS", 30),
|
||||
JWKSURL: env.Get("JWKS_URL", "http://localhost:50052/.well-known/jwks.json"),
|
||||
KafkaBrokers: parseBrokers(env.Get("KAFKA_BROKERS", "localhost:9092")),
|
||||
// ARB-013 canonical topic name: edu.<domain>.<aggregate>.<action>.
|
||||
// msg's Outbox publisher emits to this topic.
|
||||
KafkaNotificationTopic: env.Get("KAFKA_NOTIFICATION_TOPIC", "edu.notify.notification.sent"),
|
||||
KafkaConsumerGroup: env.Get("KAFKA_CONSUMER_GROUP", "push-gateway"),
|
||||
InstanceID: env.Get("INSTANCE_ID", generateInstanceID()),
|
||||
}
|
||||
}
|
||||
@@ -105,18 +98,6 @@ func parseOrigins(raw string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// parseBrokers splits a comma-separated list of Kafka broker addresses.
|
||||
func parseBrokers(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if trimmed := strings.TrimSpace(p); trimmed != "" {
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// generateInstanceID returns a stable process-unique identifier. Falls back to
|
||||
// the hostname when INSTANCE_ID is not explicitly set, allowing each replica to
|
||||
// be uniquely identifiable in the Redis online-presence SET.
|
||||
|
||||
@@ -32,13 +32,6 @@ func TestLoadDevMode(t *testing.T) {
|
||||
if cfg.HeartbeatInterval != 30 {
|
||||
t.Errorf("HeartbeatInterval = %d, want 30", cfg.HeartbeatInterval)
|
||||
}
|
||||
// ARB-013: canonical topic name is edu.notify.notification.sent.
|
||||
if cfg.KafkaNotificationTopic != "edu.notify.notification.sent" {
|
||||
t.Errorf("KafkaNotificationTopic = %q, want edu.notify.notification.sent", cfg.KafkaNotificationTopic)
|
||||
}
|
||||
if cfg.KafkaConsumerGroup != "push-gateway" {
|
||||
t.Errorf("KafkaConsumerGroup = %q, want push-gateway", cfg.KafkaConsumerGroup)
|
||||
}
|
||||
if cfg.InstanceID == "" {
|
||||
t.Error("InstanceID should default to hostname, got empty")
|
||||
}
|
||||
@@ -113,17 +106,6 @@ func TestParseOrigins(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseBrokers verifies comma-separated broker parsing.
|
||||
func TestParseBrokers(t *testing.T) {
|
||||
got := parseBrokers("kafka1:9092,kafka2:9092,kafka3:9092")
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("parseBrokers returned %d items, want 3", len(got))
|
||||
}
|
||||
if got[0] != "kafka1:9092" || got[1] != "kafka2:9092" || got[2] != "kafka3:9092" {
|
||||
t.Errorf("parseBrokers = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateInstanceID verifies fallback to hostname.
|
||||
func TestGenerateInstanceID(t *testing.T) {
|
||||
id := generateInstanceID()
|
||||
|
||||
Reference in New Issue
Block a user