feat: add kafka admin integration
This commit is contained in:
@@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -42,6 +43,18 @@ func (c *configCache[T]) set(key string, value T) {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *configCache[T]) delete(key string) {
|
||||
c.mu.Lock()
|
||||
delete(c.items, key)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *configCache[T]) clear() {
|
||||
c.mu.Lock()
|
||||
c.items = make(map[string]configCacheEntry[T])
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
var (
|
||||
tenantByCodeCache = newConfigCache[*types.Tenant]()
|
||||
tenantByIDCache = newConfigCache[*types.Tenant]()
|
||||
@@ -83,3 +96,43 @@ func thirdPartyCacheKey(group string, keys ...string) string {
|
||||
sort.Strings(normalized)
|
||||
return group + ":" + strings.Join(normalized, ",")
|
||||
}
|
||||
|
||||
// InvalidateTenantConfigCache clears cached tenant registry/config records. An
|
||||
// empty selector clears all tenant records.
|
||||
func InvalidateTenantConfigCache(code string, id types.TenantID) {
|
||||
if strings.TrimSpace(code) != "" {
|
||||
tenantByCodeCache.delete(strings.TrimSpace(code))
|
||||
} else {
|
||||
tenantByCodeCache.clear()
|
||||
}
|
||||
if id.IsValid() {
|
||||
tenantByIDCache.delete(formatTenantIDForCache(id))
|
||||
} else if strings.TrimSpace(code) == "" {
|
||||
tenantByIDCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateRegistrationConfigCache clears cached global registration config.
|
||||
func InvalidateRegistrationConfigCache() {
|
||||
registrationSMSCache.clear()
|
||||
}
|
||||
|
||||
// InvalidateThirdPartyConfigCache clears cached third-party config.
|
||||
func InvalidateThirdPartyConfigCache(group string) {
|
||||
if strings.TrimSpace(group) == "" {
|
||||
thirdPartyConfigCache.clear()
|
||||
return
|
||||
}
|
||||
prefix := strings.TrimSpace(group) + ":"
|
||||
thirdPartyConfigCache.mu.Lock()
|
||||
for key := range thirdPartyConfigCache.items {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
delete(thirdPartyConfigCache.items, key)
|
||||
}
|
||||
}
|
||||
thirdPartyConfigCache.mu.Unlock()
|
||||
}
|
||||
|
||||
func formatTenantIDForCache(id types.TenantID) string {
|
||||
return strconv.FormatInt(int64(id), 10)
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ type groupMessageRuntime interface {
|
||||
RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error
|
||||
CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error)
|
||||
AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error)
|
||||
ClaimShortState(namespace, id string, ttl time.Duration) (bool, error)
|
||||
}
|
||||
|
||||
var groupMsgRuntime groupMessageRuntime
|
||||
@@ -88,6 +89,14 @@ func MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device,
|
||||
return groupMsgRuntime.MarkUserOnline(tenantID, uid, sid, nodeID, device, userAgent)
|
||||
}
|
||||
|
||||
func ClaimShortState(namespace, id string, ttl time.Duration) (bool, bool, error) {
|
||||
if groupMsgRuntime == nil || namespace == "" || id == "" {
|
||||
return false, false, nil
|
||||
}
|
||||
ok, err := groupMsgRuntime.ClaimShortState(namespace, id, ttl)
|
||||
return ok, true, err
|
||||
}
|
||||
|
||||
func RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
|
||||
return nil
|
||||
@@ -169,6 +178,13 @@ func DeleteCachedObject(tenantID types.TenantID, namespace, id string) error {
|
||||
return groupMsgRuntime.DeleteCachedObject(tenantID, namespace, id)
|
||||
}
|
||||
|
||||
func InvalidateHotMessages(tenantID types.TenantID, topic string, ranges []types.Range) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.InvalidateHotMessages(tenantID, topic, ranges)
|
||||
}
|
||||
|
||||
func AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || member == "" {
|
||||
return nil
|
||||
@@ -730,6 +746,13 @@ func (r *redisGroupMessageRuntime) AllowRate(tenantID types.TenantID, scope stri
|
||||
return count <= int64(limit), int(count), ttl, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) ClaimShortState(namespace, id string, ttl time.Duration) (bool, error) {
|
||||
if ttl <= 0 {
|
||||
ttl = time.Duration(r.cfg.ShortTTL) * time.Second
|
||||
}
|
||||
return r.client.SetNX(context.Background(), r.shortStateKey(namespace, id), "1", ttl).Result()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic string, opts *types.QueryOpt, limit int) ([]int64, bool, error) {
|
||||
if opts != nil && len(opts.IdRanges) > 0 {
|
||||
seqs := expandRanges(opts.IdRanges, limit)
|
||||
@@ -851,6 +874,10 @@ func (r *redisGroupMessageRuntime) rateKey(tenantID types.TenantID, scope string
|
||||
return r.key(tenantID, "rate", scope)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) shortStateKey(namespace, id string) string {
|
||||
return r.key(types.ZeroTenantID, "short", namespace, id)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) cacheTTL(namespace string) time.Duration {
|
||||
ttl := r.cfg.CacheTTL
|
||||
if namespace == "verification" || namespace == "action_token" {
|
||||
|
||||
@@ -14,6 +14,18 @@ type HTTPInfrastructurePersistenceInterface interface {
|
||||
CreateEvent(types.TenantID, *types.BusinessEvent) error
|
||||
ListPendingEvents(types.TenantID, int, time.Time) ([]types.BusinessEvent, error)
|
||||
MarkEventDelivered(types.TenantID, string, time.Time) error
|
||||
BeginKafkaConsume(types.TenantID, *types.KafkaConsumeRecord, time.Time) (*types.KafkaConsumeRecord, bool, error)
|
||||
MarkKafkaConsumeSucceeded(types.TenantID, string, string, time.Time) error
|
||||
MarkKafkaConsumeFailed(types.TenantID, string, string, string, string, time.Time) (*types.KafkaConsumeRecord, error)
|
||||
MarkKafkaConsumeDead(types.TenantID, *types.KafkaDeadLetter, time.Time) error
|
||||
ListKafkaDeadLetters(types.TenantID, types.KafkaDeadLetterListOptions) (*types.KafkaDeadLetterPage, error)
|
||||
GetKafkaDeadLetter(types.TenantID, string) (*types.KafkaDeadLetter, error)
|
||||
MarkKafkaDeadLetter(types.TenantID, string, string, string, string, time.Time) error
|
||||
MarkKafkaDeadLetterReplay(types.TenantID, string, string, string, time.Time) (*types.KafkaDeadLetter, error)
|
||||
CreateKafkaOutbox(types.TenantID, *types.KafkaOutboxRecord) error
|
||||
ListPendingKafkaOutbox(types.TenantID, int, time.Time) ([]types.KafkaOutboxRecord, error)
|
||||
MarkKafkaOutboxSent(types.TenantID, string, time.Time) error
|
||||
MarkKafkaOutboxFailed(types.TenantID, string, string, string, time.Time, time.Time) error
|
||||
}
|
||||
|
||||
type httpInfrastructureMapper struct{}
|
||||
@@ -79,3 +91,99 @@ func (httpInfrastructureMapper) MarkEventDelivered(tenantID types.TenantID, even
|
||||
}
|
||||
return infrastructure.TenantBusinessEventMarkDelivered(tenantID, eventID, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) BeginKafkaConsume(tenantID types.TenantID, record *types.KafkaConsumeRecord, deadline time.Time) (*types.KafkaConsumeRecord, bool, error) {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return infrastructure.TenantKafkaConsumeBegin(tenantID, record, deadline)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkKafkaConsumeSucceeded(tenantID types.TenantID, eventID, consumerGroup string, now time.Time) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return infrastructure.TenantKafkaConsumeSucceed(tenantID, eventID, consumerGroup, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkKafkaConsumeFailed(tenantID types.TenantID, eventID, consumerGroup, reason, message string, now time.Time) (*types.KafkaConsumeRecord, error) {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infrastructure.TenantKafkaConsumeFail(tenantID, eventID, consumerGroup, reason, message, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkKafkaConsumeDead(tenantID types.TenantID, dead *types.KafkaDeadLetter, now time.Time) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return infrastructure.TenantKafkaConsumeDead(tenantID, dead, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) ListKafkaDeadLetters(tenantID types.TenantID, opts types.KafkaDeadLetterListOptions) (*types.KafkaDeadLetterPage, error) {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infrastructure.TenantKafkaDeadLetterList(tenantID, opts)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) GetKafkaDeadLetter(tenantID types.TenantID, deadID string) (*types.KafkaDeadLetter, error) {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infrastructure.TenantKafkaDeadLetterGet(tenantID, deadID)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkKafkaDeadLetter(tenantID types.TenantID, deadID, status, resolvedBy, note string, now time.Time) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return infrastructure.TenantKafkaDeadLetterMark(tenantID, deadID, status, resolvedBy, note, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkKafkaDeadLetterReplay(tenantID types.TenantID, deadID, resolvedBy, note string, now time.Time) (*types.KafkaDeadLetter, error) {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infrastructure.TenantKafkaDeadLetterMarkReplay(tenantID, deadID, resolvedBy, note, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) CreateKafkaOutbox(tenantID types.TenantID, record *types.KafkaOutboxRecord) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return infrastructure.TenantKafkaOutboxCreate(tenantID, record)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) ListPendingKafkaOutbox(tenantID types.TenantID, limit int, now time.Time) ([]types.KafkaOutboxRecord, error) {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infrastructure.TenantKafkaOutboxListPending(tenantID, limit, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkKafkaOutboxSent(tenantID types.TenantID, eventID string, now time.Time) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return infrastructure.TenantKafkaOutboxMarkSent(tenantID, eventID, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkKafkaOutboxFailed(tenantID types.TenantID, eventID, reason, message string, nextRetryAt, now time.Time) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return infrastructure.TenantKafkaOutboxMarkFailed(tenantID, eventID, reason, message, nextRetryAt, now)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
adapter "github.com/tinode/chat/server/db"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type InternalAdminQueryPersistenceInterface interface {
|
||||
Query(types.TenantID, string, string, []any, int, int) (*types.InternalAdminQueryResult, error)
|
||||
QueryOne(types.TenantID, string, []any) (map[string]any, error)
|
||||
}
|
||||
|
||||
type internalAdminQueryMapper struct{}
|
||||
|
||||
var InternalAdminQueries InternalAdminQueryPersistenceInterface = internalAdminQueryMapper{}
|
||||
|
||||
func internalAdminQueryAdapter(tenantID types.TenantID) (adapter.TenantInternalAdminQueryAdapter, error) {
|
||||
business, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queryAdp, ok := business.(adapter.TenantInternalAdminQueryAdapter)
|
||||
if !ok {
|
||||
return nil, types.ErrUnsupported
|
||||
}
|
||||
return queryAdp, nil
|
||||
}
|
||||
|
||||
func (internalAdminQueryMapper) Query(tenantID types.TenantID, query string, countQuery string, args []any, page, perPage int) (*types.InternalAdminQueryResult, error) {
|
||||
queryAdp, err := internalAdminQueryAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return queryAdp.TenantInternalAdminQuery(tenantID, query, countQuery, args, page, perPage)
|
||||
}
|
||||
|
||||
func (internalAdminQueryMapper) QueryOne(tenantID types.TenantID, query string, args []any) (map[string]any, error) {
|
||||
queryAdp, err := internalAdminQueryAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return queryAdp.TenantInternalAdminQueryOne(tenantID, query, args)
|
||||
}
|
||||
@@ -330,6 +330,36 @@ func (mr *MockTenantsPersistenceInterfaceMockRecorder) GetByCode(code interface{
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByCode", reflect.TypeOf((*MockTenantsPersistenceInterface)(nil).GetByCode), code)
|
||||
}
|
||||
|
||||
// GetByID mocks base method.
|
||||
func (m *MockTenantsPersistenceInterface) GetByID(id types.TenantID) (*types.Tenant, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetByID", id)
|
||||
ret0, _ := ret[0].(*types.Tenant)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetByID indicates an expected call of GetByID.
|
||||
func (mr *MockTenantsPersistenceInterfaceMockRecorder) GetByID(id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockTenantsPersistenceInterface)(nil).GetByID), id)
|
||||
}
|
||||
|
||||
// ListActive mocks base method.
|
||||
func (m *MockTenantsPersistenceInterface) ListActive(limit int) ([]types.Tenant, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListActive", limit)
|
||||
ret0, _ := ret[0].([]types.Tenant)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListActive indicates an expected call of ListActive.
|
||||
func (mr *MockTenantsPersistenceInterfaceMockRecorder) ListActive(limit interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListActive", reflect.TypeOf((*MockTenantsPersistenceInterface)(nil).ListActive), limit)
|
||||
}
|
||||
|
||||
// MockUsersPersistenceInterface is a mock of UsersPersistenceInterface interface.
|
||||
type MockUsersPersistenceInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
@@ -1220,6 +1250,36 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) Save(tenantID, msg, atta
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).Save), tenantID, msg, attachmentURLs, readBySender)
|
||||
}
|
||||
|
||||
// PreparePersist mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) PreparePersist(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PreparePersist", tenantID, msg, attachmentURLs, readBySender)
|
||||
ret0, _ := ret[0].(*types.MessageSaveResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PreparePersist indicates an expected call of PreparePersist.
|
||||
func (mr *MockMessagesPersistenceInterfaceMockRecorder) PreparePersist(tenantID, msg, attachmentURLs, readBySender interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreparePersist", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).PreparePersist), tenantID, msg, attachmentURLs, readBySender)
|
||||
}
|
||||
|
||||
// PersistPrepared mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) PersistPrepared(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PersistPrepared", tenantID, msg, attachmentURLs, readBySender)
|
||||
ret0, _ := ret[0].(*types.MessageSaveResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PersistPrepared indicates an expected call of PersistPrepared.
|
||||
func (mr *MockMessagesPersistenceInterfaceMockRecorder) PersistPrepared(tenantID, msg, attachmentURLs, readBySender interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PersistPrepared", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).PersistPrepared), tenantID, msg, attachmentURLs, readBySender)
|
||||
}
|
||||
|
||||
// MockDevicePersistenceInterface is a mock of DevicePersistenceInterface interface.
|
||||
type MockDevicePersistenceInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
|
||||
+115
-2
@@ -151,6 +151,7 @@ const (
|
||||
type TenantsPersistenceInterface interface {
|
||||
GetByCode(code string) (*types.Tenant, error)
|
||||
GetByID(id types.TenantID) (*types.Tenant, error)
|
||||
ListActive(limit int) ([]types.Tenant, error)
|
||||
}
|
||||
|
||||
type tenantsMapper struct{}
|
||||
@@ -158,6 +159,22 @@ type tenantsMapper struct{}
|
||||
// Tenants is the tenant registry access object.
|
||||
var Tenants TenantsPersistenceInterface
|
||||
|
||||
// AdminInternalConfigProvider optionally resolves hotgo-owned configuration
|
||||
// through the Admin service instead of reading the hotgo database directly.
|
||||
type AdminInternalConfigProvider interface {
|
||||
GetTenantByCode(code string) (*types.Tenant, error)
|
||||
GetTenantByID(id types.TenantID) (*types.Tenant, error)
|
||||
ListActiveTenants(limit int) ([]types.Tenant, error)
|
||||
RegistrationSMSEnabled() (bool, error)
|
||||
ThirdPartyConfig(group string, keys ...string) (map[string]string, error)
|
||||
}
|
||||
|
||||
var adminInternalConfigProvider AdminInternalConfigProvider
|
||||
|
||||
func SetAdminInternalConfigProvider(provider AdminInternalConfigProvider) {
|
||||
adminInternalConfigProvider = provider
|
||||
}
|
||||
|
||||
// RegistrationConfigPersistenceInterface exposes global registration policy.
|
||||
type RegistrationConfigPersistenceInterface interface {
|
||||
SMSEnabled() (bool, error)
|
||||
@@ -172,6 +189,14 @@ func (registrationConfigMapper) SMSEnabled() (bool, error) {
|
||||
if enabled, ok := registrationSMSCache.get("im:register_sms_enabled"); ok {
|
||||
return enabled, nil
|
||||
}
|
||||
if adminInternalConfigProvider != nil {
|
||||
if enabled, err := adminInternalConfigProvider.RegistrationSMSEnabled(); err == nil {
|
||||
registrationSMSCache.set("im:register_sms_enabled", enabled)
|
||||
return enabled, nil
|
||||
} else {
|
||||
logs.Warn.Printf("admin internal registration config fallback failed: %v", err)
|
||||
}
|
||||
}
|
||||
configAdp, ok := adp.(adapter.RegistrationConfigAdapter)
|
||||
if !ok {
|
||||
return true, types.ErrUnsupported
|
||||
@@ -198,6 +223,14 @@ func (thirdPartyConfigMapper) Get(group string, keys ...string) (map[string]stri
|
||||
if cached, ok := thirdPartyConfigCache.get(cacheKey); ok {
|
||||
return cloneStringMap(cached), nil
|
||||
}
|
||||
if adminInternalConfigProvider != nil {
|
||||
if result, err := adminInternalConfigProvider.ThirdPartyConfig(group, keys...); err == nil {
|
||||
thirdPartyConfigCache.set(cacheKey, cloneStringMap(result))
|
||||
return result, nil
|
||||
} else {
|
||||
logs.Warn.Printf("admin internal third-party config fallback failed: group=%s err=%v", group, err)
|
||||
}
|
||||
}
|
||||
configAdp, ok := adp.(adapter.ThirdPartyConfigAdapter)
|
||||
if !ok {
|
||||
return nil, types.ErrUnsupported
|
||||
@@ -261,6 +294,15 @@ func (tenantsMapper) GetByCode(code string) (*types.Tenant, error) {
|
||||
if tenant, ok := tenantByCodeCache.get(code); ok {
|
||||
return cloneTenant(tenant), nil
|
||||
}
|
||||
if adminInternalConfigProvider != nil {
|
||||
if tenant, err := adminInternalConfigProvider.GetTenantByCode(code); err == nil && tenant != nil {
|
||||
tenantByCodeCache.set(code, cloneTenant(tenant))
|
||||
tenantByIDCache.set(strconv.FormatInt(int64(tenant.ID), 10), cloneTenant(tenant))
|
||||
return tenant, nil
|
||||
} else if err != nil {
|
||||
logs.Warn.Printf("admin internal tenant by code fallback failed: code=%s err=%v", code, err)
|
||||
}
|
||||
}
|
||||
tenantAdp, ok := adp.(adapter.TenantAdapter)
|
||||
if !ok {
|
||||
return nil, types.ErrUnsupported
|
||||
@@ -282,6 +324,15 @@ func (tenantsMapper) GetByID(id types.TenantID) (*types.Tenant, error) {
|
||||
if tenant, ok := tenantByIDCache.get(idKey); ok {
|
||||
return cloneTenant(tenant), nil
|
||||
}
|
||||
if adminInternalConfigProvider != nil {
|
||||
if tenant, err := adminInternalConfigProvider.GetTenantByID(id); err == nil && tenant != nil {
|
||||
tenantByIDCache.set(idKey, cloneTenant(tenant))
|
||||
tenantByCodeCache.set(tenant.Code, cloneTenant(tenant))
|
||||
return tenant, nil
|
||||
} else if err != nil {
|
||||
logs.Warn.Printf("admin internal tenant by id fallback failed: tenant=%d err=%v", id, err)
|
||||
}
|
||||
}
|
||||
tenantAdp, ok := adp.(adapter.TenantAdapter)
|
||||
if !ok {
|
||||
return nil, types.ErrUnsupported
|
||||
@@ -294,6 +345,29 @@ func (tenantsMapper) GetByID(id types.TenantID) (*types.Tenant, error) {
|
||||
return tenant, err
|
||||
}
|
||||
|
||||
func (tenantsMapper) ListActive(limit int) ([]types.Tenant, error) {
|
||||
if limit <= 0 {
|
||||
limit = 1000
|
||||
}
|
||||
if adminInternalConfigProvider != nil {
|
||||
if tenants, err := adminInternalConfigProvider.ListActiveTenants(limit); err == nil {
|
||||
for i := range tenants {
|
||||
tenant := tenants[i]
|
||||
tenantByIDCache.set(strconv.FormatInt(int64(tenant.ID), 10), cloneTenant(&tenant))
|
||||
tenantByCodeCache.set(tenant.Code, cloneTenant(&tenant))
|
||||
}
|
||||
return tenants, nil
|
||||
} else {
|
||||
logs.Warn.Printf("admin internal active tenants fallback failed: err=%v", err)
|
||||
}
|
||||
}
|
||||
tenantAdp, ok := adp.(adapter.TenantAdapter)
|
||||
if !ok {
|
||||
return nil, types.ErrUnsupported
|
||||
}
|
||||
return tenantAdp.TenantListActive(limit)
|
||||
}
|
||||
|
||||
func tenantAuthAdapter(tenantID types.TenantID) (adapter.TenantAuthAdapter, error) {
|
||||
if !tenantID.IsValid() {
|
||||
return nil, types.ErrMalformed
|
||||
@@ -1267,6 +1341,8 @@ func subscriptionCacheID(topic string, user types.Uid, keepDeleted bool) string
|
||||
// MessagesPersistenceInterface is an interface which defines methods for persistent storage of messages.
|
||||
type MessagesPersistenceInterface interface {
|
||||
Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error)
|
||||
PreparePersist(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error)
|
||||
PersistPrepared(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error)
|
||||
GetOne(tenantID types.TenantID, topic string, seq int64) (*types.Message, error)
|
||||
Recall(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error)
|
||||
Edit(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, head types.KVMap, content any, reason string, editedAt time.Time) (*types.Message, error)
|
||||
@@ -3063,6 +3139,17 @@ func cachedBlockContains(tenantID types.TenantID, blocker, blocked types.Uid) (b
|
||||
|
||||
// Save message
|
||||
func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||
saveResult, err := Messages.PreparePersist(tenantID, msg, attachmentURLs, readBySender)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if saveResult != nil && saveResult.Duplicate {
|
||||
return saveResult, nil
|
||||
}
|
||||
return Messages.PersistPrepared(tenantID, msg, attachmentURLs, readBySender)
|
||||
}
|
||||
|
||||
func (messagesMapper) PreparePersist(tenantID types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||
if msg == nil || msg.TenantID != tenantID {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
@@ -3126,12 +3213,29 @@ func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachme
|
||||
}
|
||||
msg.InitTimes()
|
||||
msg.SetUid(Store.GetUid())
|
||||
// Increment topic's or user's SeqId
|
||||
// Reserve topic's or user's SeqId before optional async persistence.
|
||||
err = tenantAdp.TenantTopicUpdateOnMessage(tenantID, msg.Topic, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: msg.SeqId}, nil
|
||||
}
|
||||
|
||||
func (messagesMapper) PersistPrepared(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||
if msg == nil || msg.TenantID != tenantID || msg.Topic == "" || msg.SeqId <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientMsgID, err := normalizeClientMsgID(msg.Head)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.ClientMsgID = clientMsgID
|
||||
fromUid := types.ParseUid(msg.From)
|
||||
isGroupMessage := types.GetTopicCat(msg.Topic) == types.TopicCatGrp
|
||||
if isGroupMessage {
|
||||
err = tenantAdp.TenantGroupMessageSave(tenantID, msg)
|
||||
} else {
|
||||
@@ -3220,6 +3324,14 @@ func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachme
|
||||
return &types.MessageSaveResult{SeqId: msg.SeqId, MarkedReadBySender: markedReadBySender}, nil
|
||||
}
|
||||
|
||||
func PrepareMessagePersist(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||
return Messages.PreparePersist(tenantID, msg, attachmentURLs, readBySender)
|
||||
}
|
||||
|
||||
func PersistPreparedMessage(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||
return Messages.PersistPrepared(tenantID, msg, attachmentURLs, readBySender)
|
||||
}
|
||||
|
||||
func retryTenantMessageSaveAfterSeqConflict(tenantAdp adapter.TenantBusinessAdapter, tenantID types.TenantID, msg *types.Message) error {
|
||||
latest, err := tenantAdp.TenantMessageGetAll(tenantID, msg.Topic, types.ZeroUid, &types.QueryOpt{Before: 1 << 62, Limit: 1})
|
||||
if err != nil {
|
||||
@@ -3821,7 +3933,7 @@ func (fileMapper) LinkAttachments(tenantID types.TenantID, topic string, msgId t
|
||||
|
||||
if len(fids) > 0 {
|
||||
userId := types.ZeroUid
|
||||
if types.GetTopicCat(topic) == types.TopicCatMe {
|
||||
if topic != "" && types.GetTopicCat(topic) == types.TopicCatMe {
|
||||
userId = types.ParseUserId(topic)
|
||||
topic = ""
|
||||
}
|
||||
@@ -4353,6 +4465,7 @@ func init() {
|
||||
Tenants = tenantsMapper{}
|
||||
RegistrationConfig = registrationConfigMapper{}
|
||||
TenantEntry = tenantEntryMapper{}
|
||||
InternalAdminQueries = internalAdminQueryMapper{}
|
||||
Users = usersMapper{}
|
||||
Topics = topicsMapper{}
|
||||
Subs = subsMapper{}
|
||||
|
||||
@@ -12,6 +12,21 @@ const (
|
||||
|
||||
BusinessEventPending = "pending"
|
||||
BusinessEventDelivered = "delivered"
|
||||
|
||||
KafkaConsumeProcessing = "processing"
|
||||
KafkaConsumeSucceeded = "succeeded"
|
||||
KafkaConsumeFailed = "failed"
|
||||
KafkaConsumeDead = "dead"
|
||||
|
||||
KafkaDeadStatusDead = "dead"
|
||||
KafkaDeadStatusPendingReplay = "pending_replay"
|
||||
KafkaDeadStatusReplayed = "replayed"
|
||||
KafkaDeadStatusIgnored = "ignored"
|
||||
KafkaDeadStatusResolved = "resolved"
|
||||
|
||||
KafkaOutboxPending = "pending"
|
||||
KafkaOutboxSent = "sent"
|
||||
KafkaOutboxFailed = "failed"
|
||||
)
|
||||
|
||||
// HTTPIdempotencyRecord stores the first response for one authenticated write.
|
||||
@@ -46,6 +61,80 @@ type BusinessEvent struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// KafkaConsumeRecord stores idempotent consumer state for one event and group.
|
||||
type KafkaConsumeRecord struct {
|
||||
EventID string
|
||||
ConsumerGroup string
|
||||
SourceTopic string
|
||||
EventType string
|
||||
BizType string
|
||||
BizID string
|
||||
Status string
|
||||
RetryCount int
|
||||
LastReason string
|
||||
LastError string
|
||||
ProcessingDeadline *time.Time
|
||||
SucceededAt *time.Time
|
||||
DeadAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// KafkaDeadLetter stores a poison Kafka event for manual handling.
|
||||
type KafkaDeadLetter struct {
|
||||
ID int64
|
||||
DeadID string
|
||||
EventID string
|
||||
ParentEventID string
|
||||
SourceTopic string
|
||||
ConsumerGroup string
|
||||
EventType string
|
||||
BizType string
|
||||
BizID string
|
||||
Envelope []byte
|
||||
RetryCount int
|
||||
LastReason string
|
||||
LastError string
|
||||
Status string
|
||||
ReplayCount int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type KafkaDeadLetterListOptions struct {
|
||||
Status string
|
||||
BizType string
|
||||
BizID string
|
||||
EventType string
|
||||
Cursor string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type KafkaDeadLetterPage struct {
|
||||
Items []KafkaDeadLetter
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// KafkaOutboxRecord stores a Kafka event waiting for reliable publication.
|
||||
type KafkaOutboxRecord struct {
|
||||
ID int64
|
||||
EventID string
|
||||
Topic string
|
||||
EventType string
|
||||
BizType string
|
||||
BizID string
|
||||
Key string
|
||||
Envelope []byte
|
||||
Status string
|
||||
Attempts int
|
||||
NextRetryAt *time.Time
|
||||
SentAt *time.Time
|
||||
LastReason string
|
||||
LastError string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// StableEventID derives an idempotent event identifier from domain identity.
|
||||
func StableEventID(parts ...any) string {
|
||||
hash := sha256.New()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package types
|
||||
|
||||
type InternalAdminQueryResult struct {
|
||||
Items []map[string]any
|
||||
Total int64
|
||||
Page int
|
||||
PerPage int
|
||||
}
|
||||
Reference in New Issue
Block a user