Refactor Kafka publishing without DB outbox

This commit is contained in:
richjohnrambo
2026-09-01 23:00:14 +08:00
parent 18237cf3ec
commit 929f847003
25 changed files with 416 additions and 478 deletions
+193
View File
@@ -1,6 +1,8 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
@@ -9,6 +11,7 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/segmentio/kafka-go"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
@@ -901,6 +904,196 @@ func TestHandleBroadcastDataFiltersInvalidMentions(t *testing.T) {
verifyResponseCodes(helper.results[0], []int{http.StatusAccepted}, t)
}
func TestSaveMessageMaybeKafkaPublishesWithoutDBPersist(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldKafkaBus := kafkaBus
oldMessages := store.Messages
oldPublisher := publishKafkaMessagePersist
defer func() {
kafkaBus = oldKafkaBus
store.Messages = oldMessages
publishKafkaMessagePersist = oldPublisher
}()
kafkaBus = &kafkaRuntime{}
store.Messages = mock_store.NewMockMessagesPersistenceInterface(ctrl)
var publishedTopic string
var publishedKey string
var publishedEnvelope kafkaEnvelope
publishKafkaMessagePersist = func(_ context.Context, topicName, key string, envelope kafkaEnvelope) error {
publishedTopic = topicName
publishedKey = key
publishedEnvelope = envelope
return nil
}
msg := &types.Message{
TenantID: 1,
Topic: "grp-test",
SeqId: 1,
From: types.Uid(1).String(),
Head: types.KVMap{"x-im-client-mid": "client-1"},
Content: "hello",
}
got, err := saveMessageMaybeKafka(context.Background(), 1, msg, []string{"file1"}, true)
if err != nil {
t.Fatalf("saveMessageMaybeKafka returned error: %v", err)
}
if got == nil || got.SeqId != 1 || !got.MarkedReadBySender {
t.Fatalf("unexpected save result: %#v", got)
}
if msg.ClientMsgID != "client-1" {
t.Fatalf("client message id not normalized: %q", msg.ClientMsgID)
}
if publishedTopic != "message_persist" {
t.Fatalf("unexpected kafka topic: %q", publishedTopic)
}
if publishedKey != "grp-test" {
t.Fatalf("unexpected kafka key: %q", publishedKey)
}
if publishedEnvelope.EventType != "message.persist.group" {
t.Fatalf("unexpected event type: %q", publishedEnvelope.EventType)
}
if publishedEnvelope.TenantID != 1 || publishedEnvelope.TenantCode != "" {
t.Fatalf("unexpected tenant routing fields: tenant_id=%d tenant_code=%q", publishedEnvelope.TenantID, publishedEnvelope.TenantCode)
}
if publishedEnvelope.IdempotencyKey != "client-1" {
t.Fatalf("unexpected idempotency key: %q", publishedEnvelope.IdempotencyKey)
}
}
func TestShouldTryHotMessageFallback(t *testing.T) {
if !shouldTryHotMessageFallback(types.TopicCatGrp, 159, &types.QueryOpt{Since: 159, Limit: 1}) {
t.Fatal("expected fallback for latest group seq query")
}
if shouldTryHotMessageFallback(types.TopicCatP2P, 159, &types.QueryOpt{Since: 159, Limit: 1}) {
t.Fatal("did not expect fallback for p2p topic")
}
if shouldTryHotMessageFallback(types.TopicCatGrp, 159, &types.QueryOpt{Since: 1, Limit: 1}) {
t.Fatal("did not expect fallback for old history query")
}
if shouldTryHotMessageFallback(types.TopicCatGrp, 159, &types.QueryOpt{Since: 159, Limit: 50}) {
t.Fatal("did not expect fallback for broad query")
}
}
func TestFilterHotMessagesForUserSkipsDeleted(t *testing.T) {
uid := types.Uid(1)
now := time.Now()
messages := []types.Message{
{SeqId: 1},
{SeqId: 2, DeletedFor: []types.SoftDelete{{User: uid.String()}}},
{SeqId: 3, DeletedFor: []types.SoftDelete{{User: uid.UserId()}}},
{SeqId: 4, DeletedAt: &now},
{SeqId: 5, DelId: 10},
}
filtered := filterHotMessagesForUser(messages, uid)
if len(filtered) != 1 || filtered[0].SeqId != 1 {
t.Fatalf("unexpected filtered messages: %#v", filtered)
}
}
func TestMarkKafkaDeadLetterPublishesBeforeDBWrite(t *testing.T) {
oldKafkaBus := kafkaBus
oldPublisher := publishKafkaDeadLetter
oldInfrastructure := store.HTTPInfrastructure
defer func() {
kafkaBus = oldKafkaBus
publishKafkaDeadLetter = oldPublisher
store.HTTPInfrastructure = oldInfrastructure
}()
kafkaBus = &kafkaRuntime{config: kafkaConfig{ConsumerGroup: "im-server"}}
infrastructure := &testHTTPInfrastructureStore{}
store.HTTPInfrastructure = infrastructure
var publishedEnvelope kafkaEnvelope
publishKafkaDeadLetter = func(_ context.Context, topicName, key string, envelope kafkaEnvelope) error {
if topicName != "dead_letter" || key != "event-1" {
t.Fatalf("unexpected dead letter publish target topic=%q key=%q", topicName, key)
}
publishedEnvelope = envelope
return nil
}
original := kafkaEnvelope{
EventID: "event-1",
TenantID: 1,
EventType: "message.persist.group",
BizType: "message",
BizID: "grp-test:1",
}
consume := &types.KafkaConsumeRecord{
EventID: "event-1",
ConsumerGroup: "im-server",
SourceTopic: "im.message.persist",
RetryCount: 3,
}
if err := markKafkaDeadLetter(context.Background(), 1, original, consume, "DB_FAILED", "boom"); err != nil {
t.Fatalf("markKafkaDeadLetter returned error: %v", err)
}
if len(infrastructure.deadLetters) != 0 {
t.Fatalf("dead letter was written before dead_letter topic consumption: %#v", infrastructure.deadLetters)
}
if publishedEnvelope.EventID == "" || publishedEnvelope.TenantID != 1 {
t.Fatalf("dead letter event was not published correctly: %#v", publishedEnvelope)
}
}
func TestHandleKafkaDeadLetterWritesDB(t *testing.T) {
oldKafkaBus := kafkaBus
oldInfrastructure := store.HTTPInfrastructure
defer func() {
kafkaBus = oldKafkaBus
store.HTTPInfrastructure = oldInfrastructure
}()
kafkaBus = nil
infrastructure := &testHTTPInfrastructureStore{}
store.HTTPInfrastructure = infrastructure
original := kafkaEnvelope{
EventID: "event-1",
TenantID: 1,
EventType: "message.persist.group",
BizType: "message",
BizID: "grp-test:1",
}
encodedOriginal, _ := json.Marshal(original)
dead := types.KafkaDeadLetter{
DeadID: "dead-1",
EventID: "event-1",
SourceTopic: "im.message.persist",
ConsumerGroup: "im-server",
EventType: "message.persist.group",
BizType: "message",
BizID: "grp-test:1",
Envelope: encodedOriginal,
RetryCount: 3,
LastReason: "DB_FAILED",
LastError: "boom",
Status: types.KafkaDeadStatusDead,
}
payload, _ := json.Marshal(dead)
envelope := kafkaEnvelope{
EventID: "dead-1",
TenantID: 1,
EventType: "kafka.dead_letter.created",
ParentEventID: "event-1",
Payload: payload,
}
if err := handleKafkaDeadLetter(context.Background(), envelope, kafka.Message{}); err != nil {
t.Fatalf("handleKafkaDeadLetter returned error: %v", err)
}
if len(infrastructure.deadLetters) != 1 {
t.Fatalf("expected one dead letter write, got %d", len(infrastructure.deadLetters))
}
if infrastructure.deadLetters[0].DeadID != "dead-1" || infrastructure.deadLetters[0].EventID != "event-1" {
t.Fatalf("unexpected dead letter write: %#v", infrastructure.deadLetters[0])
}
}
func TestHandleBroadcastDataInactiveTopic(t *testing.T) {
numUsers := 2
helper := TopicTestHelper{}