package main import ( "context" "encoding/json" "errors" "strings" "time" "github.com/segmentio/kafka-go" "github.com/tinode/chat/server/logs" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) var errKafkaConsumeInProgress = errors.New("kafka event is already being processed") const ( configEventTenantUpdated = "tenant.updated" configEventTenantStatusChanged = "tenant.status.changed" configEventTenantLimitsUpdated = "tenant.limits.updated" configEventTenantFeaturesUpdated = "tenant.features.updated" configEventDictionaryUpdated = "dictionary.updated" configEventThirdPartyUpdated = "third_party.updated" configEventRegistrationUpdated = "registration.config.updated" configEventAppReleaseUpdated = "app.release.updated" configEventSecurityPolicyUpdated = "security.policy.updated" configEventSensitiveWordsUpdated = "sensitive_words.updated" configEventMessageKeysUpdated = "message.keys.updated" configEventMessageKeysActivated = "message.keys.activated" ) type configEventPayload struct { TenantCode string `json:"tenant_code"` TenantID types.TenantID `json:"tenant_id"` Group string `json:"group"` Keys []string `json:"keys"` } var kafkaConfigEventConsumerGroup string func startKafkaConfigEventConsumer() { kafkaConfigEventConsumerGroup = startKafkaBroadcastConsumer("config_event", handleKafkaConfigEvent) } func handleKafkaConfigEvent(ctx context.Context, envelope kafkaEnvelope, _ kafka.Message) error { if strings.TrimSpace(envelope.EventID) == "" { return nil } tenantID, _ := kafkaTenantIDFromEnvelopeOrPayload(envelope) if tenantID.IsZero() { tenantID = types.TenantID(1) } consumerGroup := kafkaConfigEventConsumerGroup consume, claimed, err := beginKafkaConsumeForTopicWithGroup(tenantID, envelope, "config_event", consumerGroup) if err != nil { return err } if !claimed { logs.Info.Printf("kafka config event skipped: event=%s group=%s status=%s", envelope.EventID, consumerGroup, consume.Status) return nil } if err = executeKafkaConfigEvent(envelope); err != nil { reason := kafkaCommandErrorReason(err) failed, markErr := store.HTTPInfrastructure.MarkKafkaConsumeFailed(tenantID, envelope.EventID, consumerGroup, reason, err.Error(), time.Now().UTC().Round(time.Millisecond)) if markErr != nil { return markErr } if failed.RetryCount >= kafkaBus.config.MaxRetryCount { return markKafkaDeadLetter(ctx, tenantID, envelope, failed, reason, err.Error()) } return err } return store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(tenantID, envelope.EventID, consumerGroup, time.Now().UTC().Round(time.Millisecond)) } func executeKafkaConfigEvent(envelope kafkaEnvelope) error { var payload configEventPayload if len(envelope.Payload) > 0 { if err := json.Unmarshal(envelope.Payload, &payload); err != nil { return err } } tenantCode := firstNonEmpty(payload.TenantCode, envelope.TenantCode) tenantID := payload.TenantID if tenantID.IsZero() && tenantCode != "" { if tenant, err := store.Tenants.GetByCode(tenantCode); err == nil && tenant != nil { tenantID = tenant.ID } } switch strings.TrimSpace(envelope.EventType) { case configEventTenantUpdated, configEventTenantStatusChanged, configEventTenantLimitsUpdated, configEventTenantFeaturesUpdated: store.InvalidateTenantConfigCache(tenantCode, tenantID) if strings.TrimSpace(envelope.EventType) == configEventTenantFeaturesUpdated { store.InvalidateModerationConfigCache(tenantCode, false, true, false) } case configEventDictionaryUpdated, configEventThirdPartyUpdated: store.InvalidateThirdPartyConfigCache(payload.Group) if strings.TrimSpace(envelope.EventType) == configEventDictionaryUpdated { store.InvalidateDictionaryConfigCache(payload.Group) } if payload.Group == "moderation" { store.InvalidateModerationConfigCache(tenantCode, true, false, false) } case configEventRegistrationUpdated: store.InvalidateRegistrationConfigCache() case configEventAppReleaseUpdated: store.InvalidateAppReleaseConfigCache() case configEventSecurityPolicyUpdated: store.InvalidateModerationConfigCache(tenantCode, false, true, false) store.InvalidateSecurityPolicyCache(tenantCode) case configEventSensitiveWordsUpdated: store.InvalidateModerationConfigCache(tenantCode, false, false, true) case configEventMessageKeysUpdated, configEventMessageKeysActivated: store.InvalidateMessageEncryptionKeys(tenantID) if strings.TrimSpace(envelope.EventType) == configEventMessageKeysUpdated && envelope.Version > 0 { if err := store.PrepareMessageEncryptionKey(tenantID, tenantCode, int(envelope.Version)); err != nil { return err } } default: store.InvalidateTenantConfigCache(tenantCode, tenantID) store.InvalidateThirdPartyConfigCache(payload.Group) store.InvalidateRegistrationConfigCache() store.InvalidateAppReleaseConfigCache() store.InvalidateModerationConfigCache(tenantCode, true, true, true) store.InvalidateSecurityPolicyCache(tenantCode) } logs.Info.Printf("kafka config event applied: event=%s type=%s tenant=%s group=%s", envelope.EventID, envelope.EventType, tenantCode, payload.Group) return nil } func kafkaTenantIDFromEnvelopeOrPayload(envelope kafkaEnvelope) (types.TenantID, error) { var payload configEventPayload if len(envelope.Payload) > 0 { _ = json.Unmarshal(envelope.Payload, &payload) } if payload.TenantID.IsValid() { return payload.TenantID, nil } return kafkaTenantID(firstNonEmpty(envelope.TenantCode, payload.TenantCode)) } func beginKafkaConsumeForTopic(tenantID types.TenantID, envelope kafkaEnvelope, topicName string) (*types.KafkaConsumeRecord, bool, error) { if kafkaBus == nil { return nil, false, types.ErrUnsupported } return beginKafkaConsumeForTopicWithGroup(tenantID, envelope, topicName, kafkaBus.config.ConsumerGroup) } func beginKafkaConsumeForTopicWithGroup(tenantID types.TenantID, envelope kafkaEnvelope, topicName, consumerGroup string) (*types.KafkaConsumeRecord, bool, error) { if kafkaBus == nil { return nil, false, types.ErrUnsupported } if consumerGroup = strings.TrimSpace(consumerGroup); consumerGroup == "" { consumerGroup = kafkaBus.config.ConsumerGroup } deadline := time.Now().UTC().Add(5 * time.Minute).Round(time.Millisecond) record, claimed, err := store.HTTPInfrastructure.BeginKafkaConsume(tenantID, &types.KafkaConsumeRecord{ EventID: envelope.EventID, ConsumerGroup: consumerGroup, SourceTopic: kafkaTopic(topicName), EventType: envelope.EventType, BizType: envelope.BizType, BizID: envelope.BizID, }, deadline) if err != nil || claimed || record == nil { return record, claimed, err } if record.Status != types.KafkaConsumeSucceeded && record.Status != types.KafkaConsumeDead { return record, false, errKafkaConsumeInProgress } return record, false, nil }