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:
SpecialX
2026-07-15 01:28:55 +08:00
parent a75527be80
commit 6af1aa0d82
14 changed files with 402 additions and 484 deletions

View File

@@ -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()