fix kafka message persistence completion
This commit is contained in:
@@ -41,7 +41,8 @@ type testVerificationStore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testHTTPInfrastructureStore struct {
|
type testHTTPInfrastructureStore struct {
|
||||||
deadLetters []types.KafkaDeadLetter
|
deadLetters []types.KafkaDeadLetter
|
||||||
|
markSucceededErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
type testTenantEntryStore struct {
|
type testTenantEntryStore struct {
|
||||||
@@ -110,6 +111,9 @@ func (s *testHTTPInfrastructureStore) BeginKafkaConsume(_ types.TenantID, record
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *testHTTPInfrastructureStore) MarkKafkaConsumeSucceeded(_ types.TenantID, _, _ string, _ time.Time) error {
|
func (s *testHTTPInfrastructureStore) MarkKafkaConsumeSucceeded(_ types.TenantID, _, _ string, _ time.Time) error {
|
||||||
|
if s.markSucceededErr != nil {
|
||||||
|
return s.markSucceededErr
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinode/chat/server/logs"
|
"github.com/tinode/chat/server/logs"
|
||||||
@@ -20,6 +21,9 @@ const (
|
|||||||
|
|
||||||
imYouthActionMessage = "message"
|
imYouthActionMessage = "message"
|
||||||
imYouthActionCall = "call"
|
imYouthActionCall = "call"
|
||||||
|
|
||||||
|
imSecurityPolicyCacheTTL = 5 * time.Second
|
||||||
|
imSecurityPolicyErrorCacheTTL = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type imPolicyDecision struct {
|
type imPolicyDecision struct {
|
||||||
@@ -29,6 +33,17 @@ type imPolicyDecision struct {
|
|||||||
Details map[string]any
|
Details map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type imSecurityPolicyCacheEntry struct {
|
||||||
|
policy *types.SecurityPolicy
|
||||||
|
err error
|
||||||
|
expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var imSecurityPolicyCache = struct {
|
||||||
|
sync.Mutex
|
||||||
|
entries map[string]imSecurityPolicyCacheEntry
|
||||||
|
}{entries: make(map[string]imSecurityPolicyCacheEntry)}
|
||||||
|
|
||||||
func imAllowDecision() imPolicyDecision {
|
func imAllowDecision() imPolicyDecision {
|
||||||
return imPolicyDecision{Allowed: true}
|
return imPolicyDecision{Allowed: true}
|
||||||
}
|
}
|
||||||
@@ -119,7 +134,28 @@ func imSecurityPolicy(tenantID types.TenantID, tenantCode string) (*types.Securi
|
|||||||
if tenantCode == "" {
|
if tenantCode == "" {
|
||||||
return nil, types.ErrNotFound
|
return nil, types.ErrNotFound
|
||||||
}
|
}
|
||||||
|
key := fmt.Sprintf("%d:%s", tenantID, tenantCode)
|
||||||
|
now := time.Now()
|
||||||
|
imSecurityPolicyCache.Lock()
|
||||||
|
if cached, ok := imSecurityPolicyCache.entries[key]; ok && now.Before(cached.expires) {
|
||||||
|
imSecurityPolicyCache.Unlock()
|
||||||
|
return cached.policy, cached.err
|
||||||
|
}
|
||||||
|
if store.ModerationConfig == nil {
|
||||||
|
imSecurityPolicyCache.Unlock()
|
||||||
|
return nil, types.ErrUnsupported
|
||||||
|
}
|
||||||
policy, err := store.ModerationConfig.SecurityPolicy(tenantCode)
|
policy, err := store.ModerationConfig.SecurityPolicy(tenantCode)
|
||||||
|
ttl := imSecurityPolicyCacheTTL
|
||||||
|
if err != nil {
|
||||||
|
ttl = imSecurityPolicyErrorCacheTTL
|
||||||
|
}
|
||||||
|
imSecurityPolicyCache.entries[key] = imSecurityPolicyCacheEntry{
|
||||||
|
policy: policy,
|
||||||
|
err: err,
|
||||||
|
expires: now.Add(ttl),
|
||||||
|
}
|
||||||
|
imSecurityPolicyCache.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,9 @@ func prepareKafkaMessagePersistItem(ctx context.Context, message kafka.Message,
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
if !claimed {
|
if !claimed {
|
||||||
|
if consume != nil && consume.Status != types.KafkaConsumeSucceeded && consume.Status != types.KafkaConsumeDead {
|
||||||
|
return nil, false, fmt.Errorf("%w: event=%s status=%s", errKafkaConsumeInProgress, envelope.EventID, consume.Status)
|
||||||
|
}
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
item := &kafkaMessagePersistItem{envelope: envelope, tenantID: tenantID}
|
item := &kafkaMessagePersistItem{envelope: envelope, tenantID: tenantID}
|
||||||
@@ -311,6 +314,8 @@ func persistKafkaMessageIndividually(ctx context.Context, item *kafkaMessagePers
|
|||||||
// preparing the item.
|
// preparing the item.
|
||||||
immetrics.KafkaMessagePersistTotal.WithLabelValues(kind, "retry_exhausted").Inc()
|
immetrics.KafkaMessagePersistTotal.WithLabelValues(kind, "retry_exhausted").Inc()
|
||||||
immetrics.MessagePersistDuration.WithLabelValues(kind, "retry_exhausted").Observe(time.Since(started).Seconds())
|
immetrics.MessagePersistDuration.WithLabelValues(kind, "retry_exhausted").Observe(time.Since(started).Seconds())
|
||||||
|
logs.Err.Printf("kafka message persist retry exhausted: tenant=%d event=%s topic=%s seq=%d kind=%s err=%v",
|
||||||
|
item.tenantID, item.envelope.EventID, item.payload.Message.Topic, item.payload.Message.SeqId, kind, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -341,8 +346,16 @@ func finishKafkaMessagePersist(ctx context.Context, item *kafkaMessagePersistIte
|
|||||||
return fmt.Errorf("file lifecycle enqueue failed: %w", err)
|
return fmt.Errorf("file lifecycle enqueue failed: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(item.tenantID, item.envelope.EventID,
|
if err := store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(item.tenantID, item.envelope.EventID,
|
||||||
kafkaBus.config.ConsumerGroup, time.Now().UTC().Round(time.Millisecond))
|
kafkaBus.config.ConsumerGroup, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||||
|
if errors.Is(err, types.ErrNotFound) {
|
||||||
|
logs.Warn.Printf("kafka message persist consume state missing after durable write: tenant=%d event=%s topic=%s seq=%d",
|
||||||
|
item.tenantID, item.envelope.EventID, item.payload.Message.Topic, item.payload.Message.SeqId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func kafkaMessagePersistFailureReason(err error) string {
|
func kafkaMessagePersistFailureReason(err error) string {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/segmentio/kafka-go"
|
"github.com/segmentio/kafka-go"
|
||||||
|
"github.com/tinode/chat/server/store"
|
||||||
"github.com/tinode/chat/server/store/types"
|
"github.com/tinode/chat/server/store/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -253,3 +254,30 @@ func TestRunKafkaMessageSingleRetriesStopsOnSuccess(t *testing.T) {
|
|||||||
t.Fatalf("calls operation=%d failed=%d, want 3/2", operationCalls, markFailedCalls)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user