284 lines
8.5 KiB
Go
284 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/segmentio/kafka-go"
|
|
"github.com/tinode/chat/server/store"
|
|
"github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
func TestConsumeKafkaMessageBatchRetriesHandlerAndCommit(t *testing.T) {
|
|
handlerCalls := 0
|
|
commitCalls := 0
|
|
waitCalls := 0
|
|
messages := []kafka.Message{{Offset: 1}, {Offset: 2}}
|
|
|
|
err := consumeKafkaMessageBatch(context.Background(), messages,
|
|
func(context.Context, []kafka.Message) error {
|
|
handlerCalls++
|
|
if handlerCalls == 1 {
|
|
return errors.New("batch failed")
|
|
}
|
|
return nil
|
|
},
|
|
func(_ context.Context, committed ...kafka.Message) error {
|
|
commitCalls++
|
|
if len(committed) != len(messages) {
|
|
t.Fatalf("committed %d messages, want %d", len(committed), len(messages))
|
|
}
|
|
if commitCalls == 1 {
|
|
return errors.New("commit failed")
|
|
}
|
|
return nil
|
|
},
|
|
func(context.Context, time.Duration) error {
|
|
waitCalls++
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("consumeKafkaMessageBatch() error = %v", err)
|
|
}
|
|
if handlerCalls != 2 || commitCalls != 2 || waitCalls != 2 {
|
|
t.Fatalf("calls handler=%d commit=%d wait=%d, want 2/2/2", handlerCalls, commitCalls, waitCalls)
|
|
}
|
|
}
|
|
|
|
func TestConsumeKafkaMessageBatchDoesNotCommitWhileHandlerFails(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
handlerCalls := 0
|
|
commitCalls := 0
|
|
|
|
err := consumeKafkaMessageBatch(ctx, []kafka.Message{{Offset: 1}},
|
|
func(context.Context, []kafka.Message) error {
|
|
handlerCalls++
|
|
return errors.New("database unavailable")
|
|
},
|
|
func(context.Context, ...kafka.Message) error {
|
|
commitCalls++
|
|
return nil
|
|
},
|
|
func(ctx context.Context, _ time.Duration) error {
|
|
cancel()
|
|
return ctx.Err()
|
|
})
|
|
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("consumeKafkaMessageBatch() error = %v, want context canceled", err)
|
|
}
|
|
if handlerCalls != 1 || commitCalls != 0 {
|
|
t.Fatalf("calls handler=%d commit=%d, want 1/0", handlerCalls, commitCalls)
|
|
}
|
|
}
|
|
|
|
func TestFetchKafkaMessageBatchWithRetainsPartialBatchOnFetchError(t *testing.T) {
|
|
fetchCalls := 0
|
|
fetchErr := errors.New("broker connection reset")
|
|
messages, err := fetchKafkaMessageBatchWith(context.Background(), 10, time.Second,
|
|
func(context.Context) (kafka.Message, error) {
|
|
fetchCalls++
|
|
if fetchCalls == 1 {
|
|
return kafka.Message{Partition: 2, Offset: 41}, nil
|
|
}
|
|
return kafka.Message{}, fetchErr
|
|
})
|
|
|
|
if !errors.Is(err, fetchErr) {
|
|
t.Fatalf("fetchKafkaMessageBatchWith() error = %v, want %v", err, fetchErr)
|
|
}
|
|
if len(messages) != 1 || messages[0].Partition != 2 || messages[0].Offset != 41 {
|
|
t.Fatalf("fetchKafkaMessageBatchWith() messages = %#v, want retained offset 41", messages)
|
|
}
|
|
}
|
|
|
|
func TestFetchKafkaMessageBatchWithReturnsInitialFetchErrorWithoutMessages(t *testing.T) {
|
|
fetchErr := errors.New("broker unavailable")
|
|
messages, err := fetchKafkaMessageBatchWith(context.Background(), 10, time.Second,
|
|
func(context.Context) (kafka.Message, error) {
|
|
return kafka.Message{}, fetchErr
|
|
})
|
|
|
|
if !errors.Is(err, fetchErr) {
|
|
t.Fatalf("fetchKafkaMessageBatchWith() error = %v, want %v", err, fetchErr)
|
|
}
|
|
if len(messages) != 0 {
|
|
t.Fatalf("fetchKafkaMessageBatchWith() messages = %#v, want none", messages)
|
|
}
|
|
}
|
|
|
|
func TestProcessKafkaMessagePersistBatchFlushesClaimsBeforePrepareRetry(t *testing.T) {
|
|
prepareErr := errors.New("redis temporarily unavailable")
|
|
prepareCalls := 0
|
|
persistCalls := 0
|
|
|
|
err := processKafkaMessagePersistBatch(context.Background(), []kafka.Message{{Offset: 10}, {Offset: 11}}, "group_message_persist",
|
|
func(_ context.Context, message kafka.Message, topic string) (*kafkaMessagePersistItem, bool, error) {
|
|
prepareCalls++
|
|
if topic != "group_message_persist" {
|
|
t.Fatalf("prepare topic = %q", topic)
|
|
}
|
|
if message.Offset == 11 {
|
|
return nil, false, prepareErr
|
|
}
|
|
return &kafkaMessagePersistItem{
|
|
tenantID: types.TenantID(7),
|
|
payload: kafkaMessagePersistPayload{
|
|
MessageKind: "group",
|
|
ShardMonth: "202610",
|
|
Message: types.Message{ShardMonth: "202610"},
|
|
},
|
|
}, false, nil
|
|
},
|
|
func(_ context.Context, groups map[string][]*kafkaMessagePersistItem) error {
|
|
persistCalls++
|
|
items := groups["7:202610"]
|
|
if len(items) != 1 {
|
|
t.Fatalf("persisted groups = %#v, want one claimed item", groups)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if !errors.Is(err, prepareErr) {
|
|
t.Fatalf("processKafkaMessagePersistBatch() error = %v, want %v", err, prepareErr)
|
|
}
|
|
if prepareCalls != 2 || persistCalls != 1 {
|
|
t.Fatalf("calls prepare=%d persist=%d, want 2/1", prepareCalls, persistCalls)
|
|
}
|
|
}
|
|
|
|
func TestProcessKafkaMessagePersistBatchReportsFlushAndPrepareErrors(t *testing.T) {
|
|
prepareErr := errors.New("claim failed")
|
|
persistErr := errors.New("database unavailable")
|
|
prepareCalls := 0
|
|
|
|
err := processKafkaMessagePersistBatch(context.Background(), []kafka.Message{{Offset: 10}, {Offset: 11}}, "message_persist",
|
|
func(_ context.Context, _ kafka.Message, _ string) (*kafkaMessagePersistItem, bool, error) {
|
|
prepareCalls++
|
|
if prepareCalls == 2 {
|
|
return nil, false, prepareErr
|
|
}
|
|
return &kafkaMessagePersistItem{tenantID: types.TenantID(7)}, false, nil
|
|
},
|
|
func(context.Context, map[string][]*kafkaMessagePersistItem) error {
|
|
return persistErr
|
|
})
|
|
|
|
if !errors.Is(err, persistErr) || !strings.Contains(err.Error(), prepareErr.Error()) {
|
|
t.Fatalf("processKafkaMessagePersistBatch() error = %v, want both flush and prepare errors", err)
|
|
}
|
|
}
|
|
|
|
func TestConsumeKafkaMalformedMessageRetriesCommit(t *testing.T) {
|
|
commitCalls := 0
|
|
waitCalls := 0
|
|
handlerCalls := 0
|
|
err := consumeKafkaMessage(context.Background(), kafka.Message{Value: []byte("{")},
|
|
func(context.Context, kafkaEnvelope, kafka.Message) error {
|
|
handlerCalls++
|
|
return nil
|
|
},
|
|
func(context.Context, kafka.Message) error {
|
|
commitCalls++
|
|
if commitCalls == 1 {
|
|
return errors.New("commit failed")
|
|
}
|
|
return nil
|
|
},
|
|
func(context.Context, time.Duration) error {
|
|
waitCalls++
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("consumeKafkaMessage() error = %v", err)
|
|
}
|
|
if handlerCalls != 0 || commitCalls != 2 || waitCalls != 1 {
|
|
t.Fatalf("calls handler=%d commit=%d wait=%d, want 0/2/1", handlerCalls, commitCalls, waitCalls)
|
|
}
|
|
}
|
|
|
|
func TestRunKafkaMessageSingleRetriesReturnsErrorWithoutTerminalCommit(t *testing.T) {
|
|
operationCalls := 0
|
|
markFailedCalls := 0
|
|
waitCalls := 0
|
|
|
|
err := runKafkaMessageSingleRetries(context.Background(), 3,
|
|
func() error {
|
|
operationCalls++
|
|
return errors.New("write failed")
|
|
},
|
|
func(error) (*types.KafkaConsumeRecord, error) {
|
|
markFailedCalls++
|
|
return &types.KafkaConsumeRecord{RetryCount: markFailedCalls}, nil
|
|
},
|
|
func(context.Context, time.Duration) error {
|
|
waitCalls++
|
|
return nil
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "message persistence retries exhausted") {
|
|
t.Fatalf("runKafkaMessageSingleRetries() error = %v, want retry exhaustion", err)
|
|
}
|
|
if operationCalls != 4 || markFailedCalls != 4 || waitCalls != 3 {
|
|
t.Fatalf("calls operation=%d failed=%d wait=%d, want 4/4/3",
|
|
operationCalls, markFailedCalls, waitCalls)
|
|
}
|
|
}
|
|
|
|
func TestRunKafkaMessageSingleRetriesStopsOnSuccess(t *testing.T) {
|
|
operationCalls := 0
|
|
markFailedCalls := 0
|
|
err := runKafkaMessageSingleRetries(context.Background(), 3,
|
|
func() error {
|
|
operationCalls++
|
|
if operationCalls < 3 {
|
|
return errors.New("temporary failure")
|
|
}
|
|
return nil
|
|
},
|
|
func(error) (*types.KafkaConsumeRecord, error) {
|
|
markFailedCalls++
|
|
return &types.KafkaConsumeRecord{RetryCount: markFailedCalls}, nil
|
|
},
|
|
func(context.Context, time.Duration) error { return nil })
|
|
|
|
if err != nil {
|
|
t.Fatalf("runKafkaMessageSingleRetries() error = %v", err)
|
|
}
|
|
if operationCalls != 3 || markFailedCalls != 2 {
|
|
t.Fatalf("calls operation=%d failed=%d, want 3/2", operationCalls, markFailedCalls)
|
|
}
|
|
}
|
|
|
|
func TestFinishKafkaMessagePersistTreatsMissingConsumeStateAsSucceeded(t *testing.T) {
|
|
oldInfrastructure := store.HTTPInfrastructure
|
|
oldKafkaBus := kafkaBus
|
|
defer func() {
|
|
store.HTTPInfrastructure = oldInfrastructure
|
|
kafkaBus = oldKafkaBus
|
|
}()
|
|
|
|
store.HTTPInfrastructure = &testHTTPInfrastructureStore{markSucceededErr: types.ErrNotFound}
|
|
kafkaBus = &kafkaRuntime{config: kafkaConfig{ConsumerGroup: "test-consumer"}}
|
|
|
|
err := finishKafkaMessagePersist(context.Background(), &kafkaMessagePersistItem{
|
|
envelope: kafkaEnvelope{EventID: "msg.persist.8.p2pA.1"},
|
|
tenantID: types.TenantID(8),
|
|
payload: kafkaMessagePersistPayload{
|
|
MessageKind: "p2p",
|
|
Message: types.Message{
|
|
Topic: "p2pA",
|
|
SeqId: 1,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("finishKafkaMessagePersist() error = %v, want nil", err)
|
|
}
|
|
}
|